From d90f19d53173d5adc155e992293223bbf156d023 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:44:36 -0700 Subject: [PATCH 1/8] feat(agent-settings): lift the D215 agent-settings frame (TASK-731) Rebases the lift onto main (squashed, one commit) so it applies on top of #3169 (Wake table fold), #3167 (phone top bar) and the Control row. - Avatar header with Profile / Memory / Settings tabs (ph, ahead, seg) - Wake table lifted: gtitle with bolt and info, one cap in the header, Mentions / People / All segment, board toggle, heartbeat select with Off first, row-menu icon, phone accordion for each pod - Identity & access list: Connection, Key (with key panels), Memory, How it runs (disc) - Surface CSS in v2-agent-settings-surface.css; v2-agent-wake-pods.css deleted Not built, named for the next slice: Talk to / More, Name / Listing / Connectors / Skills rows, Regenerate menu, Add to pod. Policy, connector grants, identity editing, runtime controls and the provider-key card stay where they are until #3184 merges (option a). Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/i18n/locales/en.json | 10 +- frontend/src/i18n/locales/zh-CN.json | 10 +- .../v2/__tests__/V2AgentSettingsPage.test.tsx | 25 +-- .../src/v2/__tests__/V2AgentWakePods.test.tsx | 10 +- .../v2/__tests__/v2-layout-invariants.test.ts | 33 ++-- .../src/v2/agents/V2AgentSettingsPage.tsx | 121 ++++++++------ frontend/src/v2/agents/V2AgentWakePods.tsx | 149 +++++++++++------- .../v2/agents/v2-agent-settings-surface.css | 142 +++++++++++++++++ frontend/src/v2/agents/v2-agent-wake-pods.css | 25 --- .../v2/components/V2AgentControlSection.tsx | 25 +-- 10 files changed, 368 insertions(+), 182 deletions(-) create mode 100644 frontend/src/v2/agents/v2-agent-settings-surface.css delete mode 100644 frontend/src/v2/agents/v2-agent-wake-pods.css diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index f4f14cd8b9..3c113507ec 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2881,7 +2881,15 @@ "revoke": "Revoke", "updateError": "Could not update the key. Try again.", "noPod": "This agent has no active pod.", - "missingReplacement": "The replacement key was not returned." + "missingReplacement": "The replacement key was not returned.", + "colMessages": "messages", + "colBoard": "board", + "colHeartbeat": "heartbeat", + "messageWakeShort_mentions": "Mentions", + "messageWakeShort_human": "People", + "messageWakeShort_all": "All", + "turnsUsed": "{{used}} / {{cap}} turns a day", + "turnsUsedNoCap": "{{used}} turns a day · cap unavailable" }, "agentPolicy": { "title": "Policy", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 51dcc5beb5..2492adc083 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2884,7 +2884,15 @@ "revoke": "撤销", "updateError": "无法更新密钥,请重试。", "noPod": "此智能体没有活跃的 Pod。", - "missingReplacement": "未返回新密钥。" + "missingReplacement": "未返回新密钥。", + "colMessages": "消息", + "colBoard": "看板", + "colHeartbeat": "心跳", + "messageWakeShort_mentions": "仅提及", + "messageWakeShort_human": "人", + "messageWakeShort_all": "全部", + "turnsUsed": "{{used}} / {{cap}} 次/天", + "turnsUsedNoCap": "{{used}} 次/天 · 上限不可用" }, "agentPolicy": { "title": "策略", diff --git a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx index 6eeae4d674..204d87e744 100644 --- a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx @@ -421,10 +421,10 @@ test('selects an installation the owner can manage without exposing a foreign po ); expect(screen.getByText('Pod · My pod')).toBeInTheDocument(); expect(screen.queryByText('Pod · Other pod')).not.toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'Key' }).compareDocumentPosition( + expect(screen.getByText('Key', { selector: '.lb' }).compareDocumentPosition( screen.getByRole('heading', { name: 'Skills' }), ) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); - expect(screen.getByRole('heading', { name: 'Wake' }).compareDocumentPosition( + expect(screen.getByRole('heading', { name: /^Wake/ }).compareDocumentPosition( screen.getByRole('heading', { name: 'Skills' }), ) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); fireEvent.click(screen.getByRole('button', { name: 'Regenerate key' })); @@ -452,19 +452,22 @@ test('runtime editor writes environment controls through the shared settings hoo }); renderPage(); const model = await screen.findByLabelText('model'); - await screen.findByRole('heading', { name: 'Wake' }); + await screen.findByRole('heading', { name: /^Wake/ }); await screen.findByRole('heading', { name: 'Policy' }); await screen.findByRole('heading', { name: 'Skills' }); - const sections = Array.from(document.querySelectorAll('.v2-agent-settings h2')).map((node) => node.textContent); + // Headings and row labels, in document order. The Wake title carries its tooltip text, so match it by prefix. + const sections = Array.from(document.querySelectorAll('.v2-agent-settings h2, .v2-agent-settings .lb')) + .map((node) => node.textContent.trim()); + const at = (label) => sections.findIndex((text) => (label === 'Wake' ? text.startsWith('Wake') : text === label)); expect(sections).toEqual(expect.arrayContaining(['Identity', 'Policy', 'Skills'])); // D215 artboard: Wake first, then Identity & access (Key, Identity). - expect(sections.indexOf('Wake')).toBeLessThan(sections.indexOf('Key')); - expect(sections.indexOf('Key')).toBeLessThan(sections.indexOf('Identity')); - expect(sections.indexOf('Identity')).toBeLessThan(sections.indexOf('How it runs')); - expect(sections.indexOf('Wake')).toBeLessThan(sections.indexOf('How it runs')); - expect(sections.indexOf('How it runs')).toBeLessThan(sections.indexOf('Connector grants')); - expect(sections.indexOf('Connector grants')).toBeLessThan(sections.indexOf('Policy')); - expect(sections.indexOf('Policy')).toBeLessThan(sections.indexOf('Skills')); + expect(at('Wake')).toBeLessThan(at('Key')); + expect(at('Key')).toBeLessThan(at('Identity')); + expect(at('How it runs')).toBeLessThan(at('Identity')); + expect(at('Wake')).toBeLessThan(at('How it runs')); + expect(at('How it runs')).toBeLessThan(at('Connector grants')); + expect(at('Connector grants')).toBeLessThan(at('Policy')); + expect(at('Policy')).toBeLessThan(at('Skills')); fireEvent.change(model, { target: { value: 'new-model' } }); fireEvent.click(screen.getByRole('button', { name: 'Save runtime' })); await waitFor(() => expect(axios.patch).toHaveBeenCalledWith( diff --git a/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx b/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx index b59d17c491..bb297fc99f 100644 --- a/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx @@ -198,9 +198,9 @@ describe('per-agent Wake and Pods settings', () => { it('previews heartbeat turns against the plan cap before saving', async () => { renderSettings(); - expect(await screen.findByText('24 turns/day · 30 available')).toBeInTheDocument(); - fireEvent.change(screen.getByLabelText('Interval'), { target: { value: '30' } }); - expect(screen.getByText(/48 turns\/day · 30 available/)).toBeInTheDocument(); + expect(await screen.findByText('0 / 30 turns a day')).toBeInTheDocument(); + fireEvent.change(screen.getByLabelText('Heartbeat'), { target: { value: '30' } }); + expect(screen.getByText('48 / 30 turns a day')).toBeInTheDocument(); expect(screen.getByText(/This exceeds the daily cap/)).toBeInTheDocument(); expect(updatePod).not.toHaveBeenCalled(); }); @@ -208,10 +208,10 @@ describe('per-agent Wake and Pods settings', () => { it('states the daily turn count clearly when the cap is unavailable', async () => { client.get.mockResolvedValue({ data: { activePodCount: 1, podCount: 1, installations: [{ podId: 'pod-1', podName: 'Launch', status: 'active', wakeOnMessage: false, humanOnly: false, - boardWake: false, heartbeatEnabled: false, heartbeatMinutes: 60, + boardWake: false, heartbeatEnabled: true, heartbeatMinutes: 60, }] } }); renderSettings(); - expect(await screen.findByText('24 turns/day · plan cap unavailable')).toBeInTheDocument(); + expect(await screen.findByText('24 turns a day · cap unavailable')).toBeInTheDocument(); }); it('warns about key revocation when removing the only active installation beside a paused one', async () => { diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 19c13c8e38..7247a2111a 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5050,29 +5050,22 @@ describe('GitHub gig card fits the chat column', () => { }); }); -describe('agent Wake and Pods controls', () => { - const css = read('../agents/v2-agent-wake-pods.css'); - test('action buttons keep a visible surface over the v2 button reset', () => { - const save = ruleBody(css, '.v2-agent-settings .v2-agent-wake-pods .v2-agent-wake-pods__pod button'); - const remove = ruleBody(css, '.v2-agent-settings .v2-agent-wake-pods .v2-agent-wake-pods__remove'); - expect(save).toContain('background: var(--v2-surface)'); - expect(remove).toContain('background: var(--v2-surface)'); - expect(remove).toContain('border: 1px solid var(--v2-border)'); - }); - - test('message-wake segment wraps its options instead of overflowing the row', () => { - const segment = ruleBody(css, '.v2-agent-wake-pods__segment'); - expect(segment).toContain('display: flex'); +describe('agent settings surface (D223 lift)', () => { + const css = read('../agents/v2-agent-settings-surface.css'); + test('the Save action keeps a visible surface over the v2 button reset', () => { + expect(ruleBody(css, '.v2-agent-settings-page .b-s')).toContain('background: linear-gradient'); + }); + + test('message-wake segment wraps its options instead of overflowing the Wake row', () => { + const segment = ruleBody(css, '.v2-agent-settings-page .wt .seg'); + expect(segment).toContain('display: inline-flex'); expect(segment).toContain('flex-wrap: wrap'); }); - it('keeps the settings header on one row with the tabs at its end', () => { - const css = fs.readFileSync(path.join(__dirname, '../agents/v2-agent-settings.css'), 'utf8'); - const head = ruleBody(css, '.v2-agent-settings__head'); - expect(head).toContain('display: flex'); - expect(head).toContain('align-items: center'); - expect(ruleBody(css, '.v2-agent-settings__head .v2-agent-settings__seg')).toContain('margin-left: auto'); - expect(ruleBody(css, '.v2-agent-settings__head .v2-agent-settings__seg [aria-current="page"]')).toContain('border-bottom: 0'); + test('the avatar header, Identity & access rows and Wake table keep their frame layout', () => { + expect(ruleBody(css, '.v2-agent-settings-page .ph')).toContain('min-height: 76px'); + expect(ruleBody(css, '.v2-agent-settings-page .lrow')).toContain('grid-template-columns: 180px minmax(0, 1fr) auto'); + expect(ruleBody(css, '.v2-agent-settings-page .wt')).toContain('border-collapse: collapse'); }); }); diff --git a/frontend/src/v2/agents/V2AgentSettingsPage.tsx b/frontend/src/v2/agents/V2AgentSettingsPage.tsx index 11e6cc1f4b..d03304a126 100644 --- a/frontend/src/v2/agents/V2AgentSettingsPage.tsx +++ b/frontend/src/v2/agents/V2AgentSettingsPage.tsx @@ -12,6 +12,7 @@ import V2AgentWakePods from './V2AgentWakePods'; import V2AgentPolicy from './V2AgentPolicy'; import V2AgentSkills from '../components/V2AgentSkills'; import './v2-agent-settings.css'; +import './v2-agent-settings-surface.css'; interface RuntimeKey { _id: string; @@ -253,61 +254,88 @@ const V2AgentSettingsPage: React.FC = () => { ); return ( -
-
+
+
-
+

{access.displayName || agentName}

+
{t('agentProfile.hero.activeInPods', { count: access.pods.length })}
-
-
- - -
-
-

{t('agentSettings.key')}

- {keysForbidden ? null : installation?.runtime?.runtimeType === 'wrapper' ? ( - {t('agentSettings.wrapperReconnect')} - ) : !disconnected && ( - - )} -
- {error &&

{error}

} - {pendingAction === 'replace' && !keysForbidden && ( -
-

{t('agentByo.rekey.warning', { name: access.displayName || agentName })}

-
- - -
+
+
+ +

{t('agentSettings.identityAccess', { defaultValue: 'Identity & access' })}

+
+ +
+ + {keysForbidden ? t('agentSettings.keyUnavailable') : activeKeys.length ? t('agentSettings.keyActive', { defaultValue: 'active' }) : t('agentSettings.noActiveKeys')} + + {keysForbidden ? null : installation?.runtime?.runtimeType === 'wrapper' ? ( + {t('agentSettings.wrapperReconnect')} + ) : !disconnected && ( + + )} + +
+
+ {error &&

{error}

} + {pendingAction === 'replace' && !keysForbidden && ( +
+

{t('agentByo.rekey.warning', { name: access.displayName || agentName })}

+
+ + +
+
+ )} + {oneTimeKey && ( +
+ {t('agentSettings.copyNow')} + {oneTimeKey} + +
+ )} + {!keysForbidden && ( + <> + {activeKeys.length ?
    {activeKeys.map(renderKey)}
+ : null} + {revokedKeys.length > 0 &&
+ {t('agentSettings.revokedCount', { count: revokedKeys.length })} +
    {revokedKeys.map(renderKey)}
+
} + + )}
- )} - {oneTimeKey && ( -
- {t('agentSettings.copyNow')} - {oneTimeKey} - +
+ {t('agentSettings.memory', { defaultValue: 'Memory' })} + + + + + +
- )} - {keysForbidden ?

{t('agentSettings.keyUnavailable')}

: ( - <> - {activeKeys.length ?
    {activeKeys.map(renderKey)}
- :

{t('agentSettings.noActiveKeys')}

} - {revokedKeys.length > 0 &&
- {t('agentSettings.revokedCount', { count: revokedKeys.length })} -
    {revokedKeys.map(renderKey)}
-
} - - )} -
+
+ + {t('agentSettings.howItRuns', { defaultValue: 'How it runs' })} + {installation?.runtime?.runtimeType || t('agentSettings.notReported', { defaultValue: 'not reported' })} + + +
+
{installation && } {
pod.id === podId)?.name || podId} agentName={agentName} instanceId={instanceId} />
+
); diff --git a/frontend/src/v2/agents/V2AgentWakePods.tsx b/frontend/src/v2/agents/V2AgentWakePods.tsx index d077623a33..fcf2ab7eeb 100644 --- a/frontend/src/v2/agents/V2AgentWakePods.tsx +++ b/frontend/src/v2/agents/V2AgentWakePods.tsx @@ -3,7 +3,6 @@ import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import axios from '../../utils/axiosConfig'; import { AgentPodWakePatch } from './useAgentSettings'; -import './v2-agent-wake-pods.css'; interface InstallationSettings { podId: string; @@ -45,6 +44,9 @@ const V2AgentWakePods: React.FC<{ const [podCount, setPodCount] = useState(0); const [turnCap, setTurnCap] = useState(null); const [draftMinutes, setDraftMinutes] = useState>({}); + // A pod whose heartbeat select is set to Off, until its Save applies it. + const [draftOff, setDraftOff] = useState>({}); + const [openPods, setOpenPods] = useState>({}); const [loadedIdentity, setLoadedIdentity] = useState(null); const query = `instanceId=${encodeURIComponent(instanceId)}`; const base = `/api/registry/agents/${encodeURIComponent(agentName)}/manage-settings`; @@ -63,6 +65,7 @@ const V2AgentWakePods: React.FC<{ setBusyPod(null); setError(null); setDraftMinutes({}); + setDraftOff({}); committedRef.current = {}; queuedRef.current = {}; sendingRef.current = {}; @@ -175,6 +178,14 @@ const V2AgentWakePods: React.FC<{ } }; + // Sum of the draft heartbeat turns, so the header previews the cap as the select changes. + const turnsUsed = installations.reduce((total, entry) => { + const off = draftOff[entry.podId] ?? !entry.heartbeatEnabled; + const minutes = draftMinutes[entry.podId] ?? entry.heartbeatMinutes; + return off ? total : total + Math.ceil(1440 / minutes); + }, 0); + const overCap = turnCap !== null && turnsUsed > turnCap; + if (loading || loadedIdentity !== identity) { return
{t('agentProfile.loading')}
; } @@ -182,85 +193,101 @@ const V2AgentWakePods: React.FC<{ return (
{error &&

{error}

} -
-

{t('agentSettings.wakeTitle')}

-

{t('agentSettings.mentionsAlwaysWake')}

+

+ + {t('agentSettings.wakeTitle')} + + + {t('agentSettings.mentionsAlwaysWake')} + + + {turnCap === null + ? t('agentSettings.turnsUsedNoCap', { used: turnsUsed }) + : t('agentSettings.turnsUsed', { used: turnsUsed, cap: turnCap })} + +

+ {overCap &&

{t('agentSettings.overCap')}

} +
{installations.length ? ( - +
- - - + + + {installations.map((entry) => ( - - + + - - @@ -268,7 +295,7 @@ const V2AgentWakePods: React.FC<{
{t('agentSettings.podsTitle')}{t('agentSettings.messageWake')}{t('agentSettings.boardWake')}{t('agentSettings.heartbeat')}{t('agentSettings.colMessages')}{t('agentSettings.colBoard')}{t('agentSettings.colHeartbeat')} {t('agentSettings.remove')}
{entry.podName}
-
{entry.podName} + {t(`agentSettings.messageWakeShort_${messageWakeChoice(entry)}`)} + +
+
-
- {MESSAGE_WAKE_CHOICES.map((choice) => ( - - ))} -
+ {MESSAGE_WAKE_CHOICES.map((choice) => ( + + ))}
- + - - -

- {turnCap === null - ? t('agentSettings.capUnavailable', { - count: Math.ceil(1440 / (draftMinutes[entry.podId] ?? entry.heartbeatMinutes)), - }) - : t('agentSettings.turnsPerDay', { - count: Math.ceil(1440 / (draftMinutes[entry.podId] ?? entry.heartbeatMinutes)), cap: turnCap, - })} - {turnCap !== null && Math.ceil(1440 / (draftMinutes[entry.podId] ?? entry.heartbeatMinutes)) > turnCap - && ` ${t('agentSettings.overCap')}`} -

- {entry.heartbeatEnabled && draftMinutes[entry.podId] !== entry.heartbeatMinutes - && } +
+ {(() => { + const off = draftOff[entry.podId] ?? !entry.heartbeatEnabled; + const minutes = draftMinutes[entry.podId] ?? entry.heartbeatMinutes; + const dirty = off === entry.heartbeatEnabled || (!off && minutes !== entry.heartbeatMinutes); + return ( + <> + + {dirty && } + + ); + })()} -
) :

{t('agentSettings.noPods')}

} -
+
); }; diff --git a/frontend/src/v2/agents/v2-agent-settings-surface.css b/frontend/src/v2/agents/v2-agent-settings-surface.css new file mode 100644 index 0000000000..0f8453565b --- /dev/null +++ b/frontend/src/v2/agents/v2-agent-settings-surface.css @@ -0,0 +1,142 @@ +/* Agent settings surface, lifted from the D223 artboard (docs/design/artboards/d215, + index.html). Class names and values are the artboard's. The artboard's variable names + alias onto the v2 primitive tokens (TASK-730), which carry the same light and dark values, + so this file needs no second palette. Every rule is scoped to .v2-agent-settings-page. */ + +.v2-agent-settings-page { + --sf: var(--v2-bg); + --p: var(--v2-bg-subtle); + --t: var(--v2-surface-hover); + --d: var(--v2-border-soft); + --b: var(--v2-border); + --mu: var(--v2-text-muted); + --se: var(--v2-text-secondary); + --ph: var(--v2-text-placeholder); + --ink: var(--v2-text-primary); + --ink2: var(--v2-ink-2); + --c: var(--v2-accent); + --c-soft: var(--v2-accent-soft); + --on-c: var(--v2-on-accent); + --c-fill-a: var(--v2-accent-fill-a); + --c-fill-b: var(--v2-accent-fill-b); + --fill-a: var(--v2-fill-a); + --fill-b: var(--v2-fill-b); + --fill-edge: var(--v2-fill-edge); + --on-fill: var(--v2-on-fill); + --key-a: var(--v2-key-a); + --key-b: var(--v2-key-b); + --key-edge: var(--v2-key-edge); + --sh-key: var(--v2-key-inset); + --sh-pane: var(--v2-shadow-pane); + --sh-pop: var(--v2-shadow-pop); + --av-ring: var(--v2-av-ring); + --f-display: var(--v2-font-display); + --f-body: var(--v2-font); + --f-mono: var(--v2-font-mono); + color: var(--ink); + font-family: var(--f-body); +} + +/* buttons */ +.v2-agent-settings-page .b { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 7px; height: 34px; padding: 0 13px; border-radius: 10px; font: 600 13px/1 var(--f-body); cursor: pointer; white-space: nowrap; transition: transform 120ms, background 160ms, box-shadow 140ms; } +.v2-agent-settings-page .b svg.i { width: 16px; height: 16px; } +.v2-agent-settings-page .b:active { transform: translateY(1px); } +.v2-agent-settings-page .b:focus-visible { box-shadow: 0 0 0 3px var(--c-soft), 0 0 0 1px var(--c); } +.v2-agent-settings-page .b-s { color: var(--ink); background: linear-gradient(180deg, var(--key-a), var(--key-b)); border: 1px solid var(--key-edge); box-shadow: var(--sh-key); } +.v2-agent-settings-page .b-g { color: var(--se); background: transparent; border: 1px solid transparent; } +.v2-agent-settings-page .b-g:hover { background: var(--t); color: var(--ink); } +.v2-agent-settings-page .b-sm { height: 28px; padding: 0 10px; border-radius: 8px; font-size: 12.5px; } +.v2-agent-settings-page .ib { width: 32px; padding: 0; height: 32px; border-radius: 8px; } +.v2-agent-settings-page .ib.b-sm { width: 28px; height: 28px; } +.v2-agent-settings-page .ib svg.i { width: 17px; height: 17px; } + +/* segmented control */ +.v2-agent-settings-page .seg { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--t); box-shadow: inset 0 0 0 1px var(--d); flex: none; } +.v2-agent-settings-page .seg > button, .v2-agent-settings-page .seg > a { all: unset; box-sizing: border-box; cursor: pointer; height: 28px; padding: 0 10px; border-radius: 7px; display: inline-flex; align-items: center; gap: 6px; font: 500 12.5px/1 var(--f-body); color: var(--se); white-space: nowrap; text-decoration: none; } +.v2-agent-settings-page .seg > .on, .v2-agent-settings-page .seg > [aria-current="page"] { background: var(--sf); color: var(--ink); font-weight: 600; box-shadow: 0 0 0 1px var(--key-edge), 0 1px 2px rgba(16,24,40,.08); } +.v2-agent-settings-page .seg--sm > button { height: 24px; padding: 0 8px; font-size: 12px; } +.v2-agent-settings-page .tg { width: 30px; height: 18px; border-radius: 9px; background: var(--b); position: relative; flex: none; } +.v2-agent-settings-page .tg::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--sf); box-shadow: 0 1px 2px rgba(0,0,0,.2); } +.v2-agent-settings-page .tg.on { background: var(--c); } +.v2-agent-settings-page .tg.on::after { left: 14px; background: #fff; } +.v2-agent-settings-page .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--c); flex: none; position: relative; display: inline-block; } +.v2-agent-settings-page .dot--idle { background: var(--ph); } +.v2-agent-settings-page .dot--off { background: transparent; box-shadow: inset 0 0 0 1.5px var(--ph); } +.v2-agent-settings-page .av { width: 32px; height: 32px; border-radius: 8px; overflow: hidden; box-shadow: 0 0 0 1px var(--av-ring); flex: none; display: block; } +.v2-agent-settings-page .av svg { width: 100%; height: 100%; display: block; } +.v2-agent-settings-page .av--44 { width: 44px; height: 44px; border-radius: 11px; } +.v2-agent-settings-page .info { all: unset; cursor: help; position: relative; display: inline-grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; color: var(--ph); vertical-align: middle; } +.v2-agent-settings-page .info svg.i { width: 15px; height: 15px; } +.v2-agent-settings-page .info:hover, .v2-agent-settings-page .info:focus-visible { color: var(--ink); } +.v2-agent-settings-page .info .tip { display: none; position: absolute; z-index: 30; top: 24px; left: -8px; width: 260px; padding: 10px 12px; border-radius: 10px; background: var(--sf); box-shadow: var(--sh-pop); font: 400 12.5px/1.45 var(--f-body); color: var(--se); text-align: left; white-space: normal; } +.v2-agent-settings-page .info:hover .tip, .v2-agent-settings-page .info:focus-visible .tip, .v2-agent-settings-page .info.pin .tip { display: block; } +.v2-agent-settings-page .sel { height: 28px; border-radius: 8px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--b); display: inline-flex; align-items: center; gap: 6px; padding: 0 8px 0 10px; font-size: 12.5px; color: var(--ink2); white-space: nowrap; } +.v2-agent-settings-page .sel svg.i { width: 14px; height: 14px; color: var(--mu); } + +/* page head: avatar, name, status, tabs and actions on one 76px row */ +.v2-agent-settings-page .ph { flex: none; min-height: 76px; display: flex; align-items: center; gap: 10px; padding: 0 20px 0 28px; border-bottom: 1px solid var(--d); box-sizing: border-box; } +.v2-agent-settings-page .ph h1 { font: 800 24px/1 var(--f-display); letter-spacing: -0.03em; margin: 0; white-space: nowrap; } +.v2-agent-settings-page .ph .sp { flex: 1; } +.v2-agent-settings-page .ahead { display: flex; align-items: center; gap: 12px; } +.v2-agent-settings-page .ahead h1 { font: 800 24px/1 var(--f-display); letter-spacing: -0.03em; margin: 0; } +.v2-agent-settings-page .ahead .st { display: flex; align-items: center; gap: 6px; font: 500 11.5px/1 var(--f-mono); color: var(--mu); margin-top: 5px; } +.v2-agent-settings-page .mo { font-family: var(--f-mono); } + +/* body and grouped lists */ +.v2-agent-settings-page .pbody { flex: 1; min-height: 0; padding: 24px 28px; box-sizing: border-box; } +.v2-agent-settings-page .gtitle { font: 500 11.5px/1 var(--f-mono); color: var(--mu); margin: 18px 4px 8px; display: flex; align-items: center; gap: 8px; } +.v2-agent-settings-page .gtitle:first-child { margin-top: 0; } +.v2-agent-settings-page .list { border-radius: 12px; box-shadow: inset 0 0 0 1px var(--d); background: var(--sf); } +.v2-agent-settings-page .lrow { display: grid; grid-template-columns: 180px minmax(0, 1fr) auto; align-items: center; gap: 14px; min-height: 52px; padding: 8px 14px 8px 16px; border-top: 1px solid var(--d); } +.v2-agent-settings-page .lrow:first-child { border-top: 0; } +.v2-agent-settings-page .lrow .lb { font-size: 13px; font-weight: 600; color: var(--ink); display: flex; align-items: center; gap: 8px; } +.v2-agent-settings-page .lrow .lb svg.i { width: 16px; height: 16px; color: var(--se); } +.v2-agent-settings-page .lrow .vl { font-size: 13.5px; color: var(--ink2); min-width: 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.v2-agent-settings-page .lrow .vl .mo { font-size: 12px; color: var(--mu); } +.v2-agent-settings-page .lrow .ac { display: flex; gap: 6px; align-items: center; justify-content: flex-end; } +.v2-agent-settings-page details.disc { border-top: 1px solid var(--d); } +.v2-agent-settings-page details.disc > summary { list-style: none; cursor: pointer; display: grid; grid-template-columns: 180px minmax(0,1fr) auto; align-items: center; gap: 14px; min-height: 52px; padding: 8px 14px 8px 16px; } +.v2-agent-settings-page details.disc > summary::-webkit-details-marker { display: none; } + +/* wake table */ +.v2-agent-settings-page .wt { width: 100%; border-collapse: collapse; font-size: 13px; } +.v2-agent-settings-page .wt th { font: 500 11px/1 var(--f-mono); color: var(--mu); text-align: left; padding: 10px 14px 8px; font-weight: 500; } +.v2-agent-settings-page .wt td { padding: 8px 14px; border-top: 1px solid var(--d); vertical-align: middle; color: var(--ink2); } +.v2-agent-settings-page .wt td:first-child { font-weight: 600; color: var(--ink); max-width: 320px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.v2-agent-settings-page .wt td:last-child { text-align: right; width: 44px; } + +/* wake table controls: the message-wake segment, board and heartbeat toggles, the interval select */ +.v2-agent-settings-page .wt .seg { display: inline-flex; flex-wrap: wrap; } +.v2-agent-settings-page .seg > label { position: relative; cursor: pointer; height: 24px; padding: 0 8px; border-radius: 7px; display: inline-flex; align-items: center; gap: 6px; font: 500 12px/1 var(--f-body); color: var(--se); white-space: nowrap; } +.v2-agent-settings-page .seg > label input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none; } +.v2-agent-settings-page .seg > label:has(input:checked) { background: var(--sf); color: var(--ink); font-weight: 600; box-shadow: 0 0 0 1px var(--key-edge), 0 1px 2px rgba(16,24,40,.08); } +.v2-agent-settings-page .seg > label:has(input:focus-visible) { box-shadow: 0 0 0 3px var(--c-soft), 0 0 0 1px var(--c); } +.v2-agent-settings-page .v2-agent-wake-pods__toggle { position: relative; display: inline-flex; cursor: pointer; } +.v2-agent-settings-page .v2-agent-wake-pods__toggle input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; } +.v2-agent-settings-page .v2-agent-wake-pods__toggle input:focus-visible + .tg { box-shadow: 0 0 0 3px var(--c-soft), 0 0 0 1px var(--c); } +.v2-agent-settings-page .v2-agent-wake-pods__interval select { font: inherit; color: inherit; background: transparent; border: 0; padding: 0; } +.v2-agent-settings-page .v2-agent-wake-pods__hint { margin: 4px 0 0; font: 400 11.5px/1.4 var(--f-mono); color: var(--mu); } +.v2-agent-settings-page .v2-agent-wake-pods__sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; } +.v2-agent-settings-page .v2-agent-wake-pods__error { margin: 0 0 8px; color: var(--ink); } +.v2-agent-settings-page .v2-agent-wake-pods__empty { margin: 0; padding: 16px 14px; color: var(--se); font-size: 13px; } +.v2-agent-settings-page .v2-agent-wake-pods__loading { padding: 16px 0; color: var(--se); } +@media (max-width: 600px) { + .v2-agent-settings-page .wt thead { display: none; } + .v2-agent-settings-page .wt, .v2-agent-settings-page .wt tbody, .v2-agent-settings-page .wt tr, .v2-agent-settings-page .wt td { display: block; width: 100%; } +} +.v2-agent-settings-page .v2-agent-wake-pods__cap { margin-left: auto; font: 400 11.5px/1 var(--f-mono); color: var(--mu); } +.v2-agent-settings-page .v2-agent-wake-pods__mode, .v2-agent-settings-page .v2-agent-wake-pods__fold { display: none; } +@media (max-width: 600px) { + .v2-agent-settings-page .wt td:not(:first-child) { display: none; } + .v2-agent-settings-page .wt tr[data-open="true"] td:not(:first-child) { display: block; } + .v2-agent-settings-page .wt td:first-child { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 12px 14px; } + .v2-agent-settings-page .wt td:first-child a { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; } + .v2-agent-settings-page .wt td:first-child { max-width: none; } + .v2-agent-settings-page .v2-agent-wake-pods__mode { display: inline; font: 400 11.5px/1 var(--f-mono); color: var(--mu); } + .v2-agent-settings-page .v2-agent-wake-pods__fold { all: unset; cursor: pointer; display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: var(--mu); margin-left: auto; } + .v2-agent-settings-page .v2-agent-wake-pods__fold svg.i { width: 16px; height: 16px; transition: transform 120ms; } + .v2-agent-settings-page .v2-agent-wake-pods__fold[aria-expanded="true"] svg.i { transform: rotate(90deg); } + .v2-agent-settings-page .wt td[data-label]::before { content: attr(data-label); display: block; font: 500 11px/1 var(--f-mono); color: var(--mu); margin-bottom: 6px; } + .v2-agent-settings-page .wt tr[data-open="true"] td[data-label] { padding: 8px 14px; } +} +.v2-agent-settings-page .v2-agent-settings-page__keypanel { padding: 12px 16px; border-top: 1px solid var(--d); display: grid; gap: 10px; } diff --git a/frontend/src/v2/agents/v2-agent-wake-pods.css b/frontend/src/v2/agents/v2-agent-wake-pods.css deleted file mode 100644 index af1a5522ef..0000000000 --- a/frontend/src/v2/agents/v2-agent-wake-pods.css +++ /dev/null @@ -1,25 +0,0 @@ -.v2-agent-wake-pods { display: grid; gap: 24px; } -.v2-agent-wake-pods__section { border: 1px solid var(--v2-border); border-radius: 6px; padding: 20px; background: var(--v2-surface); } -.v2-agent-wake-pods__section h2 { margin: 0 0 16px; color: var(--v2-text-primary); font: 700 20px/28px var(--v2-font-display); } -.v2-agent-wake-pods__row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 44px; color: var(--v2-text-primary); font: 500 14px/20px var(--v2-font); } -.v2-agent-wake-pods__row input { width: 18px; height: 18px; accent-color: var(--v2-ink); } -.v2-agent-wake-pods__subrow { padding-left: 18px; } -.v2-agent-wake-pods__hint { margin: 4px 0 12px; color: var(--v2-text-muted); font: 400 13px/19px var(--v2-font); } -.v2-agent-settings .v2-agent-wake-pods .v2-agent-wake-pods__pod button { border: 1px solid var(--v2-border); border-radius: 4px; padding: 7px 10px; background: var(--v2-surface); color: var(--v2-text-primary); cursor: pointer; } -.v2-agent-wake-pods__row select { max-width: 55%; border: 1px solid var(--v2-border); border-radius: 4px; padding: 7px; background: var(--v2-surface); color: var(--v2-text-primary); } -.v2-agent-wake-pods__error { margin: 0; color: var(--v2-danger); } -.v2-agent-wake-pods__empty, .v2-agent-wake-pods__loading { margin: 0; color: var(--v2-text-muted); } -@media (max-width: 600px) { .v2-agent-wake-pods__section { padding: 16px; } } -.v2-agent-wake-pods__choice { min-width: 0; margin: 0; border: 0; padding: 0; } -.v2-agent-wake-pods__choice > span { display: block; padding: 12px 0 0; color: var(--v2-text-primary); font: 500 14px/20px var(--v2-font); } -.v2-agent-wake-pods__segment { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 0 0; } -.v2-agent-wake-pods__option { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0 10px; border: 1px solid var(--v2-border); border-radius: 999px; color: var(--v2-text-primary); font: 500 13px/18px var(--v2-font); cursor: pointer; } -.v2-agent-wake-pods__option input { width: 14px; height: 14px; accent-color: var(--v2-ink); } -.v2-agent-wake-pods__table { width: 100%; border-collapse: collapse; } -.v2-agent-wake-pods__table th, .v2-agent-wake-pods__table td { padding: 12px 10px; border-top: 1px solid var(--v2-border-soft); vertical-align: top; text-align: left; } -.v2-agent-wake-pods__table thead th { border-top: 0; color: var(--v2-text-muted); font: 600 12px/16px var(--v2-font); } -.v2-agent-wake-pods__table tbody th a { color: var(--v2-accent-text); font: 600 14px/20px var(--v2-font); } -.v2-agent-settings .v2-agent-wake-pods .v2-agent-wake-pods__remove { border: 1px solid var(--v2-border); border-radius: 4px; padding: 7px 10px; background: var(--v2-surface); color: var(--v2-text-secondary); cursor: pointer; } -.v2-agent-wake-pods__remove:disabled { opacity: .5; cursor: default; } -.v2-agent-wake-pods__sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; } -@media (max-width: 600px) { .v2-agent-wake-pods__table thead { display: none; } .v2-agent-wake-pods__table, .v2-agent-wake-pods__table tbody, .v2-agent-wake-pods__table tr, .v2-agent-wake-pods__table td, .v2-agent-wake-pods__table th { display: block; width: 100%; } } diff --git a/frontend/src/v2/components/V2AgentControlSection.tsx b/frontend/src/v2/components/V2AgentControlSection.tsx index cce854622f..e38fa44399 100644 --- a/frontend/src/v2/components/V2AgentControlSection.tsx +++ b/frontend/src/v2/components/V2AgentControlSection.tsx @@ -19,18 +19,18 @@ const V2AgentControlSection: React.FC = ({ agentName, instanceId, podId, const needsReconnect = connection.state === 'revoked' || connection.state === 'stale'; const canRegenerateMachineKey = runtimeType === 'wrapper' && connection.state === 'connected'; return ( -
-
-

{t('agentConnection.control')}

+
+ {t('agentSettings.connection', { defaultValue: 'Connection' })} + {connectionLabel(connection, t)} + {(needsReconnect || canRegenerateMachineKey) && ( - {t(needsReconnect ? 'agentConnection.reconnect' : 'agentSettings.regenerate')} )} -
-

{connectionLabel(connection, t)}

-
+ + ); }; @@ -92,10 +92,11 @@ export const AgentControlLoader: React.FC<{ }, [agentName, instanceId, refreshNonce, onConnectionState]); if (status) return ; - return
-

{t('agentConnection.control')}

-

+ return

+ {t('agentSettings.connection', { defaultValue: 'Connection' })} + {t(unavailable ? 'agentConnection.unavailable' : 'agentConnection.checking')} -

-
; + + + ; }; From aca02485772250f0f5b1ba8d4ae32ba62c379f93 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:51:57 -0700 Subject: [PATCH 2/8] fix(agent-settings): pin the menu and disclosure glyph sizes (TASK-731) UX Lead's 1440 light sheet at d90f19d53 showed the row-menu glyphs about 2px wide and the How it runs icon at 180px. The disclosure icon sat outside .lrow, so no size rule reached it, and the button glyphs could shrink as flex items. Pin both to the artboard values and cover them with a layout presence test. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/v2/__tests__/v2-layout-invariants.test.ts | 6 ++++++ frontend/src/v2/agents/v2-agent-settings-surface.css | 2 ++ 2 files changed, 8 insertions(+) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 7247a2111a..65372f35ea 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5067,6 +5067,12 @@ describe('agent settings surface (D223 lift)', () => { expect(ruleBody(css, '.v2-agent-settings-page .lrow')).toContain('grid-template-columns: 180px minmax(0, 1fr) auto'); expect(ruleBody(css, '.v2-agent-settings-page .wt')).toContain('border-collapse: collapse'); }); + + test('menu and disclosure glyphs keep their frame size instead of stretching or shrinking', () => { + expect(ruleBody(css, '.v2-agent-settings-page .ib svg.i')).toContain('width: 17px'); + expect(ruleBody(css, '.v2-agent-settings-page details.disc > summary .lb svg.i')).toContain('width: 16px'); + expect(ruleBody(css, '.v2-agent-settings-page .b svg.i, .v2-agent-settings-page .ib svg.i')).toContain('flex: none'); + }); }); describe('agent runtime and grants fit the Settings column', () => { diff --git a/frontend/src/v2/agents/v2-agent-settings-surface.css b/frontend/src/v2/agents/v2-agent-settings-surface.css index 0f8453565b..7f56863e04 100644 --- a/frontend/src/v2/agents/v2-agent-settings-surface.css +++ b/frontend/src/v2/agents/v2-agent-settings-surface.css @@ -49,6 +49,7 @@ .v2-agent-settings-page .ib { width: 32px; padding: 0; height: 32px; border-radius: 8px; } .v2-agent-settings-page .ib.b-sm { width: 28px; height: 28px; } .v2-agent-settings-page .ib svg.i { width: 17px; height: 17px; } +.v2-agent-settings-page .b svg.i, .v2-agent-settings-page .ib svg.i { flex: none; } /* segmented control */ .v2-agent-settings-page .seg { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--t); box-shadow: inset 0 0 0 1px var(--d); flex: none; } @@ -104,6 +105,7 @@ .v2-agent-settings-page .wt td { padding: 8px 14px; border-top: 1px solid var(--d); vertical-align: middle; color: var(--ink2); } .v2-agent-settings-page .wt td:first-child { font-weight: 600; color: var(--ink); max-width: 320px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .v2-agent-settings-page .wt td:last-child { text-align: right; width: 44px; } +.v2-agent-settings-page details.disc > summary .lb svg.i { width: 16px; height: 16px; color: var(--se); flex: none; } /* wake table controls: the message-wake segment, board and heartbeat toggles, the interval select */ .v2-agent-settings-page .wt .seg { display: inline-flex; flex-wrap: wrap; } From 68b134a1e2396fff6b849ee7ec7fb190d6227733 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:52:23 -0700 Subject: [PATCH 3/8] fix(agent-settings): phone header wraps and key rows fit 390 (TASK-731) At 390 the Regenerate key action spanned the full row and pushed the Settings tab off the header. The header now wraps, and the list row uses the two-column phone grid the artboard draws for .app.m .lrow. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/v2/__tests__/v2-layout-invariants.test.ts | 3 +++ frontend/src/v2/agents/v2-agent-settings-surface.css | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 65372f35ea..6b923ccc42 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5072,6 +5072,9 @@ describe('agent settings surface (D223 lift)', () => { expect(ruleBody(css, '.v2-agent-settings-page .ib svg.i')).toContain('width: 17px'); expect(ruleBody(css, '.v2-agent-settings-page details.disc > summary .lb svg.i')).toContain('width: 16px'); expect(ruleBody(css, '.v2-agent-settings-page .b svg.i, .v2-agent-settings-page .ib svg.i')).toContain('flex: none'); + expect(ruleBody(css, '.v2-agent-settings-page .lrow')).toContain('grid-template-columns: 180px minmax(0, 1fr) auto'); + expect(css).toContain('.v2-agent-settings-page .lrow { grid-template-columns: minmax(0, 1fr) auto;'); + expect(css).toContain('.v2-agent-settings-page .ph { flex-wrap: wrap;'); }); }); diff --git a/frontend/src/v2/agents/v2-agent-settings-surface.css b/frontend/src/v2/agents/v2-agent-settings-surface.css index 7f56863e04..6ed716ba53 100644 --- a/frontend/src/v2/agents/v2-agent-settings-surface.css +++ b/frontend/src/v2/agents/v2-agent-settings-surface.css @@ -122,6 +122,13 @@ .v2-agent-settings-page .v2-agent-wake-pods__error { margin: 0 0 8px; color: var(--ink); } .v2-agent-settings-page .v2-agent-wake-pods__empty { margin: 0; padding: 16px 14px; color: var(--se); font-size: 13px; } .v2-agent-settings-page .v2-agent-wake-pods__loading { padding: 16px 0; color: var(--se); } +@media (max-width: 600px) { + .v2-agent-settings-page .ph { flex-wrap: wrap; min-height: 0; padding: 14px 16px; row-gap: 10px; } + .v2-agent-settings-page .ph h1 { white-space: normal; overflow-wrap: anywhere; } + .v2-agent-settings-page .ph .sp { display: none; } + .v2-agent-settings-page .lrow { grid-template-columns: minmax(0, 1fr) auto; min-height: 48px; padding: 8px 12px; } + .v2-agent-settings-page .lrow .lb { font-weight: 500; } +} @media (max-width: 600px) { .v2-agent-settings-page .wt thead { display: none; } .v2-agent-settings-page .wt, .v2-agent-settings-page .wt tbody, .v2-agent-settings-page .wt tr, .v2-agent-settings-page .wt td { display: block; width: 100%; } From 0c7cb37cb5518fc32b4e65064ea38eb948d10882 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:03:38 -0800 Subject: [PATCH 4/8] fix(agent-settings): zh turn strings pass the spacing rule, and the Wake total is tested across pods (TASK-731) The zh-CN turns strings had a space between the numeral and the hanzi, which the zh number-spacing test rejects (it failed Frontend tests on 68b134a1e). Also adds the aggregate test CodeRabbit asked for: two enabled pods and one Off pod total 72 / 30 turns a day. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/i18n/locales/zh-CN.json | 4 ++-- frontend/src/v2/__tests__/V2AgentWakePods.test.tsx | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 2492adc083..8b83d9a264 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2891,8 +2891,8 @@ "messageWakeShort_mentions": "仅提及", "messageWakeShort_human": "人", "messageWakeShort_all": "全部", - "turnsUsed": "{{used}} / {{cap}} 次/天", - "turnsUsedNoCap": "{{used}} 次/天 · 上限不可用" + "turnsUsed": "{{used}} / {{cap}}次/天", + "turnsUsedNoCap": "{{used}}次/天 · 上限不可用" }, "agentPolicy": { "title": "策略", diff --git a/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx b/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx index bb297fc99f..c591e5eb53 100644 --- a/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx @@ -205,6 +205,19 @@ describe('per-agent Wake and Pods settings', () => { expect(updatePod).not.toHaveBeenCalled(); }); + it('totals heartbeat turns across pods, counting an Off pod as zero', async () => { + client.get.mockResolvedValue({ data: { turnCap: 30, activePodCount: 2, podCount: 3, installations: [ + { podId: 'pod-a', podName: 'Alpha', status: 'active', wakeOnMessage: false, humanOnly: false, + boardWake: false, heartbeatEnabled: true, heartbeatMinutes: 60 }, + { podId: 'pod-b', podName: 'Beta', status: 'active', wakeOnMessage: false, humanOnly: false, + boardWake: false, heartbeatEnabled: false, heartbeatMinutes: 30 }, + { podId: 'pod-c', podName: 'Gamma', status: 'active', wakeOnMessage: false, humanOnly: false, + boardWake: false, heartbeatEnabled: true, heartbeatMinutes: 30 }, + ] } }); + renderSettings(); + expect(await screen.findByText('72 / 30 turns a day')).toBeInTheDocument(); + }); + it('states the daily turn count clearly when the cap is unavailable', async () => { client.get.mockResolvedValue({ data: { activePodCount: 1, podCount: 1, installations: [{ podId: 'pod-1', podName: 'Launch', status: 'active', wakeOnMessage: false, humanOnly: false, From f4bcf3256fa3c085b59a36e8d4284d05015ba5bd Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 01:05:46 -0800 Subject: [PATCH 5/8] fix(i18n): zh-CN agent settings labels use the glossary terms (TASK-731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Column header 看板 becomes 任务板 (the glossary's task board), and the human-only wake label 人 becomes 真人 so it reads as messages from people. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/i18n/locales/zh-CN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 8b83d9a264..af030f8bfe 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2886,10 +2886,10 @@ "noPod": "此智能体没有活跃的 Pod。", "missingReplacement": "未返回新密钥。", "colMessages": "消息", - "colBoard": "看板", + "colBoard": "任务板", "colHeartbeat": "心跳", "messageWakeShort_mentions": "仅提及", - "messageWakeShort_human": "人", + "messageWakeShort_human": "真人", "messageWakeShort_all": "全部", "turnsUsed": "{{used}} / {{cap}}次/天", "turnsUsedNoCap": "{{used}}次/天 · 上限不可用" From 176e75f65f1fcd0dbf5ba90cecba70ae5743254b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 03:38:11 -0800 Subject: [PATCH 6/8] fix(agent-settings): close the code-gate HOLD on the lift (TASK-731) - Add the seven missing agentSettings keys to en and zh-CN (identityAccess, keyActive, openMemory, heartbeatOff, connection) and drop their inline defaults. zh-CN uses the glossary terms. - Remove the empty "How it runs" disclosure. The runtime controls stay where they are in V2AgentRuntimeGrants, so the heading no longer shows twice. Restore the Identity-before-How-it-runs order assertion. - Stop the old `.v2-root .v2-agent-settings button` rule (0,3,1) from out-ranking the lifted 28px icon buttons and segment tabs. It now excludes `.ib` and `.seg > button`; kept sections keep their current chrome. Hit-test (headless Chromium, real CSS): .ib.b-sm 28x28, seg tab 28 tall, seg--sm tab 24 tall, kept buttons unchanged at 34. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/i18n/locales/en.json | 5 +++++ frontend/src/i18n/locales/zh-CN.json | 5 +++++ .../src/v2/__tests__/V2AgentSettingsPage.test.tsx | 2 +- .../src/v2/__tests__/v2-layout-invariants.test.ts | 2 +- frontend/src/v2/agents/V2AgentSettingsPage.tsx | 15 ++++----------- frontend/src/v2/agents/V2AgentWakePods.tsx | 2 +- frontend/src/v2/agents/v2-agent-settings.css | 10 ++++++---- .../src/v2/components/V2AgentControlSection.tsx | 4 ++-- 8 files changed, 25 insertions(+), 20 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 3c113507ec..26b045fba8 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2786,6 +2786,11 @@ }, "agentSettings": { "identityTitle": "Identity", + "identityAccess": "Identity & access", + "keyActive": "active", + "openMemory": "Open memory", + "heartbeatOff": "Off", + "connection": "Connection", "displayName": "Display name", "persona": "Persona", "tone": "Tone", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index af030f8bfe..3d68de2298 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2789,6 +2789,11 @@ }, "agentSettings": { "identityTitle": "身份", + "identityAccess": "身份与访问", + "keyActive": "已启用", + "openMemory": "打开记忆", + "heartbeatOff": "关闭", + "connection": "连接", "displayName": "显示名称", "persona": "角色设定", "tone": "语气", diff --git a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx index 204d87e744..4705699567 100644 --- a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx @@ -463,7 +463,7 @@ test('runtime editor writes environment controls through the shared settings hoo // D215 artboard: Wake first, then Identity & access (Key, Identity). expect(at('Wake')).toBeLessThan(at('Key')); expect(at('Key')).toBeLessThan(at('Identity')); - expect(at('How it runs')).toBeLessThan(at('Identity')); + expect(at('Identity')).toBeLessThan(at('How it runs')); expect(at('Wake')).toBeLessThan(at('How it runs')); expect(at('How it runs')).toBeLessThan(at('Connector grants')); expect(at('Connector grants')).toBeLessThan(at('Policy')); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 6b923ccc42..7908809102 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -209,7 +209,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { }); test('agent Settings buttons retain their chrome over the v2 button reset', () => { - const button = ruleBody(agentSettings, '.v2-root .v2-agent-settings button:not(.MuiButtonBase-root)'); + const button = ruleBody(agentSettings, '.v2-root .v2-agent-settings button:not(.MuiButtonBase-root):not(.ib):not(.seg > button)'); expect(button).toContain('border: 1px solid var(--v2-border)'); expect(button).toContain('background: var(--v2-surface)'); expect(ruleBody(agentSettings, '.v2-root .v2-agent-settings .v2-agent-settings__section-head button')).toContain('background: var(--v2-ink)'); diff --git a/frontend/src/v2/agents/V2AgentSettingsPage.tsx b/frontend/src/v2/agents/V2AgentSettingsPage.tsx index d03304a126..106649c882 100644 --- a/frontend/src/v2/agents/V2AgentSettingsPage.tsx +++ b/frontend/src/v2/agents/V2AgentSettingsPage.tsx @@ -271,13 +271,13 @@ const V2AgentSettingsPage: React.FC = () => {
-

{t('agentSettings.identityAccess', { defaultValue: 'Identity & access' })}

+

{t('agentSettings.identityAccess')}

- {keysForbidden ? t('agentSettings.keyUnavailable') : activeKeys.length ? t('agentSettings.keyActive', { defaultValue: 'active' }) : t('agentSettings.noActiveKeys')} + {keysForbidden ? t('agentSettings.keyUnavailable') : activeKeys.length ? t('agentSettings.keyActive') : t('agentSettings.noActiveKeys')} {keysForbidden ? null : installation?.runtime?.runtimeType === 'wrapper' ? ( {t('agentSettings.wrapperReconnect')} @@ -322,19 +322,12 @@ const V2AgentSettingsPage: React.FC = () => { + aria-label={t('agentSettings.openMemory')} + title={t('agentSettings.openMemory')}>
-
- - {t('agentSettings.howItRuns', { defaultValue: 'How it runs' })} - {installation?.runtime?.runtimeType || t('agentSettings.notReported', { defaultValue: 'not reported' })} - - -
{installation && } ({ ...current, [entry.podId]: value === 'off' })); if (value !== 'off') setDraftMinutes((current) => ({ ...current, [entry.podId]: Number(value) })); }}> - + {[...new Set([15, 30, 60, 120, 360, 720, 1440, entry.heartbeatMinutes])] .sort((a, b) => a - b).map((value) => ( diff --git a/frontend/src/v2/agents/v2-agent-settings.css b/frontend/src/v2/agents/v2-agent-settings.css index f85c977e0f..bc419d5bf1 100644 --- a/frontend/src/v2/agents/v2-agent-settings.css +++ b/frontend/src/v2/agents/v2-agent-settings.css @@ -41,7 +41,9 @@ .v2-agent-settings__section { padding: 28px 0; } .v2-agent-settings__section-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .v2-agent-settings__section h2 { margin: 0; font-family: var(--v2-font-display); font-size: 20px; } -.v2-root .v2-agent-settings button:not(.MuiButtonBase-root) { +/* Icon buttons and segment tabs belong to the lifted surface (v2-agent-settings-surface.css, 28px); + excluded here so this (0,3,1) rule does not out-rank their sizing. */ +.v2-root .v2-agent-settings button:not(.MuiButtonBase-root):not(.ib):not(.seg > button) { min-height: 34px; padding: 6px 12px; border: 1px solid var(--v2-border); @@ -53,9 +55,9 @@ font-weight: 600; cursor: pointer; } -.v2-root .v2-agent-settings button:hover { background: var(--v2-surface-hover); } -.v2-root .v2-agent-settings button:focus-visible { box-shadow: var(--v2-focus-ring); outline: 1px solid var(--v2-accent); } -.v2-root .v2-agent-settings button:disabled { opacity: .55; cursor: default; } +.v2-root .v2-agent-settings button:not(.ib):not(.seg > button):hover { background: var(--v2-surface-hover); } +.v2-root .v2-agent-settings button:not(.ib):not(.seg > button):focus-visible { box-shadow: var(--v2-focus-ring); outline: 1px solid var(--v2-accent); } +.v2-root .v2-agent-settings button:not(.ib):not(.seg > button):disabled { opacity: .55; cursor: default; } .v2-root .v2-agent-settings .v2-agent-settings__section-head button { background: var(--v2-ink); color: var(--v2-on-ink); border-color: var(--v2-ink); } .v2-root .v2-agent-settings .v2-agent-settings__actions button:first-child { background: var(--v2-ink); color: var(--v2-on-ink); border-color: var(--v2-ink); } .v2-root .v2-agent-settings .v2-agent-settings__section-head button:hover, diff --git a/frontend/src/v2/components/V2AgentControlSection.tsx b/frontend/src/v2/components/V2AgentControlSection.tsx index e38fa44399..676ff50f48 100644 --- a/frontend/src/v2/components/V2AgentControlSection.tsx +++ b/frontend/src/v2/components/V2AgentControlSection.tsx @@ -20,7 +20,7 @@ const V2AgentControlSection: React.FC = ({ agentName, instanceId, podId, const canRegenerateMachineKey = runtimeType === 'wrapper' && connection.state === 'connected'; return (
- {t('agentSettings.connection', { defaultValue: 'Connection' })} + {t('agentSettings.connection')} {connectionLabel(connection, t)} {(needsReconnect || canRegenerateMachineKey) && ( @@ -93,7 +93,7 @@ export const AgentControlLoader: React.FC<{ if (status) return ; return
- {t('agentSettings.connection', { defaultValue: 'Connection' })} + {t('agentSettings.connection')} {t(unavailable ? 'agentConnection.unavailable' : 'agentConnection.checking')} From 0b61f7c0fe5c64af359fde31dd65369ec3d37cf6 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 06:16:14 -0800 Subject: [PATCH 7/8] fix(agent-settings): .b controls keep 28px and segment tabs keep focus ring (TASK-731) - The old `.v2-root .v2-agent-settings button` rule now excludes `.b` (lifted controls, including Regenerate key and the Wake save button) instead of `.ib`. Those controls were still picking up its 34px min-height. - Segment tabs (`.seg > button`, `.seg > a`) get the same focus ring `.b` uses, because the old base focus rule no longer reaches them. - Guards for both in the layout invariants. Headless Chromium on the real CSS: .b.b-g.b-sm 28px, .ib 28px, seg tab 28px, plain kept button 34px (unchanged), tab focus-visible shows the ring. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- .../src/v2/__tests__/v2-layout-invariants.test.ts | 11 ++++++++++- frontend/src/v2/agents/v2-agent-settings-surface.css | 1 + frontend/src/v2/agents/v2-agent-settings.css | 12 ++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 7908809102..3062061648 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -209,7 +209,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { }); test('agent Settings buttons retain their chrome over the v2 button reset', () => { - const button = ruleBody(agentSettings, '.v2-root .v2-agent-settings button:not(.MuiButtonBase-root):not(.ib):not(.seg > button)'); + const button = ruleBody(agentSettings, '.v2-root .v2-agent-settings button:not(.MuiButtonBase-root):not(.b):not(.seg > button)'); expect(button).toContain('border: 1px solid var(--v2-border)'); expect(button).toContain('background: var(--v2-surface)'); expect(ruleBody(agentSettings, '.v2-root .v2-agent-settings .v2-agent-settings__section-head button')).toContain('background: var(--v2-ink)'); @@ -5056,6 +5056,15 @@ describe('agent settings surface (D223 lift)', () => { expect(ruleBody(css, '.v2-agent-settings-page .b-s')).toContain('background: linear-gradient'); }); + 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)'); + }); + + test('lifted .b-sm controls keep their 28px size', () => { + expect(ruleBody(css, '.v2-agent-settings-page .b-sm')).toContain('height: 28px'); + }); + test('message-wake segment wraps its options instead of overflowing the Wake row', () => { const segment = ruleBody(css, '.v2-agent-settings-page .wt .seg'); expect(segment).toContain('display: inline-flex'); diff --git a/frontend/src/v2/agents/v2-agent-settings-surface.css b/frontend/src/v2/agents/v2-agent-settings-surface.css index 6ed716ba53..46dac5f420 100644 --- a/frontend/src/v2/agents/v2-agent-settings-surface.css +++ b/frontend/src/v2/agents/v2-agent-settings-surface.css @@ -55,6 +55,7 @@ .v2-agent-settings-page .seg { display: inline-flex; gap: 2px; padding: 2px; border-radius: 9px; background: var(--t); box-shadow: inset 0 0 0 1px var(--d); flex: none; } .v2-agent-settings-page .seg > button, .v2-agent-settings-page .seg > a { all: unset; box-sizing: border-box; cursor: pointer; height: 28px; padding: 0 10px; border-radius: 7px; display: inline-flex; align-items: center; gap: 6px; font: 500 12.5px/1 var(--f-body); color: var(--se); white-space: nowrap; text-decoration: none; } .v2-agent-settings-page .seg > .on, .v2-agent-settings-page .seg > [aria-current="page"] { background: var(--sf); color: var(--ink); font-weight: 600; box-shadow: 0 0 0 1px var(--key-edge), 0 1px 2px rgba(16,24,40,.08); } +.v2-agent-settings-page .seg > button:focus-visible, .v2-agent-settings-page .seg > a:focus-visible { box-shadow: 0 0 0 3px var(--c-soft), 0 0 0 1px var(--c); } .v2-agent-settings-page .seg--sm > button { height: 24px; padding: 0 8px; font-size: 12px; } .v2-agent-settings-page .tg { width: 30px; height: 18px; border-radius: 9px; background: var(--b); position: relative; flex: none; } .v2-agent-settings-page .tg::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--sf); box-shadow: 0 1px 2px rgba(0,0,0,.2); } diff --git a/frontend/src/v2/agents/v2-agent-settings.css b/frontend/src/v2/agents/v2-agent-settings.css index bc419d5bf1..4b85fc4d18 100644 --- a/frontend/src/v2/agents/v2-agent-settings.css +++ b/frontend/src/v2/agents/v2-agent-settings.css @@ -41,9 +41,9 @@ .v2-agent-settings__section { padding: 28px 0; } .v2-agent-settings__section-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .v2-agent-settings__section h2 { margin: 0; font-family: var(--v2-font-display); font-size: 20px; } -/* Icon buttons and segment tabs belong to the lifted surface (v2-agent-settings-surface.css, 28px); - excluded here so this (0,3,1) rule does not out-rank their sizing. */ -.v2-root .v2-agent-settings button:not(.MuiButtonBase-root):not(.ib):not(.seg > button) { +/* Lifted buttons (.b, icon .ib, segment tabs) belong to the surface (v2-agent-settings-surface.css); + excluded here so this (0,3,1) rule does not out-rank their sizing or focus ring. */ +.v2-root .v2-agent-settings button:not(.MuiButtonBase-root):not(.b):not(.seg > button) { min-height: 34px; padding: 6px 12px; border: 1px solid var(--v2-border); @@ -55,9 +55,9 @@ font-weight: 600; cursor: pointer; } -.v2-root .v2-agent-settings button:not(.ib):not(.seg > button):hover { background: var(--v2-surface-hover); } -.v2-root .v2-agent-settings button:not(.ib):not(.seg > button):focus-visible { box-shadow: var(--v2-focus-ring); outline: 1px solid var(--v2-accent); } -.v2-root .v2-agent-settings button:not(.ib):not(.seg > button):disabled { opacity: .55; cursor: default; } +.v2-root .v2-agent-settings button:not(.b):not(.seg > button):hover { background: var(--v2-surface-hover); } +.v2-root .v2-agent-settings button:not(.b):not(.seg > button):focus-visible { box-shadow: var(--v2-focus-ring); outline: 1px solid var(--v2-accent); } +.v2-root .v2-agent-settings button:not(.b):not(.seg > button):disabled { opacity: .55; cursor: default; } .v2-root .v2-agent-settings .v2-agent-settings__section-head button { background: var(--v2-ink); color: var(--v2-on-ink); border-color: var(--v2-ink); } .v2-root .v2-agent-settings .v2-agent-settings__actions button:first-child { background: var(--v2-ink); color: var(--v2-on-ink); border-color: var(--v2-ink); } .v2-root .v2-agent-settings .v2-agent-settings__section-head button:hover, From 02cd7e93507d06c856b9407df60e36c480c582be Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 06:21:57 -0800 Subject: [PATCH 8/8] fix(agent-settings): disabled lifted controls dim again (TASK-731) The old :disabled rule skips .b and seg tabs after the .b exclusion, so a disabled lifted button stopped dimming. Add the same opacity and cursor on .b:disabled and .seg > button:disabled in the surface sheet, with a guard. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/v2/__tests__/v2-layout-invariants.test.ts | 5 +++++ frontend/src/v2/agents/v2-agent-settings-surface.css | 1 + 2 files changed, 6 insertions(+) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 3062061648..4649103528 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5061,6 +5061,11 @@ describe('agent settings surface (D223 lift)', () => { .toContain('box-shadow: 0 0 0 3px var(--c-soft)'); }); + test('disabled lifted controls still dim, since the old :disabled rule skips .b and seg tabs', () => { + expect(ruleBody(css, '.v2-agent-settings-page .b:disabled, .v2-agent-settings-page .seg > button:disabled')) + .toContain('opacity: .55'); + }); + test('lifted .b-sm controls keep their 28px size', () => { expect(ruleBody(css, '.v2-agent-settings-page .b-sm')).toContain('height: 28px'); }); diff --git a/frontend/src/v2/agents/v2-agent-settings-surface.css b/frontend/src/v2/agents/v2-agent-settings-surface.css index 46dac5f420..e194c3ffe3 100644 --- a/frontend/src/v2/agents/v2-agent-settings-surface.css +++ b/frontend/src/v2/agents/v2-agent-settings-surface.css @@ -56,6 +56,7 @@ .v2-agent-settings-page .seg > button, .v2-agent-settings-page .seg > a { all: unset; box-sizing: border-box; cursor: pointer; height: 28px; padding: 0 10px; border-radius: 7px; display: inline-flex; align-items: center; gap: 6px; font: 500 12.5px/1 var(--f-body); color: var(--se); white-space: nowrap; text-decoration: none; } .v2-agent-settings-page .seg > .on, .v2-agent-settings-page .seg > [aria-current="page"] { background: var(--sf); color: var(--ink); font-weight: 600; box-shadow: 0 0 0 1px var(--key-edge), 0 1px 2px rgba(16,24,40,.08); } .v2-agent-settings-page .seg > button:focus-visible, .v2-agent-settings-page .seg > a:focus-visible { box-shadow: 0 0 0 3px var(--c-soft), 0 0 0 1px var(--c); } +.v2-agent-settings-page .b:disabled, .v2-agent-settings-page .seg > button:disabled { opacity: .55; cursor: default; } .v2-agent-settings-page .seg--sm > button { height: 24px; padding: 0 8px; font-size: 12px; } .v2-agent-settings-page .tg { width: 30px; height: 18px; border-radius: 9px; background: var(--b); position: relative; flex: none; } .v2-agent-settings-page .tg::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--sf); box-shadow: 0 1px 2px rgba(0,0,0,.2); }