From 252840c7a9128d5f6e88d28f07d059095ea7016b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:28:44 -0700 Subject: [PATCH 1/5] fix(v3): BYO unchosen slot reads as a hint; phone pod line keeps board link (TASK-647) - BYO: the dashed "Choose where it runs first" slot becomes a muted hint line with no border, so it no longer reads as a dead button. - Pod header at phone width: the compact line carries the board N open link (44px tap area via pseudo-element) and "N members", as the desktop meta does. compactMeta locale keys are removed; nothing else reads them. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: wisp Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/i18n/__tests__/i18n.test.ts | 20 ++++--------------- frontend/src/i18n/locales/en.json | 4 +--- frontend/src/i18n/locales/zh-CN.json | 4 +--- frontend/src/v2/__tests__/V2Composer.test.tsx | 3 +-- .../v2/__tests__/v2-layout-invariants.test.ts | 3 +++ frontend/src/v2/components/V2AgentBYO.tsx | 5 +++-- frontend/src/v2/components/V2Thread.tsx | 18 +++++++++++------ frontend/src/v2/v2.css | 20 ++++++++++++++----- 8 files changed, 40 insertions(+), 37 deletions(-) diff --git a/frontend/src/i18n/__tests__/i18n.test.ts b/frontend/src/i18n/__tests__/i18n.test.ts index 567da518ba..323dd697d5 100644 --- a/frontend/src/i18n/__tests__/i18n.test.ts +++ b/frontend/src/i18n/__tests__/i18n.test.ts @@ -39,22 +39,10 @@ describe('i18n configuration', () => { expect(i18n.t(key, { lng: 'en' })).toBe(expected); }); - it.each([ - [0, '1 · 0 agents', '1 member, 0 agents'], - [1, '1 · 1 agent', '1 member, 1 agent'], - [2, '1 · 2 agents', '1 member, 2 agents'], - ])('pluralizes the compact pod header at %i agents', (count, visible, accessible) => { - const members = i18n.t('podChat.header.members', { lng: 'en', count: 1 }); - const agents = i18n.t('podChat.header.agents', { lng: 'en', count }); - expect(i18n.t('podChat.header.compactMeta', { lng: 'en', members: 1, agents })).toBe(visible); - expect(i18n.t('podChat.header.compactMetaAccessible', { lng: 'en', members, agents })).toBe(accessible); - }); - - it('keeps the Chinese compact count while naming it for assistive tech', () => { - const members = i18n.t('podChat.header.members', { lng: 'zh-CN', count: 1 }); - const agents = i18n.t('podChat.header.agents', { lng: 'zh-CN', count: 1 }); - expect(i18n.t('podChat.header.compactMeta', { lng: 'zh-CN', members: 1, agents })).toBe('1 · 1个智能体'); - expect(i18n.t('podChat.header.compactMetaAccessible', { lng: 'zh-CN', members, agents })).toBe('1名成员,1个智能体'); + it('pluralizes the member count the compact pod header shows', () => { + expect(i18n.t('podChat.header.members', { lng: 'en', count: 1 })).toBe('1 member'); + expect(i18n.t('podChat.header.members', { lng: 'en', count: 2 })).toBe('2 members'); + expect(i18n.t('podChat.header.members', { lng: 'zh-CN', count: 1 })).toBe('1名成员'); }); it('uses the singular agent count in the Your Team header', () => { diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 3dbd8b52a5..7a21f26eaa 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -648,9 +648,7 @@ "agents_one": "{{count}} agent", "agents_other": "{{count}} agents", "boardOpen": "board {{count}} open", - "backToPods": "Back to pods", - "compactMeta": "{{members}} · {{agents}}", - "compactMetaAccessible": "{{members}}, {{agents}}" + "backToPods": "Back to pods" }, "team": { "view": "View pod team", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 4755445bb2..930ec558fa 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -647,9 +647,7 @@ "agents_one": "{{count}}个智能体", "agents_other": "{{count}}个智能体", "boardOpen": "看板{{count}}项待办", - "backToPods": "返回 Pod 列表", - "compactMeta": "{{members}} · {{agents}}", - "compactMetaAccessible": "{{members}},{{agents}}" + "backToPods": "返回 Pod 列表" }, "team": { "view": "查看 Pod 团队", diff --git a/frontend/src/v2/__tests__/V2Composer.test.tsx b/frontend/src/v2/__tests__/V2Composer.test.tsx index d728f06ad2..74220a3e28 100644 --- a/frontend/src/v2/__tests__/V2Composer.test.tsx +++ b/frontend/src/v2/__tests__/V2Composer.test.tsx @@ -99,8 +99,7 @@ describe('V2Composer send button', () => { const meta = header.querySelector('.v2-pod-header__meta:not(.v2-pod-header__meta--compact)'); expect(meta).toHaveTextContent('2 members'); expect(meta).toHaveTextContent('1 agent'); - expect(header.querySelector('.v2-pod-header__meta--compact')).toHaveTextContent('2 · 1 agent'); - expect(view.getByRole('group', { name: '2 members, 1 agent' })).toBeInTheDocument(); + expect(header.querySelector('.v2-pod-header__meta--compact')).toHaveTextContent('2 members'); expect(view.container.querySelector('.v2-thread__working')).toBeNull(); }); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 48bb48d98e..3cebe19321 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -1554,6 +1554,9 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(compactMeta).toBeGreaterThan(-1); expect(ruleBody(v2, '.v2-pod-header__meta')).toContain('display: flex'); expect(compactMeta).toBeGreaterThan(baseMeta); + // TASK-647 item 3: the phone line keeps the board link, with a 44px hit + // area drawn as a pseudo-element so the 28px meta row does not grow. + expect(ruleBody(v2, 'button.v2-pod-header__board--compact::before')).toContain('inset: -8px -10px'); expect(v2).not.toContain('.v2-thread__working'); }); diff --git a/frontend/src/v2/components/V2AgentBYO.tsx b/frontend/src/v2/components/V2AgentBYO.tsx index 0375afc34d..d7571f97ab 100644 --- a/frontend/src/v2/components/V2AgentBYO.tsx +++ b/frontend/src/v2/components/V2AgentBYO.tsx @@ -1326,8 +1326,9 @@ const V2AgentBYO: React.FC = () => { )} {nameLookupError && mode === 'byo' &&
{t('agentByo.errors.nameLookupFailed')}
} {mode === null ? ( - // Signal §3 not yet: a dashed slot where the act will be, until a - // place to run it is chosen. + // Signal §3 not yet: a muted line where the act will be, until a + // place to run it is chosen. Plain text, not a box, so it can't + // read as a dead button (UX Lead, TASK-647 item 1).
{t('agentByo.actions.chooseFirst')}
diff --git a/frontend/src/v2/components/V2Thread.tsx b/frontend/src/v2/components/V2Thread.tsx index e095a8e636..d49cfa51c2 100644 --- a/frontend/src/v2/components/V2Thread.tsx +++ b/frontend/src/v2/components/V2Thread.tsx @@ -1802,12 +1802,18 @@ const V2Thread: React.FC = ({ detail, firstRunVisible = false, in {pod.description && {pod.description}} - - + {/* The phone line carries the board link and the member count, the + same two facts the desktop meta leads with (TASK-647 item 3). */} + + {headerMeta.boardOpen !== null && ( + <> + + + + )} + {memberCountText} {metaParts.map((part, index) => ( diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index cdcc9b7a51..de8995210d 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -6536,14 +6536,15 @@ body.modern-ui.v2-canvas { color: var(--v2-text-primary); } /* Signal §3 not yet: where the act will be, until a place to run it is - chosen. Sized like the submit button it stands in for. */ + chosen. A muted hint line, not a box: a dashed slot read as a dead button + (TASK-647 item 1). */ .v2-byo__unchosen { align-self: flex-start; - padding: 10px 18px; - border: 1px dashed var(--v2-border-strong); - border-radius: var(--v2-radius); + padding: 0; + border: 0; color: var(--v2-text-muted); - font-size: 14px; + font-size: 13px; + line-height: 20px; } .v2-root button.v2-byo__link { /* A text action beside or under a line of copy ("Change", "Other ways to @@ -12074,6 +12075,15 @@ body.modern-ui.v2-canvas { .v2-root button.v2-pod-header__board:hover { text-decoration: underline; } +/* Phone: the board link keeps a 44px tap target without growing the line: + the hit area is a pseudo-element that extends past the 28px meta row. */ +.v2-root button.v2-pod-header__board--compact { position: relative; } +.v2-root button.v2-pod-header__board--compact::before { + content: ''; + position: absolute; + inset: -8px -10px; +} + .v2-root button.v2-thread__back { display: none; } @media (max-width: 760px) { From 30de5e5871a711a4dc2ff4e2d8dccae34eb5b118 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:36:23 -0700 Subject: [PATCH 2/5] fix(v3): phone pod header keeps one line and a 44px board tap area (TASK-647) UX gate on 705332fe: the board link's tap area was 32px, and the "i" hint wrapped under the name, growing My Workspace from 51px to 79px at 390. - The name takes the free width with a zero basis and truncates, so the hint stays on its line. The title-line wrap rule is unchanged. - The compact meta does not shrink or wrap. - The board link's hit area is 44px tall (16px line + 14px each side). Co-Authored-By: Claude Haiku 5.5 Co-authored-by: wisp Agent-Model: claude/claude-haiku-5-5/default --- .../src/v2/__tests__/v2-layout-invariants.test.ts | 2 +- frontend/src/v2/v2.css | 12 ++++++++---- 2 files changed, 9 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 3cebe19321..920d4dba55 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -1556,7 +1556,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(compactMeta).toBeGreaterThan(baseMeta); // TASK-647 item 3: the phone line keeps the board link, with a 44px hit // area drawn as a pseudo-element so the 28px meta row does not grow. - expect(ruleBody(v2, 'button.v2-pod-header__board--compact::before')).toContain('inset: -8px -10px'); + expect(ruleBody(v2, 'button.v2-pod-header__board--compact::before')).toContain('inset: -14px -10px'); expect(v2).not.toContain('.v2-thread__working'); }); diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index de8995210d..1d7c8f3529 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -11601,7 +11601,10 @@ body.modern-ui.v2-canvas { .v2-thread__header-row { align-items: center; gap: 8px; } .v2-thread__title-line { display: flex; flex-wrap: wrap; align-items: center; } - .v2-thread__title h1 { font: 600 15px/20px var(--v2-font); } + /* The name takes the free width with a zero basis and truncates, so the + "i" hint keeps its place on the line instead of wrapping under it and + growing the header (TASK-647 item 3). */ + .v2-thread__title h1 { font: 600 15px/20px var(--v2-font); flex: 1 1 0; min-width: 0; white-space: nowrap; text-overflow: ellipsis; } .v2-thread .v2-msg__author, .v2-thread .v2-msg__author-btn { font-size: 15px; } @@ -12076,12 +12079,13 @@ body.modern-ui.v2-canvas { .v2-root button.v2-pod-header__board:hover { text-decoration: underline; } /* Phone: the board link keeps a 44px tap target without growing the line: - the hit area is a pseudo-element that extends past the 28px meta row. */ + the hit area is a pseudo-element (16px text line + 14px each side = 44px + tall, 10px wider each side) that extends past the 28px meta row. */ .v2-root button.v2-pod-header__board--compact { position: relative; } .v2-root button.v2-pod-header__board--compact::before { content: ''; position: absolute; - inset: -8px -10px; + inset: -14px -10px; } .v2-root button.v2-thread__back { display: none; } @@ -12144,7 +12148,7 @@ body.modern-ui.v2-canvas { align-items: flex-start; } .v2-pod-header__meta { display: none; } - .v2-pod-header__meta.v2-pod-header__meta--compact { display: flex; min-height: 28px; } + .v2-pod-header__meta.v2-pod-header__meta--compact { display: flex; min-height: 28px; flex: 0 0 auto; white-space: nowrap; } .v2-root button.v2-thread__back { display: inline-grid; From 7aa8561c2fba364e85328eeceb6f957dcc08b9dd Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:37:03 -0700 Subject: [PATCH 3/5] test(v3): pin the compact header meta rule in the copy budget (TASK-647) Co-Authored-By: Claude Haiku 5.5 Co-authored-by: wisp Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/v2/__tests__/v2-copy-budget.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/v2/__tests__/v2-copy-budget.test.tsx b/frontend/src/v2/__tests__/v2-copy-budget.test.tsx index 0882d042a7..33d0f12f25 100644 --- a/frontend/src/v2/__tests__/v2-copy-budget.test.tsx +++ b/frontend/src/v2/__tests__/v2-copy-budget.test.tsx @@ -50,7 +50,7 @@ describe('page head copy budget', () => { expect(css).toContain('.v2-thread__title-line { display: flex; flex-wrap: wrap; align-items: center; }'); expect(css).toContain('.v2-thread__title-line .v2-hint__content { width: 100%; flex-basis: 100%; order: 1; }'); expect(css).toMatch(/\.v2-pod-header \.v2-thread__header-row \{[^}]*padding: 11px 0;[^}]*align-items: flex-start;/); - expect(css).toContain('.v2-pod-header__meta.v2-pod-header__meta--compact { display: flex; min-height: 28px; }'); + expect(css).toContain('.v2-pod-header__meta.v2-pod-header__meta--compact { display: flex; min-height: 28px; flex: 0 0 auto; white-space: nowrap; }'); expect(css).toContain('box-shadow: var(--v2-focus-ring);'); }); }); From 5655da4f5576fca391855d0940b64714e5f03c01 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:39:51 -0700 Subject: [PATCH 4/5] fix(v3): phone header rules and My Workspace primary start (TASK-647) Item 3, phone header at 760 and under (UX gate on bf0e02fd): - the board link's 44px hit area moves into the phone media block (inset -14px -10px), and the name gets max-width calc(100% - 36px); - the compact meta gap is 4px. The compact line shows the member count only, so the agent count stays in the desktop meta. Item 4, My Workspace: - the run-hosted start (keyed on WELCOME_HOSTED_ACTION, not position) is the filled ink primary with an ink hover; - the first-run "Open connection setup" link is bordered, so the card keeps one ink action. Tests: the layout invariants pin the new rules, and the thread starter test checks the compact member count instead of the removed aria group. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: wisp Agent-Model: claude/claude-haiku-5-5/default --- .../src/v2/__tests__/V2ThreadStarter.test.tsx | 9 ++-- .../v2/__tests__/v2-layout-invariants.test.ts | 3 +- .../src/v2/components/V2ThreadMessages.tsx | 2 +- frontend/src/v2/v2.css | 45 ++++++++++++++----- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/frontend/src/v2/__tests__/V2ThreadStarter.test.tsx b/frontend/src/v2/__tests__/V2ThreadStarter.test.tsx index 3042133474..43e6c69926 100644 --- a/frontend/src/v2/__tests__/V2ThreadStarter.test.tsx +++ b/frontend/src/v2/__tests__/V2ThreadStarter.test.tsx @@ -117,15 +117,16 @@ describe('V2Thread teaching empty states', () => { expect(document.querySelector('.v2-teammate')).toBeNull(); }); - test('compact header shows singular agents and names both counts for assistive tech', () => { + test('compact header shows the member count; the agent count stays in the desktop meta', () => { const detail = makeDetail({ agents: [{ agentName: 'openclaw', instanceId: 'aria', displayName: 'Aria' }], }); renderChat(detail); - const compact = screen.getByRole('group', { name: '1 member, 1 agent' }); - expect(compact).toHaveTextContent('1 · 1 agent'); - expect(compact.querySelector('[aria-hidden="true"]')).toHaveTextContent('1 · 1 agent'); + const compact = document.querySelector('.v2-pod-header__meta--compact'); + expect(compact).toHaveTextContent('1 member'); + expect(compact).not.toHaveTextContent('agent'); + expect(document.querySelector('.v2-pod-header__meta:not(.v2-pod-header__meta--compact)')).toHaveTextContent('1 agent'); }); test('multi-member regular pods use the same teaching state', () => { diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 920d4dba55..327a8c375f 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -1557,6 +1557,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { // TASK-647 item 3: the phone line keeps the board link, with a 44px hit // area drawn as a pseudo-element so the 28px meta row does not grow. expect(ruleBody(v2, 'button.v2-pod-header__board--compact::before')).toContain('inset: -14px -10px'); + expect(ruleBody(v2, '.v2-thread__title-line > h1')).toContain('max-width: calc(100% - 36px)'); expect(v2).not.toContain('.v2-thread__working'); }); @@ -2887,7 +2888,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { // row under Scout's message, in the slot the delivery cue and the // no-agent row share, so they land on the message text column too. expect(threadMessages).toContain('className="v2-chat__starts"'); - expect(threadMessages).toContain('className="v2-chat__start"'); + expect(threadMessages).toContain('v2-chat__start${start.action === WELCOME_HOSTED_ACTION ? \' v2-chat__start--primary\' : \'\'}'); // The 44px phone minimum must come AFTER the 32px base in the sheet: at // equal specificity the later rule wins, and a base rule placed after // the media block would silently render 32 on a phone. diff --git a/frontend/src/v2/components/V2ThreadMessages.tsx b/frontend/src/v2/components/V2ThreadMessages.tsx index 31e8d54f8e..7032c7283f 100644 --- a/frontend/src/v2/components/V2ThreadMessages.tsx +++ b/frontend/src/v2/components/V2ThreadMessages.tsx @@ -307,7 +307,7 @@ const V2ThreadMessages: React.FC = ({