From 567782d281034da038b243c046ae9d8a19776a30 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 31 Oct 2026 23:33:06 -0700 Subject: [PATCH 1/4] Fold the per-pod wake card into one Wake table The agent settings Wake section rendered one card per pod. The D223 artboard shows a single table, one row per pod, with the message-wake segment, board switch, heartbeat select and actions in the row. - Replace the pod cards with a table (pod, messages, board, heartbeat, remove). The message-wake segment stays the only message-wake control. - Update the pod-count and Pods-column assertions for the table. - Point the layout invariants at the table's save and remove buttons. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- .../v2/__tests__/V2AgentSettingsPage.test.tsx | 2 +- .../src/v2/__tests__/V2AgentWakePods.test.tsx | 4 +- .../v2/__tests__/v2-layout-invariants.test.ts | 4 +- frontend/src/v2/agents/V2AgentWakePods.tsx | 151 ++++++++++-------- frontend/src/v2/agents/v2-agent-wake-pods.css | 17 +- 5 files changed, 98 insertions(+), 80 deletions(-) diff --git a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx index 1612b48a1e..6eeae4d674 100644 --- a/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx @@ -348,7 +348,7 @@ test('a partial installation owner cannot see identity-wide key controls', async renderPage(); expect(await screen.findByRole('heading', { name: 'Policy' })).toBeInTheDocument(); expect(await screen.findByText('Key management is unavailable for this agent. Ask an admin for access.')).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'Pods' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: 'Pods' })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Regenerate key' })).not.toBeInTheDocument(); expect(screen.queryByText('Laptop')).not.toBeInTheDocument(); expect(axios.get.mock.calls.some(([url]) => url === '/api/agent-memory/maya-claude/default')).toBe(false); diff --git a/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx b/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx index dce2de0c26..b59d17c491 100644 --- a/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentWakePods.test.tsx @@ -188,12 +188,12 @@ describe('per-agent Wake and Pods settings', () => { view.rerender( , ); - expect((await screen.findAllByText('New pod')).length).toBe(2); + expect((await screen.findAllByText('New pod')).length).toBe(1); await act(async () => { finishOld({ data: { installations: [{ podId: 'pod-old', podName: 'Old pod', wakeOnMessage: true, boardWake: true, heartbeatMinutes: 60, }] } }); }); expect(screen.queryByText('Old pod')).not.toBeInTheDocument(); - expect(screen.getAllByText('New pod')).toHaveLength(2); + expect(screen.getByText('New pod')).toBeInTheDocument(); }); it('previews heartbeat turns against the plan cap before saving', async () => { diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index e6920c50f4..deb58eb50f 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5053,8 +5053,8 @@ 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__pod-row button'); + 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)'); diff --git a/frontend/src/v2/agents/V2AgentWakePods.tsx b/frontend/src/v2/agents/V2AgentWakePods.tsx index 3024896042..d077623a33 100644 --- a/frontend/src/v2/agents/V2AgentWakePods.tsx +++ b/frontend/src/v2/agents/V2AgentWakePods.tsx @@ -185,74 +185,89 @@ const V2AgentWakePods: React.FC<{

{t('agentSettings.wakeTitle')}

{t('agentSettings.mentionsAlwaysWake')}

- {installations.map((entry) => ( -
- {entry.podName} -
- {t('agentSettings.messageWake')} -
- {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 - && } -
- ))} -
-
-

{t('agentSettings.podsTitle')}

- {installations.length ? installations.map((entry) => ( -
- {entry.podName} - -
- )) :

{t('agentSettings.noPods')}

} + {installations.length ? ( + + + + + + + + + + + + {installations.map((entry) => ( + + + + + + + + ))} + +
{t('agentSettings.podsTitle')}{t('agentSettings.messageWake')}{t('agentSettings.boardWake')}{t('agentSettings.heartbeat')}{t('agentSettings.remove')}
{entry.podName} +
+
+ {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 + && } +
+ +
+ ) :

{t('agentSettings.noPods')}

}
); diff --git a/frontend/src/v2/agents/v2-agent-wake-pods.css b/frontend/src/v2/agents/v2-agent-wake-pods.css index 30d4faf502..f63f898c2c 100644 --- a/frontend/src/v2/agents/v2-agent-wake-pods.css +++ b/frontend/src/v2/agents/v2-agent-wake-pods.css @@ -1,17 +1,12 @@ .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__pod + .v2-agent-wake-pods__pod { border-top: 1px solid var(--v2-border-soft); margin-top: 16px; padding-top: 16px; } -.v2-agent-wake-pods__pod > a, .v2-agent-wake-pods__pod-row > a { color: var(--v2-accent-text); font: 600 14px/20px var(--v2-font); } -.v2-agent-wake-pods__row, .v2-agent-wake-pods__pod-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 { 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-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__pod-row + .v2-agent-wake-pods__pod-row { border-top: 1px solid var(--v2-border-soft); } -.v2-agent-settings .v2-agent-wake-pods .v2-agent-wake-pods__pod-row button { 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__pod-row button:disabled { opacity: .5; cursor: default; } .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; } } @@ -20,3 +15,11 @@ .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; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } +@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%; } } From 239ca6086d35defa98abd2937723804b0d202435 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 31 Oct 2026 23:55:39 -0700 Subject: [PATCH 2/4] Complete the visually-hidden pattern on the wake sr-only class srOnlyPattern pins one shape for visually-hidden text. The wake table's sr-only class declared only part of it, so the test failed on CI. Add padding, margin, border and clip-path to match the pattern. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: kite Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/v2/agents/v2-agent-wake-pods.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/v2/agents/v2-agent-wake-pods.css b/frontend/src/v2/agents/v2-agent-wake-pods.css index f63f898c2c..af1a5522ef 100644 --- a/frontend/src/v2/agents/v2-agent-wake-pods.css +++ b/frontend/src/v2/agents/v2-agent-wake-pods.css @@ -21,5 +21,5 @@ .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; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } +.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%; } } From b881aada21ad0223babf294a9d5579d33ad82397 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:02:35 -0700 Subject: [PATCH 3/4] Give agent settings the frame's avatar header with tabs The settings page opened with a page head that carried a floating "Your team" link and a separate tab row. The D223 frame puts the avatar, name and Profile / Memory / Settings tabs on one header row, the same header the Memory page already uses. - Replace the page head and the "Your team" action with the avatar header. - Move the tabs into that header, with Settings as the current page. - Pin the header row and its tabs in the layout invariants. 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 | 8 ++++++++ frontend/src/v2/agents/V2AgentSettingsPage.tsx | 15 ++++++++++----- frontend/src/v2/agents/v2-agent-settings.css | 6 ++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index deb58eb50f..b54b8338f0 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5065,6 +5065,14 @@ describe('agent Wake and Pods controls', () => { expect(segment).toContain('display: 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__seg')).toContain('margin-left: auto'); + }); }); describe('agent runtime and grants fit the Settings column', () => { diff --git a/frontend/src/v2/agents/V2AgentSettingsPage.tsx b/frontend/src/v2/agents/V2AgentSettingsPage.tsx index d6439d6b16..11e6cc1f4b 100644 --- a/frontend/src/v2/agents/V2AgentSettingsPage.tsx +++ b/frontend/src/v2/agents/V2AgentSettingsPage.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import axios from '../../utils/axiosConfig'; -import V2PageHead from '../components/V2PageHead'; +import V2Avatar from '../components/V2Avatar'; import V2AgentRuntimeGrants, { Runtime } from '../components/V2AgentRuntimeGrants'; import { AgentControlLoader } from '../components/V2AgentControlSection'; import { AgentConnection } from '../utils/agentConnection'; @@ -254,13 +254,18 @@ const V2AgentSettingsPage: React.FC = () => { return (
- {t('agentSettings.yourTeam')}} /> -
-