From 28cadf63ff7d5149bd04f5b0331b1f89bafb5900 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 08:01:18 -0800 Subject: [PATCH 1/5] feat(agent-settings): lift policy, identity, grants, runtime and own key from the frame Identity, How it runs (machine, adapter/model/effort, own API key), Connector grants and Policy now render as the artboard's grouped lists: lrow rows, details.disc disclosures, .field/.sel controls and alert rows, styled from v2-agent-settings-surface.css. V2AgentRuntimeGrants.css and V2RuntimeControls.css are deleted; the identity/listing/owned rules leave v2-agent-settings.css and the trust/quota rules leave v2-agent-policy.css. Behaviour kept: every handler, API call and error path. New behaviour from the frame notes: trust mode is a segment with Saved/error in the action slot (Saved clears after a moment), the name refusal sits under the field, grant load failure is a row with Try again, and a failed revoke is its own line above the rows. Focus rings use --v2-focus-ring; 44px targets on phones. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- frontend/src/i18n/locales/en.json | 17 +- frontend/src/i18n/locales/zh-CN.json | 15 +- .../src/v2/__tests__/V2AgentPolicy.test.tsx | 31 +++- .../__tests__/V2AgentRuntimeGrants.test.tsx | 43 +++++ .../v2/__tests__/V2AgentSettingsPage.test.tsx | 19 +++ .../v2/__tests__/V2RuntimeControls.test.tsx | 29 +++- .../v2/__tests__/v2-layout-invariants.test.ts | 66 ++++---- .../src/v2/agents/V2AgentIdentitySection.tsx | 160 ++++++++++++------ frontend/src/v2/agents/V2AgentPolicy.tsx | 66 +++++--- .../src/v2/agents/V2AgentSettingsIcon.tsx | 32 ++++ frontend/src/v2/agents/v2-agent-policy.css | 11 +- .../v2/agents/v2-agent-settings-surface.css | 137 ++++++++++++++- frontend/src/v2/agents/v2-agent-settings.css | 35 ---- .../src/v2/components/V2AgentProviderKey.tsx | 49 ++++-- .../v2/components/V2AgentRuntimeGrants.css | 24 --- .../v2/components/V2AgentRuntimeGrants.tsx | 158 ++++++++++------- .../src/v2/components/V2RuntimeControls.css | 48 ------ .../src/v2/components/V2RuntimeControls.tsx | 63 ++++--- 18 files changed, 667 insertions(+), 336 deletions(-) create mode 100644 frontend/src/v2/agents/V2AgentSettingsIcon.tsx delete mode 100644 frontend/src/v2/components/V2AgentRuntimeGrants.css delete mode 100644 frontend/src/v2/components/V2RuntimeControls.css diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index e9a07edc29..f6dcd6ee7f 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2775,7 +2775,8 @@ "targetUnavailable": "That computer is no longer available. Choose another computer.", "placementChanged": "This agent’s computer changed. Review its current placement before moving it.", "placementError": "Could not load this agent’s computer placement.", - "moveError": "Could not move this agent. Try again." + "moveError": "Could not move this agent. Try again.", + "machine": "Machine" }, "grants": { "title": "Connector grants", @@ -2791,7 +2792,10 @@ "confirmRevoke": "Yes, revoke it", "cancel": "Cancel", "loadError": "Could not load connector grants.", - "revokeError": "Could not revoke the grant." + "revokeError": "Could not revoke the grant.", + "activeCount": "{{count}} active", + "endedCount": "{{count}} ended", + "retry": "Try again" }, "controls": { "title": "How it runs", @@ -2838,7 +2842,8 @@ "remove": "Remove", "set": "set", "loadError": "Could not load key status.", - "saveError": "Could not update the key. Enter it again to retry." + "saveError": "Could not update the key. Enter it again to retry.", + "neverShown": "never shown again" } }, "agentSettings": { @@ -2951,7 +2956,11 @@ "messageWakeShort_human": "People", "messageWakeShort_all": "All", "turnsUsed": "{{used}} / {{cap}} turns a day", - "turnsUsedNoCap": "{{used}} turns a day · cap unavailable" + "turnsUsedNoCap": "{{used}} turns a day · cap unavailable", + "specialtyCount_one": "{{count}} specialty", + "specialtyCount_other": "{{count}} specialties", + "boundaryCount_one": "{{count}} boundary", + "boundaryCount_other": "{{count}} boundaries" }, "agentPolicy": { "title": "Policy", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 1e62ebb298..e0f548e67d 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2778,7 +2778,8 @@ "targetUnavailable": "这台电脑已不可用,请选择另一台电脑。", "placementChanged": "此智能体的电脑分配已变更。请先查看当前分配,再重新迁移。", "placementError": "无法加载此智能体的电脑分配信息。", - "moveError": "无法迁移此智能体,请重试。" + "moveError": "无法迁移此智能体,请重试。", + "machine": "电脑" }, "grants": { "title": "连接器授权", @@ -2794,7 +2795,10 @@ "confirmRevoke": "确认撤销", "cancel": "取消", "loadError": "无法加载连接器授权。", - "revokeError": "无法撤销授权。" + "revokeError": "无法撤销授权。", + "activeCount": "{{count}}项生效中", + "endedCount": "{{count}}项已结束", + "retry": "重试" }, "controls": { "title": "运行方式", @@ -2841,7 +2845,8 @@ "remove": "移除", "set": "已设置", "loadError": "无法加载密钥状态。", - "saveError": "无法更新密钥。请重新输入后重试。" + "saveError": "无法更新密钥。请重新输入后重试。", + "neverShown": "保存后不再显示" } }, "agentSettings": { @@ -2954,7 +2959,9 @@ "messageWakeShort_human": "真人", "messageWakeShort_all": "全部", "turnsUsed": "{{used}} / {{cap}}次/天", - "turnsUsedNoCap": "{{used}}次/天 · 上限不可用" + "turnsUsedNoCap": "{{used}}次/天 · 上限不可用", + "specialtyCount_other": "{{count}}项专长", + "boundaryCount_other": "{{count}}条行为边界" }, "agentPolicy": { "title": "策略", diff --git a/frontend/src/v2/__tests__/V2AgentPolicy.test.tsx b/frontend/src/v2/__tests__/V2AgentPolicy.test.tsx index 505d1df6ef..ad17cfb51f 100644 --- a/frontend/src/v2/__tests__/V2AgentPolicy.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentPolicy.test.tsx @@ -1,6 +1,6 @@ // @ts-nocheck import React from 'react'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import V2AgentPolicy from '../agents/V2AgentPolicy'; jest.mock('axios', () => { @@ -58,7 +58,7 @@ it('hides stored approval and context settings that no runtime enforces', async it('writes only the chosen trust mode and edits memory with its revision', async () => { renderPolicy(); await screen.findByDisplayValue('Original'); - fireEvent.change(screen.getByLabelText('Repository access'), { target: { value: 'workspace' } }); + fireEvent.click(screen.getByRole('radio', { name: 'Allow repository edits' })); fireEvent.click(screen.getByRole('button', { name: 'Save trust mode' })); await waitFor(() => expect(update).toHaveBeenCalledWith({ config: { environment: { sandbox: { mode: 'workspace' } } }, @@ -81,7 +81,8 @@ it('shows server quota for hosted agents without editable trust controls', async it('offers trust mode for the canonical BYO runtime shape', async () => { renderPolicy({ ...installation, runtime: { runtimeType: 'codex', host: 'byo' }, config: { environment: { sandbox: { mode: 'read-only' } } } }); - expect(await screen.findByLabelText('Repository access')).toHaveValue('read-only'); + expect(await screen.findByRole('radiogroup', { name: 'Repository access' })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: 'Read only' })).toBeChecked(); }); it('shows a retry in Memory when loading fails, then opens the editor', async () => { @@ -97,3 +98,27 @@ it('shows a retry in Memory when loading fails, then opens the editor', async () fireEvent.click(screen.getByRole('button', { name: 'Try again' })); expect(await screen.findByDisplayValue('Original')).toBeInTheDocument(); }); + +it('shows Saved in the trust row for a moment, then leaves the button alone', async () => { + renderPolicy(); + await screen.findByDisplayValue('Original'); + jest.useFakeTimers(); + try { + fireEvent.click(screen.getByRole('radio', { name: 'Allow repository edits' })); + fireEvent.click(screen.getByRole('button', { name: 'Save trust mode' })); + expect(await screen.findByRole('status')).toHaveTextContent('Saved'); + await act(async () => { jest.advanceTimersByTime(3000); }); + expect(screen.queryByRole('status')).toBeNull(); + expect(screen.getByRole('button', { name: 'Save trust mode' })).toBeInTheDocument(); + } finally { jest.useRealTimers(); } +}); + +it('a failed trust save stays in the action slot', async () => { + update.mockRejectedValueOnce(new Error('Could not save changes.')); + renderPolicy(); + await screen.findByDisplayValue('Original'); + fireEvent.click(screen.getByRole('button', { name: 'Save trust mode' })); + const alert = await screen.findByRole('alert'); + expect(alert).toHaveTextContent('Could not save changes.'); + expect(alert.closest('.ac')).toContainElement(screen.getByRole('button', { name: 'Save trust mode' })); +}); diff --git a/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx b/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx index da2520bc86..a4b1a96851 100644 --- a/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentRuntimeGrants.test.tsx @@ -189,3 +189,46 @@ test('does not offer revoke when the server says this viewer cannot revoke', asy expect(await screen.findByText(/Work · Granted by Owner/)).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Revoke' })).toBeNull(); }); + +test('a failed grant load is one row with Try again, which reloads the list', async () => { + let fail = true; + axios.get.mockImplementation((url) => { + if (url.includes('/agent-binding/placement')) return Promise.resolve({ data: { boundMachineId: null, requestedMachineId: null, boundMachineRevoked: false, activeMachines: [] } }); + if (url.includes('/grants') && fail) return Promise.reject(new Error('offline')); + return Promise.resolve({ data: response }); + }); + renderSection(); + const grantsSection = screen.getByRole('heading', { name: 'Connector grants' }).closest('section'); + expect(await within(grantsSection).findByRole('alert')).toHaveTextContent('Could not load connector grants.'); + fail = false; + fireEvent.click(within(grantsSection).getByRole('button', { name: 'Try again' })); + expect(await within(grantsSection).findByText(/Work · Granted by Owner/)).toBeInTheDocument(); + expect(within(grantsSection).queryByRole('alert')).toBeNull(); +}); + +test('a failed revoke says so above the rows, keeps the grant revocable and offers no reload', async () => { + axios.post.mockRejectedValue(new Error('denied')); + renderSection(); + await screen.findByText(/Work · Granted by Owner/); + fireEvent.click(screen.getByRole('button', { name: 'Revoke' })); + fireEvent.click(screen.getByRole('button', { name: 'Yes, revoke it' })); + const grantsSection = screen.getByRole('heading', { name: 'Connector grants' }).closest('section'); + expect(await within(grantsSection).findByRole('alert')).toHaveTextContent('Could not revoke the grant.'); + expect(within(grantsSection).queryByRole('button', { name: 'Try again' })).toBeNull(); + expect(within(grantsSection).getByRole('button', { name: 'Yes, revoke it' })).toBeEnabled(); +}); + +test('the inline confirm swaps back to Revoke on Cancel, and the title counts active and ended grants', async () => { + axios.get.mockImplementation((url) => Promise.resolve({ data: url.includes('/agent-binding/placement') + ? { boundMachineId: null, requestedMachineId: null, boundMachineRevoked: false, activeMachines: [] } + : { grants: [...response.grants, { ...response.grants[0], grantId: 'grant-2', connector: 'sentry', revokedAt: '2026-10-20T00:00:00Z' }] } })); + renderSection(); + expect(await screen.findByText('1 active · 1 ended')).toBeInTheDocument(); + expect(screen.getAllByRole('button', { name: 'Revoke' })).toHaveLength(1); + fireEvent.click(screen.getByRole('button', { name: 'Revoke' })); + expect(screen.queryByRole('button', { name: 'Revoke' })).toBeNull(); + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(screen.getByRole('button', { name: 'Revoke' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Yes, revoke it' })).toBeNull(); + expect(axios.post).not.toHaveBeenCalled(); +}); diff --git a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx index 4705699567..7292608817 100644 --- a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx @@ -661,3 +661,22 @@ test('key polling pauses while hidden and re-reads on focus', async () => { hidden.mockRestore(); } finally { view.unmount(); jest.useRealTimers(); } }); + +test('a name outside 1–80 characters is refused under the field; other refusals sit by Save', async () => { + renderPage(); + const save = await screen.findByRole('button', { name: 'Save identity' }); + const name = screen.getByRole('textbox', { name: 'Display name' }); + fireEvent.change(name, { target: { value: ' ' } }); + fireEvent.click(save); + const error = await screen.findByText('Enter a display name of 1–80 characters.'); + expect(name).toHaveAttribute('aria-invalid', 'true'); + expect(name).toHaveAttribute('aria-describedby', error.id); + expect(axios.patch).not.toHaveBeenCalled(); + + axios.patch.mockRejectedValue({ response: { status: 403, data: {} } }); + fireEvent.change(name, { target: { value: 'Maya 2' } }); + fireEvent.click(save); + await screen.findByText("You don't have permission to change this agent's identity."); + expect(name).not.toHaveAttribute('aria-invalid'); + expect(screen.queryByText('Enter a display name of 1–80 characters.')).toBeNull(); +}); diff --git a/frontend/src/v2/__tests__/V2RuntimeControls.test.tsx b/frontend/src/v2/__tests__/V2RuntimeControls.test.tsx index b82b17053e..84ffa06736 100644 --- a/frontend/src/v2/__tests__/V2RuntimeControls.test.tsx +++ b/frontend/src/v2/__tests__/V2RuntimeControls.test.tsx @@ -27,7 +27,7 @@ test('missing model inventory permits text, but missing effort inventory never p expect(screen.getByText(/max stays stored, not sent/)).toBeInTheDocument(); expect(screen.queryByText(/stored choice unsupported/)).toBeNull(); expect(screen.getByLabelText('effort')).not.toHaveClass('is-unsupported'); - const effortRow = screen.getByLabelText('effort').closest('.v2-runtime-controls__row')!; + const effortRow = screen.getByLabelText('effort').closest('.lrow')!; expect(within(effortRow as HTMLElement).queryByLabelText('pending next spawn')).toBeNull(); fireEvent.change(screen.getByLabelText('model'), { target: { value: 'new-unlisted-model' } }); fireEvent.click(screen.getByRole('button', { name: 'Save runtime' })); @@ -70,7 +70,7 @@ test('OpenCode provider qualification alone does not leave a permanent pending m inventory: [{ harness: 'opencode', installed: true, version: '1', models: null, efforts: [] }], observation: { ...runtime.observation, lastSpawn: { ...receipt, harness: 'opencode', model: 'explicit-provider/saved-model', effort: null } }, }} onSave={jest.fn()} />); - const modelRow = screen.getByLabelText('model').closest('.v2-runtime-controls__row')!; + const modelRow = screen.getByLabelText('model').closest('.lrow')!; expect(within(modelRow as HTMLElement).queryByLabelText('pending next spawn')).toBeNull(); expect(screen.getByText('explicit-provider/saved-model')).toBeInTheDocument(); }); @@ -82,3 +82,28 @@ test('read-only inventory mismatch preserves the saved model without claiming a expect(screen.queryByText(/saved-model · stored/)).toBeNull(); expect(screen.queryByText(/stored choice unsupported/)).toBeNull(); }); + +test('a dot marks only the rows that change on the next spawn', () => { + render(); + const row = (field: string) => screen.getByLabelText(field).closest('.lrow') as HTMLElement; + expect(within(row('model')).getByRole('img', { name: 'pending next spawn' })).toBeInTheDocument(); + expect(within(row('harness')).queryByRole('img', { name: 'pending next spawn' })).toBeNull(); + expect(within(row('effort')).queryByRole('img', { name: 'pending next spawn' })).toBeNull(); +}); + +test('a set key rests as a row; a failed save keeps the entry open, empty, with Save disabled', async () => { + mockApi.get.mockResolvedValue({ set: true, setAt: at, setBy: 'owner', lastUsedAt: null }); + mockApi.put.mockRejectedValue(new Error('proxy')); + render(); + expect(await screen.findByText('never shown again')).toBeInTheDocument(); + expect(screen.getByText('key for anthropic')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Remove' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Replace key' })); + fireEvent.change(screen.getByLabelText('API key'), { target: { value: 'sk-private-test-value' } }); + fireEvent.click(screen.getByRole('button', { name: 'Save key' })); + expect(await screen.findByRole('alert')).toHaveTextContent('Could not update the key. Enter it again to retry.'); + expect(screen.getByLabelText('API key')).toHaveValue(''); + expect(screen.getByRole('button', { name: 'Save key' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeEnabled(); +}); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index c4bbeadc74..d11a25fbb2 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5139,7 +5139,8 @@ describe('agent settings surface (D223 lift)', () => { test('segment tabs keep a keyboard focus ring now that the old base rule skips them', () => { expect(ruleBody(css, '.v2-agent-settings-page .seg > button:focus-visible, .v2-agent-settings-page .seg > a:focus-visible')) - .toContain('box-shadow: 0 0 0 3px var(--c-soft)'); + .toContain('box-shadow: var(--v2-focus-ring)'); + expect(css).not.toContain('--v2-accent-soft'); }); test('disabled lifted controls still dim, since the old :disabled rule skips .b and seg tabs', () => { @@ -5173,26 +5174,40 @@ describe('agent settings surface (D223 lift)', () => { }); }); -describe('agent runtime and grants fit the Settings column', () => { - const css = read('../components/V2AgentRuntimeGrants.css'); - test('facts stack on phones and long grant details wrap', () => { - const phone = blockContaining(css, '@media (max-width: 760px)', '.v2-agent-runtime-grants__facts > div'); - expect(phone).toContain('grid-template-columns: 1fr'); - expect(ruleBody(css, '.v2-agent-runtime-grants__list li p')).toContain('overflow-wrap: anywhere'); - }); -}); - -describe('agent Identity editor', () => { - const css = read('../agents/v2-agent-settings.css'); - test('form fields retain a visible boundary over the v2 input reset', () => { - const input = ruleBody(css, '.v2-root .v2-agent-settings .v2-agent-settings__identity input'); - expect(input).toContain('border: 1px solid var(--v2-border)'); - expect(input).toContain('background: var(--v2-surface)'); - }); - test('listing status and public-profile link remain visible in the Settings flow', () => { - expect(ruleBody(css, '.v2-agent-settings__listing')).toContain('display: grid'); - expect(ruleBody(css, '.v2-root .v2-agent-settings .v2-agent-settings__listing a')).toContain('color: var(--v2-accent)'); - expect(ruleBody(css, '.v2-root .v2-agent-settings .v2-agent-settings__listing a:focus-visible')).toContain('box-shadow: var(--v2-focus-ring)'); +describe('agent settings sections lifted from #s-agent-states', () => { + const css = read('../agents/v2-agent-settings-surface.css'); + const phone = blockContaining(css, '@media (max-width: 600px)', '.v2-agent-settings-page__stack'); + const targets = blockContaining(css, '@media (max-width: 760px)', '.v2-agent-settings-page__group .b'); + test('every lifted control is a 44px target on phones', () => { + expect(ruleBody(targets, '.v2-agent-settings-page__group .b, .v2-agent-settings-page__group .b-sm')).toContain('height: 44px'); + expect(ruleBody(targets, '.v2-agent-settings-page__group .sel, .v2-agent-settings-page__group .field:not(.field--area)')).toContain('height: 44px'); + expect(ruleBody(targets, '.v2-agent-settings-page__group .seg > label')).toContain('height: 44px'); + }); + test('setting rows stack on phones and keep their value under the label', () => { + expect(ruleBody(phone, '.v2-agent-settings-page__group .lrow.v2-agent-settings-page__stack', { within: phone })) + .toContain('grid-template-columns: minmax(0, 1fr)'); + expect(ruleBody(phone, '.v2-agent-settings-page__group .v2-agent-settings-page__stack > .vl, .v2-agent-settings-page__group .v2-agent-settings-page__stack > .ac', { within: phone })) + .toContain('grid-row: auto'); + expect(ruleBody(phone, '.v2-agent-settings-page .dbody', { within: phone })).toContain('padding: 0 12px 12px'); + }); + test('grant rows keep the logo column and long reach lines wrap', () => { + expect(ruleBody(css, '.v2-agent-settings-page .lrow.v2-agent-settings-page__grant')).toContain('grid-template-columns: 28px minmax(0, 1fr) auto'); + expect(ruleBody(css, '.v2-agent-settings-page__grantname .mo')).toContain('overflow-wrap: anywhere'); + expect(ruleBody(css, '.v2-agent-settings-page__grantname')).toContain('min-width: 0'); + }); + test('native controls fill the frame box, and the box carries the focus ring', () => { + expect(css).toContain('.v2-agent-settings-page .sel input, .v2-agent-settings-page .sel select { all: unset;'); + expect(ruleBody(css, '.v2-agent-settings-page .field:focus-within, .v2-agent-settings-page .sel:focus-within')).toContain('var(--v2-focus-ring)'); + expect(ruleBody(css, '.v2-agent-settings-page .sel:has(.is-unsupported)')).toContain('inset 0 0 0 2px var(--ink)'); + }); + test('the identity form does not add a box between the list and its rows', () => { + expect(ruleBody(css, '.v2-agent-settings-page__form')).toContain('display: contents'); + }); + test('the old section sheets are gone', () => { + expect(fs.existsSync(path.join(__dirname, '../components/V2AgentRuntimeGrants.css'))).toBe(false); + expect(fs.existsSync(path.join(__dirname, '../components/V2RuntimeControls.css'))).toBe(false); + expect(read('../agents/v2-agent-settings.css')).not.toMatch(/v2-agent-settings__(identity|listing|owned)/); + expect(read('../agents/v2-agent-policy.css')).not.toContain('v2-agent-policy__quota'); }); }); @@ -5345,15 +5360,6 @@ test('TASK-482 offer follows the final CTA so existing commands and controls can // Actual before/after top coordinates are asserted by e2e/byo-offer-layout.spec.ts. }); -test('TASK-435 runtime columns can shrink on phones without dropping the observed values', () => { - const css = read('../components/V2RuntimeControls.css'); - expect(css).toContain('@media (max-width: 760px)'); - expect(css).toContain('grid-template-columns: minmax(0, 1fr)'); - expect(css).toContain('width: 100%; min-height: 44px'); - expect(ruleBody(css, '.v2-runtime-controls__saved')).toContain('min-width: 0'); - expect(ruleBody(css, '.v2-runtime-controls__observed')).toContain('overflow-wrap: anywhere'); - expect(css).toContain('content: attr(data-label)'); -}); test('teammate invite cards keep one-column phone controls at the 44px minimum', () => { const css = read('../components/V2BringTeammate.css'); diff --git a/frontend/src/v2/agents/V2AgentIdentitySection.tsx b/frontend/src/v2/agents/V2AgentIdentitySection.tsx index 5600f96a33..88e61d4365 100644 --- a/frontend/src/v2/agents/V2AgentIdentitySection.tsx +++ b/frontend/src/v2/agents/V2AgentIdentitySection.tsx @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import axios from '../../utils/axiosConfig'; import { useAuth } from '../../context/AuthContext'; +import V2AgentSettingsIcon from './V2AgentSettingsIcon'; import type { AgentSettingsInstallation, AgentSettingsPatch } from './useAgentSettings'; interface Props { @@ -91,7 +92,7 @@ const V2AgentIdentitySection: React.FC = ({ installation, update }) => { event.preventDefault(); const name = displayName.trim(); if (!name || name.length > 80) { - setError(t('agentSettings.identityNameError')); + setError('agentSettings.identityNameError'); return; } setBusy(true); @@ -118,11 +119,11 @@ const V2AgentIdentitySection: React.FC = ({ installation, update }) => { 503: 'agentSettings.identityMirrorError', }; const code = response?.data?.code; - setError(t(code === 'agent_display_name_taken' + setError(code === 'agent_display_name_taken' ? 'agentSettings.identityNameTakenError' : code === 'identity_projection_retryable' ? 'agentSettings.identityProjectionError' - : messageKey[status || 0] || 'agentSettings.identitySaveError')); + : messageKey[status || 0] || 'agentSettings.identitySaveError'); } finally { setBusy(false); } @@ -147,56 +148,109 @@ const V2AgentIdentitySection: React.FC = ({ installation, update }) => { setListingBusy(false); } }; - return
-

{t('agentSettings.identityTitle')}

-
- - {supportsPersona &&
{t('agentSettings.persona')} - - - -