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
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,7 @@
"answer": "Answer",
"talk": "Talk",
"podsAria": "Pods",
"morePodsAria": "{{count}} more pods",
"age": {
"now": "now",
"minutes": "{{count}}m",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1937,6 +1937,7 @@
"answer": "回答",
"talk": "对话",
"podsAria": "Pod",
"morePodsAria": "还有{{count}}个 Pod",
"age": {
"now": "刚刚",
"minutes": "{{count}}分",
Expand Down
64 changes: 54 additions & 10 deletions frontend/src/v2/__tests__/V2ActivityPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,50 @@ describe('V2ActivityPage', () => {
expect(second).toHaveAttribute('aria-describedby', expect.stringContaining('description'));
});

test('clamps long queue details until the reader expands them', async () => {
const detail = 'This queue detail is intentionally long so the compact Activity row can reveal the complete context on demand.';
const queueWithLongDetail = {
...decisionQueue,
items: [{ ...decisionQueue.items[0], detail }],
count: 1,
};
mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? queueWithLongDetail : recap }));
renderPage();

const detailButton = await screen.findByRole('button', { name: detail });
expect(detailButton).toHaveClass('v2-activity__queue-detail');
expect(detailButton).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(detailButton);
expect(detailButton).toHaveAttribute('aria-expanded', 'true');
fireEvent.click(detailButton);
expect(detailButton).toHaveAttribute('aria-expanded', 'false');
});

test('a keyboard user with focus on the detail collapses it on the second toggle', async () => {
const detail = 'Keyboard focus must not hold a queue detail open after the second Enter.';
const queueWithLongDetail = {
...decisionQueue,
items: [{ ...decisionQueue.items[0], detail }],
count: 1,
};
mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? queueWithLongDetail : recap }));
renderPage();

const detailButton = await screen.findByRole('button', { name: detail });
// Focus stays on the button across both toggles, as it does for Enter on a focused control.
detailButton.focus();
fireEvent.click(detailButton);
expect(detailButton).toHaveClass('v2-activity__queue-detail--expanded');
fireEvent.click(detailButton);
expect(detailButton).not.toHaveClass('v2-activity__queue-detail--expanded');
expect(detailButton).toHaveClass('v2-activity__queue-detail--dismissed');
expect(detailButton).toHaveAttribute('aria-expanded', 'false');

// Blur clears the dismissal, so a later Tab-in previews the full copy again.
fireEvent.blur(detailButton);
expect(detailButton).not.toHaveClass('v2-activity__queue-detail--dismissed');
});

test('changes the read window and opens the source pod from a factual queue row', async () => {
renderPage();
await screen.findByText('Review requested');
Expand Down Expand Up @@ -317,7 +361,7 @@ describe('V2ActivityPage', () => {
expect(screen.getByText('Nothing needs you.')).toBeInTheDocument();
expect(screen.queryByLabelText(/waiting on you/)).not.toBeInTheDocument();
// Nobody to wake: the composer is hidden while step 1 is open.
expect(screen.queryByRole('heading', { name: /tell your agents/i })).not.toBeInTheDocument();
expect(screen.queryByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).not.toBeInTheDocument();
const card = screen.getByRole('heading', { name: 'Get started' }).closest('section') as HTMLElement;
expect(card.compareDocumentPosition(screen.getByRole('heading', { name: 'Needs you' })) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Hire an agent' }));
Expand All @@ -333,7 +377,7 @@ describe('V2ActivityPage', () => {
expect(await screen.findByText('2 steps · until your first ask arrives')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Hire an agent' })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Open Launch pod' })).toHaveClass('v2-activity__start-cta--current');
expect(screen.getByRole('heading', { name: /tell your agents/i })).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).toBeInTheDocument();
second.unmount();

// The account spoke, reply or not: step 2 leaves — with the connector row too, no card at all.
Expand Down Expand Up @@ -436,9 +480,9 @@ describe('V2ActivityPage', () => {

renderPage();

expect(await screen.findByRole('heading', { name: /tell your agents/i })).toBeInTheDocument();
expect(await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).toBeInTheDocument();
await act(async () => { releaseRegistry?.({ data: { agents: [] } }); });
await waitFor(() => expect(screen.queryByRole('heading', { name: /tell your agents/i })).not.toBeInTheDocument());
await waitFor(() => expect(screen.queryByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).not.toBeInTheDocument());
expect(screen.queryByRole('heading', { name: 'Get started' })).not.toBeInTheDocument();
expect(screen.getByText('Nothing needs you.')).toBeInTheDocument();
});
Expand All @@ -456,9 +500,9 @@ describe('V2ActivityPage', () => {

renderPage();

expect(await screen.findByRole('heading', { name: /tell your agents/i })).toBeInTheDocument();
expect(await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).toBeInTheDocument();
await act(async () => { releaseRegistry?.({ data: { agents: [] } }); });
await waitFor(() => expect(screen.queryByRole('heading', { name: /tell your agents/i })).not.toBeInTheDocument());
await waitFor(() => expect(screen.queryByRole('textbox', { name: i18n.t('activity.compose.placeholder') })).not.toBeInTheDocument());
expect(screen.queryByRole('heading', { name: 'Get started' })).not.toBeInTheDocument();
});

Expand Down Expand Up @@ -1346,7 +1390,7 @@ describe('V2ActivityPage', () => {
const activityRecap = { ...recap, pods: [...recap.pods, { id: 'pod-2', name: 'GTM Programs' }] };
mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? decisionQueue : activityRecap }));
renderPage();
await screen.findByRole('heading', { name: 'Tell your agents' });
await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') });
const picker = document.querySelector<HTMLButtonElement>('.v2-activity__compose-picker-button');
expect(picker).not.toBeNull();
fireEvent.click(picker);
Expand Down Expand Up @@ -1377,7 +1421,7 @@ describe('V2ActivityPage', () => {
const activityRecap = { ...recap, pods: [...recap.pods, { id: 'pod-2', name: 'GTM Programs' }] };
mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? decisionQueue : activityRecap }));
renderPage();
await screen.findByRole('heading', { name: 'Tell your agents' });
await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') });
const picker = document.querySelector<HTMLButtonElement>('.v2-activity__compose-picker-button');
expect(picker).not.toBeNull();
fireEvent.click(picker);
Expand All @@ -1392,7 +1436,7 @@ describe('V2ActivityPage', () => {
const activityRecap = { ...recap, pods: [...recap.pods, { id: 'pod-2', name: 'GTM Programs' }] };
mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? decisionQueue : activityRecap }));
renderPage();
await screen.findByRole('heading', { name: 'Tell your agents' });
await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') });
const picker = document.querySelector<HTMLButtonElement>('.v2-activity__compose-picker-button');
expect(picker).not.toBeNull();
picker.focus();
Expand All @@ -1406,7 +1450,7 @@ describe('V2ActivityPage', () => {
const activityRecap = { ...recap, pods: [...recap.pods, { id: 'pod-2', name: 'GTM Programs' }] };
mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' ? decisionQueue : activityRecap }));
renderPage();
await screen.findByRole('heading', { name: 'Tell your agents' });
await screen.findByRole('textbox', { name: i18n.t('activity.compose.placeholder') });
const picker = document.querySelector<HTMLButtonElement>('.v2-activity__compose-picker-button');
expect(picker).not.toBeNull();
fireEvent.click(picker);
Expand Down
34 changes: 31 additions & 3 deletions frontend/src/v2/__tests__/V2YourTeamSignal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,13 @@ const agents = {

const renderPage = (over = {}) => {
const queue = over.queue ?? [{ id: 'd-1', kind: 'decision', title: 'Slack defaults', podId: 'p2', messageId: '900', actorUserId: 'wren-user' }];
const podList = over.pods ?? [{ _id: 'p1', name: 'Sharpen — pod model, attention routing, hardening' }, { _id: 'p2', name: 'Connectors v2 — channel routing' }];
const agentRows = over.agents ?? agents;
axios.get.mockImplementation((url) => {
if (url === '/api/pods') return Promise.resolve({ data: [{ _id: 'p1', name: 'Sharpen — pod model, attention routing, hardening' }, { _id: 'p2', name: 'Connectors v2 — channel routing' }] });
if (url.startsWith('/api/registry/pods/p1/agents')) return Promise.resolve({ data: { agents: over.p1Agents || agents.p1 } });
if (url.startsWith('/api/registry/pods/p2/agents')) return Promise.resolve({ data: { agents: agents.p2 } });
if (url === '/api/pods') return Promise.resolve({ data: podList });
if (url.startsWith('/api/registry/pods/p1/agents')) return Promise.resolve({ data: { agents: over.p1Agents || agentRows.p1 } });
if (url.startsWith('/api/registry/pods/p2/agents')) return Promise.resolve({ data: { agents: agentRows.p2 } });
if (url.startsWith('/api/registry/pods/p3/agents')) return Promise.resolve({ data: { agents: agentRows.p3 || [] } });
// `claimedBy` is the claimer's User id (never a name); `assignee` is a name
// written by PATCH and is not a claim.
if (url.startsWith('/api/v1/tasks/p1')) return Promise.resolve({ data: { tasks: [{ taskId: 'TASK-131', status: 'claimed', claimedBy: 'kai-user' }, { taskId: 'TASK-140', status: 'claimed', claimedBy: null, assignee: 'sage' }] } });
Expand Down Expand Up @@ -140,6 +143,31 @@ describe('Your Team (direction C)', () => {
expect(card.querySelector('.v2-team-card__desc')).toHaveTextContent('Fixes. Small PRs with a test each.');
});

test('hides template descriptions and folds extra pods into a +N chip', async () => {
renderPage({
pods: [
{ _id: 'p1', name: 'Sharpen — pod model, attention routing, hardening' },
{ _id: 'p2', name: 'Connectors v2 — channel routing' },
{ _id: 'p3', name: 'Growth — signup traces' },
],
agents: {
...agents,
p1: [...agents.p1, {
name: 'template-agent', instanceId: 'default', displayName: 'Template', userId: 'template-user',
lastActiveAt: minutesAgo(4), description: 'Your friendly AI assistant powered by Claude…',
}],
p3: [{ name: 'wren', instanceId: 'default', displayName: 'Wren', userId: 'wren-user', lastActiveAt: minutesAgo(30) }],
},
});
await waitFor(() => expect(screen.getByText('Template')).toBeInTheDocument());

expect(cardOf('Template').querySelector('.v2-team-card__desc')).toBeNull();
const wren = cardOf('Wren');
expect(wren.querySelectorAll('.v2-team-card__pod')).toHaveLength(3);
expect(wren.querySelector('.v2-team-card__pod--more')).toHaveTextContent('+1');
expect(wren.querySelector('.v2-team-card__pod--more')).toHaveAttribute('aria-label', '1 more pods');
});

test('idle: divider mark, muted `idle · 41m`, and NO description line when the listing has none — never a quote', async () => {
renderPage();
await waitFor(() => expect(screen.getByText('Sage')).toBeInTheDocument());
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/v2/__tests__/v2-layout-invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ describe('v2 layout invariants (CSS rule presence)', () => {
const mobileTabs = read('../components/V2MobileTabs.tsx');
const podBoard = read('../components/V2PodBoard.tsx');
const activityPage = read('../components/V2ActivityPage.tsx');
const yourTeamPage = read('../components/V2YourTeamPage.tsx');
const featurePage = read('../components/V2FeaturePage.tsx');
const v2App = read('../V2App.tsx');
const app = read('../../App.tsx');
Expand Down Expand Up @@ -259,6 +260,12 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(rule).not.toContain('white-space: nowrap');
});

test('Your Team keeps template copy out of cards and folds long pod lists', () => {
expect(yourTeamPage).toContain('isTemplateDescription(description, display)');
expect(yourTeamPage).toContain("(a.podNames || []).slice(0, 2)");
expect(yourTeamPage).toContain('v2-team-card__pod--more');
});

test('the workspace grid owns the measured 56 / 260 / fluid / 272 geometry', () => {
// Direction C (walk-1 ruling a, 2026-09-06): 14px page padding and three
// 14px gaps leave the 782px chat track the artboard measured.
Expand Down Expand Up @@ -564,6 +571,32 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(ruleBody(v2, '.v2-root .v2-activity__queue-actions button')).toContain('min-height: 32px');
});

test('Activity details clamp to two lines and reveal full copy on hover, focus, or expand', () => {
const detail = ruleBody(v2, '.v2-root .v2-activity__queue-detail');
expect(detail).toContain('display: -webkit-box');
expect(detail).toContain('-webkit-line-clamp: 2');
expect(detail).toContain('overflow: hidden');
const reveal = selectorRuleBody(v2, '.v2-root .v2-activity__queue-detail--expanded');
expect(reveal).toContain('display: block');
// Tab-in preview is kept; a reader's collapse is excluded so focus can't re-open it (Lumen HOLD on #2407).
const focusReveal = selectorRuleBody(v2, '.v2-root .v2-activity__queue-detail:focus-visible:not(.v2-activity__queue-detail--dismissed)');
expect(focusReveal).toContain('display: block');
expect(focusReveal).toContain('-webkit-line-clamp: unset');
expect(activityPage).toContain('v2-activity__queue-detail--dismissed');
expect(reveal).toContain('-webkit-line-clamp: unset');
const baseDetail = selectorRuleBody(v2, '.v2-root button.v2-activity__queue-detail');
expect(baseDetail).toContain('color: var(--v2-text-secondary)');
const mentionDetail = selectorRuleBody(v2, '.v2-root .v2-activity__queue-row--mention .v2-activity__queue-detail');
expect(mentionDetail).toContain('color: var(--v2-text-primary)');
expect(v2).toContain('.v2-root:lang(zh) .v2-activity__queue-detail,');
expect(v2).toContain('.v2-root .v2-activity__queue-detail:hover:not(:focus)');
expect(v2).toContain('@media (hover: hover)');
expect(activityPage).toContain('aria-expanded={expandedDetailIds.has(item.id)}');
expect(activityPage).toContain('v2-activity__queue-detail');
expect(activityPage).toContain('aria-label={t(\'activity.compose.placeholder\')}');
expect(activityPage).not.toContain('v2-activity__compose-label');
});

test('Activity pagination keeps the Show more affordance visible and keyboard-sized', () => {
const more = ruleBody(v2, '.v2-activity__queue-more');
expect(more).toContain('min-height: 36px');
Expand Down Expand Up @@ -1196,6 +1229,22 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(chip).toContain('font-size: 12px');
});

test('UI-STRUCT B3 uses ink for primary actions and mono counts without pills', () => {
const boardNew = ruleBody(v2, '.v2-root button.v2-board__new');
expect(boardNew).toContain('background: var(--v2-ink)');
expect(boardNew).toContain('color: var(--v2-on-ink)');

const columnCount = ruleBody(v2, '.v2-board__col-count');
expect(columnCount).toContain('var(--v2-font-mono)');
expect(columnCount).not.toContain('background:');
expect(columnCount).not.toContain('border-radius:');
expect(columnCount).not.toContain('padding:');

const connectCta = lastRuleBody(v2, '.v2-root .v2-connect__cta');
expect(connectCta).toContain('background: var(--v2-ink)');
expect(connectCta).toContain('color: var(--v2-on-ink)');
});

test('the board craft-pass buttons use icon components instead of unicode glyphs', () => {
// This is structural: accessible labels cannot prove a literal glyph has
// left the JSX. The MUI imports and icon nodes are the implementation
Expand Down
Loading
Loading