Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions frontend/src/components/AppsManagement.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,19 @@ describe('AppsManagement ingest tokens', () => {
expect(screen.getByText('Commonly Apps').parentElement?.querySelector('svg')).not.toBeInTheDocument();
expect(screen.getByText('Existing Integrations').parentElement?.querySelector('svg')).not.toBeInTheDocument();
});

it('skips the integrations fetch when Settings only needs developer apps', async () => {
render(
<AuthContext.Provider value={{ user: { role: 'member' } }}>
<AppsManagement variant="settings" showIntegrations={false} />
</AuthContext.Provider>,
);

await screen.findByText('Commonly Apps');
await waitFor(() => expect(axios.get).toHaveBeenCalledWith('/api/apps', {
headers: { Authorization: 'Bearer t' },
}));
expect(axios.get.mock.calls.some(([url]) => url === '/api/integrations/user/all')).toBe(false);
expect(screen.queryByText('Existing Integrations')).not.toBeInTheDocument();
});
});
14 changes: 9 additions & 5 deletions frontend/src/components/AppsManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ interface AppsManagementProps {
* the same functional controls without duplicating its page-level heading.
*/
variant?: 'default' | 'settings';
/** Settings keeps connector rows on the Connectors page; this surface only
* needs the developer-app forms inside its collapsed disclosure. */
showIntegrations?: boolean;
}

interface AppSurfaceProps {
Expand All @@ -114,7 +117,7 @@ const AppSurface: React.FC<AppSurfaceProps> = ({ children, className, outlined =
: <Card className={className} variant={outlined ? 'outlined' : undefined} sx={spaceAfter ? { mb: 3 } : undefined}>{children}</Card>
);

const AppsManagement: React.FC<AppsManagementProps> = ({ variant = 'default' }) => {
const AppsManagement: React.FC<AppsManagementProps> = ({ variant = 'default', showIntegrations = true }) => {
const isSettingsVariant = variant === 'settings';
const { user } = useContext(AuthContext);
const navigate = useNavigate();
Expand Down Expand Up @@ -176,9 +179,10 @@ const AppsManagement: React.FC<AppsManagementProps> = ({ variant = 'default' })
};

useEffect(() => {
fetchIntegrations();
if (showIntegrations) fetchIntegrations();
else setLoading(false);
fetchApps();
}, [user]);
}, [user, showIntegrations]);

const handleDeleteClick = (integration: Integration): void => {
setSelectedIntegration(integration);
Expand Down Expand Up @@ -568,7 +572,7 @@ const AppsManagement: React.FC<AppsManagementProps> = ({ variant = 'default' })
</CardContent>
</AppSurface>

<AppSurface className="apps-management__section" settings={isSettingsVariant}>
{showIntegrations && <AppSurface className="apps-management__section" settings={isSettingsVariant}>
<CardContent>
<Box display="flex" alignItems="center" mb={2}>
{!isSettingsVariant && <SettingsIcon color="primary" />}
Expand Down Expand Up @@ -642,7 +646,7 @@ const AppsManagement: React.FC<AppsManagementProps> = ({ variant = 'default' })
</Grid>
)}
</CardContent>
</AppSurface>
</AppSurface>}
</Grid>

{!isSettingsVariant && (
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/i18n/__tests__/i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,10 @@ describe('i18n configuration', () => {
expect(i18n.t('auth.register.success.checkEmailTitle', { lng: 'zh-CN' })).toBe('欢迎加入');
expect(`${banner}${screen}`).not.toMatch(/群组|—/);
});

it('uses a Chinese comma in the BYO machine hint', () => {
const hint = i18n.t('agentByo.mode.machineHint', { lng: 'zh-CN' });
expect(hint).toBe('在你已添加的电脑上运行,你的文件、你的 CLI');
expect(hint).not.toContain('—');
});
});
7 changes: 5 additions & 2 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1469,12 +1469,16 @@
"mode": {
"label": "Where should it run?",
"hosted": "Run it here",
"hostedHint": "Commonly runs it for you. Nothing to install. Beta: {{turns}} turns a day.",
"byo": "Use my own agent",
"byoHint": "Your Claude Code or Codex, on your computer.",
"onCommonly": "on Commonly",
"yours": "your machine",
"hostedMeta": "Live in a few seconds · Beta: {{turns}} turns a day",
"byoMeta": "One command in Terminal",
"moreAbout": "More about {{mode}}",
"machine": "On my computer",
"machineHint": "Runs on a computer you've added — your files, your CLIs",
"machineMeta": "Needs the Commonly daemon running there"
},
"hosted": {
Expand All @@ -1493,8 +1497,7 @@
"draft": "not created yet",
"waiting": "not listening yet",
"starting": "starting…",
"live": "listening",
"noteMachine": "Appears when the daemon on {{machine}} starts it."
"live": "listening"
},
"persona": {
"hiring": "Hiring"
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1529,12 +1529,16 @@
"mode": {
"label": "在哪里运行?",
"hosted": "在这里运行",
"hostedHint": "由 Commonly 代为运行,无需安装。测试期:每天{{turns}}轮。",
"byo": "用我自己的智能体",
"byoHint": "你电脑上的 Claude Code 或 Codex。",
"onCommonly": "在 Commonly 上",
"yours": "你的电脑",
"hostedMeta": "几秒即可上线 · 测试期每天{{turns}}轮",
"byoMeta": "只需一条终端命令",
"moreAbout": "更多关于{{mode}}的信息",
"machine": "在我的电脑上",
"machineHint": "在你已添加的电脑上运行,你的文件、你的 CLI",
"machineMeta": "需要那台电脑运行 Commonly 守护进程"
},
"hosted": {
Expand All @@ -1553,8 +1557,7 @@
"draft": "尚未创建",
"waiting": "尚未监听",
"starting": "正在启动…",
"live": "监听中",
"noteMachine": "{{machine}} 上的守护进程启动后会显示在 Pod 中。"
"live": "监听中"
},
"persona": {
"hiring": "正在雇佣"
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/v2/__tests__/V2AgentBYOHosted.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ describe('V2AgentBYO — Run it here', () => {

await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument());
expect(screen.getByRole('group', { name: 'Where should it run?' })).toBeInTheDocument();
expect(screen.getByText('Live in a few seconds · Beta: 200 turns a day')).toBeInTheDocument();
expect(document.body.textContent).not.toContain('{{');
expect(screen.getByTestId('byo-mode-hosted')).toHaveAttribute('aria-pressed', 'false');
expect(screen.getByTestId('byo-mode-byo')).toHaveAttribute('aria-pressed', 'false');
expect(screen.getByTestId('byo-mode-unchosen')).toHaveTextContent('Choose where it runs first');
Expand Down Expand Up @@ -128,7 +130,7 @@ describe('V2AgentBYO — Run it here', () => {
renderPage();

await waitFor(() => expect(screen.getByTestId('byo-mode-hosted')).toBeInTheDocument());
expect(screen.getByText(/beta: 200 turns a day/i)).toBeInTheDocument();
expect(screen.getAllByText(/beta: 200 turns a day/i)).toHaveLength(2);
fireEvent.click(screen.getByTestId('byo-mode-hosted'));
fireEvent.click(screen.getByRole('button', { name: /^run it here$/i }));

Expand Down
36 changes: 25 additions & 11 deletions frontend/src/v2/__tests__/V2AgentBYOMachine.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,29 +78,43 @@ describe('BYO on-my-computer mode', () => {
expect(screen.getByTestId('byo-machine-select')).toHaveTextContent('Sam’s MacBook');
});

test('the preview note names the daemon machine in "on my computer" mode (#TASK-163)', async () => {
// Before TASK-163 the note read "Appears in the pod once your runtime
// connects with the token" in EVERY non-hosted mode, and this mode issues
// no token at all — ux-lead measured it drawing README frame 5. The name
// is derived once from machines+machineId, so this pins that it FOLLOWS
// the picker rather than merely being present.
test('adapter and model edits do not clear an unrelated field error', async () => {
mockGet();
renderPage();
await waitFor(() => expect(screen.getByTestId('byo-mode-machine')).toBeInTheDocument());
fireEvent.click(screen.getByTestId('byo-mode-machine'));
await waitFor(() => expect(screen.getByText('Add to this computer')).toBeEnabled());

fireEvent.change(screen.getByPlaceholderText('my-mcp-agent'), { target: { value: ' ' } });
fireEvent.click(screen.getByText('Add to this computer'));

const nameField = screen.getByPlaceholderText('my-mcp-agent').closest('label');
expect(nameField).toHaveClass('v2-byo__field--error');

fireEvent.change(screen.getByTestId('byo-adapter-select'), { target: { value: 'codex' } });
expect(nameField).toHaveClass('v2-byo__field--error');
fireEvent.change(screen.getByTestId('byo-model-select'), { target: { value: 'gpt-6-sol' } });
expect(nameField).toHaveClass('v2-byo__field--error');
});

test('the preview stays visual-only when the mode or machine changes', async () => {
const second = { id: 'm2', machineId: 'mach-b', name: 'Studio', status: 'online', agentStates: [] };
mockGet({ machines: [machineRow, second] });
renderPage();
await waitFor(() => expect(screen.getByTestId('byo-mode-machine')).toBeInTheDocument());

// The preview's name and status carry the meaning until a machine is chosen.
expect(screen.getByTestId('byo-preview').querySelector('.v2-byo__preview-note')).toBeNull();
const preview = screen.getByTestId('byo-preview');
expect(preview).not.toHaveTextContent('This is how it will appear in the pod. The face follows the name.');

// On "Bring your own runtime", the token wording is correct.
fireEvent.click(screen.getByTestId('byo-mode-byo'));
expect(screen.getByTestId('byo-preview').querySelector('.v2-byo__preview-note')).toBeNull();
expect(preview).not.toHaveTextContent('Appears in the pod once the command is running.');

fireEvent.click(screen.getByTestId('byo-mode-machine'));
expect(screen.getByTestId('byo-preview')).toHaveTextContent('Appears when the daemon on Sam’s MacBook starts it.');
expect(preview).not.toHaveTextContent('Appears in the pod once the daemon on Sam’s MacBook starts it.');

fireEvent.change(screen.getByTestId('byo-machine-select'), { target: { value: 'mach-b' } });
expect(screen.getByTestId('byo-preview')).toHaveTextContent('Appears when the daemon on Studio starts it.');
expect(preview).not.toHaveTextContent('Appears in the pod once the daemon on Studio starts it.');
});

test('no machines — no card, and the one-paste setup panel shows instead', async () => {
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/v2/__tests__/V2SettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ jest.mock('../../utils/axiosConfig', () => {
});

jest.mock('../../components/AppsManagement', () => {
const MockAppsManagement = ({ variant }: { variant?: string }) => <div data-variant={variant}>Connected app controls</div>;
const MockAppsManagement = ({ variant, showIntegrations }: { variant?: string; showIntegrations?: boolean }) => (
<div data-variant={variant} data-show-integrations={String(showIntegrations)}>Connected app controls</div>
);
MockAppsManagement.displayName = 'MockAppsManagement';
return MockAppsManagement;
});
Expand Down Expand Up @@ -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();
Expand Down
23 changes: 14 additions & 9 deletions frontend/src/v2/__tests__/v2-layout-invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<AppsManagement variant="settings" />');
expect(settingsPage).toContain('<AppsManagement variant="settings" showIntegrations={false} />');
expect(settingsPage).toContain('to="/v2/connectors"');
expect(settingsPage).toContain('<summary>Developer apps</summary>');
expect(settingsPage).toContain('<V2BillingPanel showHeading={false} />');
expect(settingsPage).toContain('<V2DevicesPanel showHeading={false} />');
expect(ruleBody(v2, '.v2-settings__avatar')).toContain('width: 64px');
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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));

Expand Down
1 change: 0 additions & 1 deletion frontend/src/v2/__tests__/v2-type-floors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading