+ );
MockAppsManagement.displayName = 'MockAppsManagement';
return MockAppsManagement;
});
@@ -140,6 +142,9 @@ describe('V2SettingsPage', () => {
expect(screen.getByRole('img', { name: 'lily avatar' })).toHaveAttribute('src', '/uploads/lily.png');
expect(screen.getByRole('img', { name: 'lily avatar' })).toHaveAttribute('data-kind', 'human');
expect(screen.getByText('Connected app controls')).toHaveAttribute('data-variant', 'settings');
+ expect(screen.getByText('Connected app controls')).toHaveAttribute('data-show-integrations', 'false');
+ expect(screen.getByRole('link', { name: /Connectors/ })).toHaveAttribute('href', '/v2/connectors');
+ expect(screen.getByText('Developer apps').closest('details')).not.toHaveAttribute('open');
expect(screen.getByRole('button', { name: 'Regenerate' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Upload photo' })).toBeInTheDocument();
expect(screen.getByText('Regenerate for another, or upload a photo.')).toBeInTheDocument();
diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
index 87fa690f1d..02d666eefc 100644
--- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
+++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
@@ -980,7 +980,9 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(settingsPage).toContain('className="v2-settings__avatar"');
expect(settingsPage).toContain('src={currentUser?.profilePicture || undefined}');
expect(settingsPage).toContain('kind="human"');
- expect(settingsPage).toContain('');
+ expect(settingsPage).toContain('');
+ expect(settingsPage).toContain('to="/v2/connectors"');
+ expect(settingsPage).toContain('Developer apps');
expect(settingsPage).toContain('');
expect(settingsPage).toContain('');
expect(ruleBody(v2, '.v2-settings__avatar')).toContain('width: 64px');
@@ -2873,10 +2875,10 @@ describe('the landing hero demo (TASK-147)', () => {
// (5, 6), and the avatar is a 4px square with a cobalt live dot (7).
//
// Items 1–6 are presence lines. The reason they can be, rather than a cascade
-// comparison: `button.v2-byo__mode` and `button.v2-byo__submit` are already
-// rooted at `.v2-root`, so the bare reset at `v2.css:201` cannot outrank them
-// the way it did the landing pill in TASK-159. That trap is pre-avoided in this
-// component, not merely untested here.
+// comparison: the mode card owns its button reset locally, while
+// `button.v2-byo__submit` is rooted at `.v2-root`, so the bare reset at
+// `v2.css:201` cannot outrank them the way it did the landing pill in TASK-159.
+// That trap is pre-avoided in this component, not merely untested here.
//
// Item 7 gets three assertions, because each of its failure modes is invisible
// to a presence line:
@@ -2957,7 +2959,7 @@ describe('Bring your own agent onto Signal (TASK-166)', () => {
expect(hover['border-color']).toBe('var(--v2-ink-hover)');
});
- test('the chosen mode card is a 2px accent border that holds its box (item 2)', () => {
+ test('the mode cards keep button-specific card styling (item 2)', () => {
const base = cssDeclarations(ruleBody(v2, '.v2-root button.v2-byo__mode'));
expect(base['border-radius']).toBe('4px');
// The 1px-per-side compensation is derived from the base shorthand, so it is
@@ -2969,6 +2971,8 @@ describe('Bring your own agent onto Signal (TASK-166)', () => {
);
expect(pressed.border).toBe('2px solid var(--v2-accent)');
expect(pressed.padding).toBe('15px 17px 13px');
+ expect(v2).toContain('.v2-root button.v2-byo__mode:hover');
+ expect(v2).toContain('.v2-root button.v2-byo__mode:focus-visible');
// (c) the ring belongs to :focus-visible, and a ring here is the defect.
expect(pressed['box-shadow']).toBeUndefined();
expect(
@@ -3133,9 +3137,10 @@ describe('Bring your own agent onto Signal (TASK-166)', () => {
// The negative that says what the fold-in removed: no status colour and no
// pill fill anywhere in this block. A future edit that adds one back is the
// regression, and a presence check on the new values cannot see it.
- const to = v2.indexOf('.v2-byo__preview-note {');
- expect(to).toBeGreaterThan(sectionFrom);
- const block = v2.slice(sectionFrom, to);
+ expect(v2).not.toContain('.v2-byo__preview-note {');
+ const blockEnd = v2.indexOf('.v2-byo__result-hero {', sectionFrom);
+ expect(blockEnd).toBeGreaterThan(sectionFrom);
+ const block = v2.slice(sectionFrom, blockEnd);
['--v2-success', '--v2-success-text', '--v2-success-soft', '--v2-warning', '--v2-surface-hover']
.forEach((token) => expect(block).not.toContain(token));
diff --git a/frontend/src/v2/__tests__/v2-type-floors.test.ts b/frontend/src/v2/__tests__/v2-type-floors.test.ts
index b5b11049f6..c760c82218 100644
--- a/frontend/src/v2/__tests__/v2-type-floors.test.ts
+++ b/frontend/src/v2/__tests__/v2-type-floors.test.ts
@@ -134,7 +134,6 @@ const ALLOWLIST: string[] = [
// TASK-166 took `.v2-byo__mode-kicker` (10.0px, no family) and
// `.v2-byo__preview-label` (11.0px, no family) to mono 500 11px, so both
// entries left this burn-down list rather than being updated in place.
- '.v2-byo__mode-meta', // 11.0px — no-mono-family
// TASK-169 took `.v2-byo__stat-label` (11.0px, no family) to mono 500 11px
// with item 2, so its entry left this burn-down list rather than being
// updated in place.
diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx
index 55f1c5e31f..c77dd179c7 100644
--- a/frontend/src/v2/components/V2AgentBYO.tsx
+++ b/frontend/src/v2/components/V2AgentBYO.tsx
@@ -21,6 +21,7 @@ import { useTranslation } from 'react-i18next';
import axios from '../../utils/axiosConfig';
import V2FeaturePage from './V2FeaturePage';
import V2Avatar from './V2Avatar';
+import V2Hint from './V2Hint';
import { useV2Api } from '../hooks/useV2Api';
import { useAuth } from '../../context/AuthContext';
import { V2Pod } from '../hooks/useV2Pods';
@@ -167,6 +168,7 @@ const V2AgentBYO: React.FC = () => {
const [pendingReplacement, setPendingReplacement] = useState<{ agentName: string; podId: string } | null>(null);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);
+ const [errorField, setErrorField] = useState<'machine' | 'name' | 'pod' | null>(null);
const [issued, setIssued] = useState<{ token: string; agentName: string; podId: string; issuedAt: number } | null>(null);
const [copied, setCopied] = useState(null);
// Which CLI the listener runs. Pinned in the command (`--adapter`), so a Mac
@@ -247,9 +249,6 @@ const V2AgentBYO: React.FC = () => {
// must not open onto a page that shows no command. Elsewhere the full page
// says why and what works instead.
const showCompactByo = compactByo && listenerAvailable && !showCompactHosted && !personaCard && mode === 'byo';
- // One lookup, two readers (TASK-163): the preview note and the placement
- // payload must name the SAME machine, so it is derived once here rather
- // than restated at each call site.
const selectedMachineName = machines.find((m) => m.machineId === machineId)?.name || machineId;
// Model choice for the on-my-computer path. Adapter and model travel
// together: a Codex model slug must never be left behind if the user flips
@@ -498,6 +497,7 @@ const V2AgentBYO: React.FC = () => {
// here either), and starts the agent — the page watches it come up.
const submitMachine = async (cleanName: string) => {
if (!machineId) {
+ setErrorField('machine');
setError(t('agentByo.errors.machineRequired'));
return;
}
@@ -548,13 +548,16 @@ const V2AgentBYO: React.FC = () => {
const submit = async () => {
setError(null);
+ setErrorField(null);
setPendingReplacement(null);
const cleanName = sanitizeAgentName(name);
if (!cleanName) {
+ setErrorField('name');
setError(t('agentByo.errors.nameRequired'));
return;
}
if (!podId) {
+ setErrorField('pod');
setError(t('agentByo.errors.podRequired'));
return;
}
@@ -758,6 +761,8 @@ const V2AgentBYO: React.FC = () => {
}
};
+ const chooseMode = (nextMode: 'hosted' | 'byo' | 'machine') => setMode(nextMode);
+
const apiUrl = typeof window !== 'undefined' && /api-dev|api\./.test(window.location.hostname)
? `https://${window.location.hostname.replace(/^app/, 'api')}`
: 'https://api.commonly.me';
@@ -916,52 +921,67 @@ const V2AgentBYO: React.FC = () => {
<>