diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index f4f14cd8b..26b045fba 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", @@ -2881,7 +2886,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 51dcc5beb..3d68de229 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": "语气", @@ -2884,7 +2889,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 6eeae4d67..470569956 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('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')); + 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 b59d17c49..c591e5eb5 100644 --- a/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx @@ -198,20 +198,33 @@ 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(); }); + 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, - 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 19c13c8e3..464910352 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(.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)'); @@ -5050,29 +5050,45 @@ 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)'); +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 row', () => { - const segment = ruleBody(css, '.v2-agent-wake-pods__segment'); - expect(segment).toContain('display: flex'); + 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('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'); + }); + + 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'); + }); + + 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'); + 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/V2AgentSettingsPage.tsx b/frontend/src/v2/agents/V2AgentSettingsPage.tsx index 11e6cc1f4..106649c88 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,81 @@ 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')}

+
+ +
+ + {keysForbidden ? t('agentSettings.keyUnavailable') : activeKeys.length ? t('agentSettings.keyActive') : 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)}
-
} - - )} -
+
{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 d077623a3..f775d6926 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 000000000..e194c3ffe --- /dev/null +++ b/frontend/src/v2/agents/v2-agent-settings-surface.css @@ -0,0 +1,153 @@ +/* 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; } +.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; } +.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); } +.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; } +.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; } +.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 .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%; } +} +.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-settings.css b/frontend/src/v2/agents/v2-agent-settings.css index f85c977e0..4b85fc4d1 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) { +/* 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); @@ -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(.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, 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 af1a5522e..000000000 --- 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 cce854622..676ff50f4 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')} + {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')} + {t(unavailable ? 'agentConnection.unavailable' : 'agentConnection.checking')} -

-
; + + + ; };