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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2789,6 +2789,11 @@
},
"agentSettings": {
"identityTitle": "身份",
"identityAccess": "身份与访问",
"keyActive": "已启用",
"openMemory": "打开记忆",
"heartbeatOff": "关闭",
"connection": "连接",
"displayName": "显示名称",
"persona": "角色设定",
"tone": "语气",
Expand Down Expand Up @@ -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": "策略",
Expand Down
25 changes: 14 additions & 11 deletions frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }));
Expand Down Expand Up @@ -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(
Expand Down
23 changes: 18 additions & 5 deletions frontend/src/v2/__tests__/V2AgentWakePods.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
54 changes: 35 additions & 19 deletions frontend/src/v2/__tests__/v2-layout-invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)');
Expand Down Expand Up @@ -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;');
});
});

Expand Down
Loading
Loading