From 10b064489f4df6131b6b3eb83d4f4f39263437f4 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:01:36 -0700 Subject: [PATCH 1/3] feat(board): card line is the lease timer and claimant avatar, no assignee text (TASK-743) Co-Authored-By: Claude Sonnet 5.5 Co-authored-by: otto Agent-Model: claude/claude-opus-5-5/default --- frontend/src/i18n/locales/en.json | 3 +++ frontend/src/i18n/locales/zh-CN.json | 3 +++ frontend/src/v2/__tests__/V2PodBoard.test.tsx | 19 +++++++++------ frontend/src/v2/components/V2PodBoard.tsx | 23 ++++++++++++------- frontend/src/v2/v2.css | 1 + 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index b825c537c..139402426 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1606,6 +1606,9 @@ }, "owner": { "left": "{{holder}} · {{minutes}}m left", + "leaseShort": "{{minutes}}m", + "expiredShort": "expired", + "claimedShort": "claimed", "assigned": "{{assignee}} · not claimed", "expired": "lease expired · {{holder}}", "claimed": "{{holder}} · claimed" diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 047e6c131..92ca569a8 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1598,6 +1598,9 @@ }, "owner": { "left": "{{holder}} · 剩余{{minutes}}分钟", + "leaseShort": "{{minutes}}分钟", + "expiredShort": "已到期", + "claimedShort": "已认领", "assigned": "{{assignee}} · 尚未认领", "expired": "认领已到期 · {{holder}}", "claimed": "{{holder}} · 已认领" diff --git a/frontend/src/v2/__tests__/V2PodBoard.test.tsx b/frontend/src/v2/__tests__/V2PodBoard.test.tsx index 7902e1aa5..c076e36fe 100644 --- a/frontend/src/v2/__tests__/V2PodBoard.test.tsx +++ b/frontend/src/v2/__tests__/V2PodBoard.test.tsx @@ -159,9 +159,10 @@ describe('V2PodBoard', () => { claimedBy: 'kai-id', claimExpiresAt: expiresAt, updates: [{ text: 'Lease renewed', author: 'Kai', authorId: 'kai-id' }], }; + const assignedOnly = { taskId: 'TASK-271', title: 'Assigned only', status: 'pending', assignee: 'lily-shen', updates: [] }; jest.useFakeTimers({ now }); axios.get.mockImplementation((url) => { - if (url.startsWith('/api/v1/tasks/')) return Promise.resolve({ data: { tasks: [task, holderFromUpdate] } }); + if (url.startsWith('/api/v1/tasks/')) return Promise.resolve({ data: { tasks: [task, holderFromUpdate, assignedOnly] } }); if (url.endsWith('/focus')) return Promise.resolve({ data: FOCUS }); if (url.startsWith('/api/pods/')) return Promise.resolve({ data: { name: 'My Workspace', members: [{ _id: 'forge-id', username: 'forge', displayName: 'Forge' }], @@ -173,15 +174,19 @@ describe('V2PodBoard', () => { renderBoard(); await act(async () => { await Promise.resolve(); }); expect(new Intl.DateTimeFormat().resolvedOptions().timeZone).toBe('America/Los_Angeles'); - expect(screen.getByTitle('TASK-269')).toHaveTextContent('Forge · 1m left'); - expect(screen.getByTitle('TASK-270')).toHaveTextContent('Kai · 1m left'); + // The card line is the timer and the claimant's avatar (named by its title); no holder text. + expect(screen.getByTitle('TASK-269')).toHaveTextContent('1m'); + expect(screen.getByTitle('TASK-269')).not.toHaveTextContent('Forge'); + expect(screen.getByTitle('TASK-270')).toHaveTextContent('1m'); + expect(screen.getByTitle('Forge')).toBeInTheDocument(); + // An assigned task nobody has claimed shows no owner line at all. + expect(screen.getByTitle('TASK-271')).not.toHaveTextContent(/lily-shen|not claimed/); act(() => { jest.advanceTimersByTime(59_999); }); - expect(screen.getByTitle('TASK-269')).toHaveTextContent('Forge · 1m left'); + expect(screen.getByTitle('TASK-269')).toHaveTextContent('1m'); act(() => { jest.advanceTimersByTime(1); }); - expect(screen.queryByText(/1m left/)).not.toBeInTheDocument(); - expect(screen.getByText('lease expired · Forge')).toBeInTheDocument(); - expect(screen.getByText('lease expired · Kai')).toBeInTheDocument(); + expect(screen.getByTitle('TASK-269')).toHaveTextContent('expired'); + expect(screen.getByTitle('TASK-270')).toHaveTextContent('expired'); expect(axios.get.mock.calls.filter(([url]) => url.startsWith('/api/v1/tasks/'))).toHaveLength(1); } finally { jest.useRealTimers(); diff --git a/frontend/src/v2/components/V2PodBoard.tsx b/frontend/src/v2/components/V2PodBoard.tsx index 03230e452..73c0ad404 100644 --- a/frontend/src/v2/components/V2PodBoard.tsx +++ b/frontend/src/v2/components/V2PodBoard.tsx @@ -267,17 +267,24 @@ const V2PodBoard: React.FC = () => { const timeLabel = (expiry: number) => new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit', hourCycle: 'h23', }).format(new Date(expiry)); + // The frame's card line is the lease timer and the claimant's avatar, nothing else: + // an assigned-but-unclaimed task shows no owner text. const ownerLine = (task: BoardTask) => { if (['done', 'completed', 'complete'].includes(task.status)) return null; const lease = leaseFor(task); - if (!lease.claimed && !task.assignee) return null; - const state = lease.expired ? 'expired' : lease.claimed ? 'live' : 'not-claimed'; - const text = lease.expired ? t('board.owner.expired', { holder: claimHolderName(task) }) - : lease.claimed ? (lease.knownExpiry - ? }} /> - : t('board.owner.claimed', { holder: claimHolderName(task) })) - : t('board.owner.assigned', { assignee: task.assignee }); - return ; + if (!lease.claimed) return null; + const holder = claimHolderName(task); + const member = podMembers.find((item) => memberId(item) === task.claimedBy || item.username === task.claimedBy); + const state = lease.expired ? 'expired' : 'live'; + return ( + + + ); }; const memberAvatar = (member: PodMember, name: string) => ( diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 4fa910a6b..169f6583f 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -13860,6 +13860,7 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor .v2-board__watcher-list { display: flex; flex-wrap: wrap; gap: 6px; } .v2-board__watcher { display: inline-flex; align-items: center; gap: 4px; min-height: 28px; max-width: 100%; padding: 0 8px 0 4px; border: 1px solid var(--v2-border-soft); border-radius: 4px; font-size: 12px; overflow-wrap: anywhere; } .v2-board .v2-board__member-avatar { width: 20px; height: 20px; min-width: 20px; border-radius: 4px; } +.v2-board .v2-board__card-avatar { width: 20px; height: 20px; min-width: 20px; border-radius: 50%; } .v2-board__watcher-hint, .v2-board__field small { margin: 8px 0 0; font-size: 12px; line-height: 16px; color: var(--v2-text-muted); } .v2-board__create-row { display: grid; grid-template-columns: minmax(0, 1fr) 140px; gap: 12px; } .v2-board__field input, .v2-board__field select { min-height: 40px; border-radius: 4px; } From 0579f082930a7686474e41eb37248345808a7ab0 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:02:46 -0700 Subject: [PATCH 2/3] fix(board): card line height, frame avatar radius, username claimant, drop dead keys (TASK-743) Co-Authored-By: Claude Sonnet 5.5 Co-authored-by: otto Agent-Model: claude/claude-opus-5-5/default --- frontend/src/i18n/locales/en.json | 6 +----- frontend/src/i18n/locales/zh-CN.json | 6 +----- frontend/src/v2/__tests__/v2-layout-invariants.test.ts | 3 +-- frontend/src/v2/components/V2PodBoard.tsx | 2 +- frontend/src/v2/v2.css | 5 +++-- 5 files changed, 7 insertions(+), 15 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 139402426..895e3715e 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -1605,13 +1605,9 @@ "defaultPriority": "default" }, "owner": { - "left": "{{holder}} · {{minutes}}m left", "leaseShort": "{{minutes}}m", "expiredShort": "expired", - "claimedShort": "claimed", - "assigned": "{{assignee}} · not claimed", - "expired": "lease expired · {{holder}}", - "claimed": "{{holder}} · claimed" + "claimedShort": "claimed" }, "filter": { "label": "Filters", diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 92ca569a8..ccaf9cc50 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -1597,13 +1597,9 @@ "defaultPriority": "默认" }, "owner": { - "left": "{{holder}} · 剩余{{minutes}}分钟", "leaseShort": "{{minutes}}分钟", "expiredShort": "已到期", - "claimedShort": "已认领", - "assigned": "{{assignee}} · 尚未认领", - "expired": "认领已到期 · {{holder}}", - "claimed": "{{holder}} · 已认领" + "claimedShort": "已认领" }, "filter": { "label": "筛选", diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 240664c74..ec56883a3 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5139,9 +5139,8 @@ describe('task priority and lease signals (TASK-443)', () => { expect(ruleBody(css, '.v2-board__card--blocked')).toContain('box-shadow: inset 3px 0 0 var(--v2-status-blocked-edge)'); expect(ruleBody(css, '.v2-board__col--blocked .v2-board__col-name')).toContain('color: var(--v2-status-blocked)'); }); - test('live, unclaimed and expired ownership retain distinct dots', () => { + test('live and expired ownership retain distinct dots', () => { expect(ruleBody(css, '.v2-board__owner-dot--live')).toContain('background: var(--v2-status-live)'); - expect(ruleBody(css, '.v2-board__owner-dot--not-claimed')).toContain('border: 1px dashed var(--v2-status-pending)'); const expired = ruleBody(css, '.v2-board__owner-dot--expired'); expect(expired).toContain('border: 1px solid var(--v2-status-idle)'); expect(expired).toContain('background: none'); diff --git a/frontend/src/v2/components/V2PodBoard.tsx b/frontend/src/v2/components/V2PodBoard.tsx index 73c0ad404..646034b6d 100644 --- a/frontend/src/v2/components/V2PodBoard.tsx +++ b/frontend/src/v2/components/V2PodBoard.tsx @@ -250,7 +250,7 @@ const V2PodBoard: React.FC = () => { }, [api, podId, detailSaving, t]); const claimHolderName = useCallback((task: BoardTask): string => { - const member = podMembers.find((item) => memberId(item) === task.claimedBy); + const member = podMembers.find((item) => memberId(item) === task.claimedBy || item.username === task.claimedBy); if (member) return memberLabel(member); // The claim route records the claimant's User id and resolved author on // the update. Match identity rather than the wording of the update text. diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 169f6583f..a7e6e809f 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -13822,7 +13822,6 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor .v2-board__owner-line { display: flex; align-items: center; gap: 6px; font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-secondary); } .v2-board__owner-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; } .v2-board__owner-dot--live { background: var(--v2-status-live); } -.v2-board__owner-dot--not-claimed { border: 1px dashed var(--v2-status-pending); } .v2-board__owner-dot--expired { border: 1px solid var(--v2-status-idle); background: none; } .v2-board__card--done { border-color: var(--v2-border-soft); } .v2-board__card--done .v2-board__card-title { color: var(--v2-text-secondary); } @@ -13860,7 +13859,7 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor .v2-board__watcher-list { display: flex; flex-wrap: wrap; gap: 6px; } .v2-board__watcher { display: inline-flex; align-items: center; gap: 4px; min-height: 28px; max-width: 100%; padding: 0 8px 0 4px; border: 1px solid var(--v2-border-soft); border-radius: 4px; font-size: 12px; overflow-wrap: anywhere; } .v2-board .v2-board__member-avatar { width: 20px; height: 20px; min-width: 20px; border-radius: 4px; } -.v2-board .v2-board__card-avatar { width: 20px; height: 20px; min-width: 20px; border-radius: 50%; } +.v2-board .v2-board__card-avatar { width: 20px; height: 20px; min-width: 20px; border-radius: 6px; } .v2-board__watcher-hint, .v2-board__field small { margin: 8px 0 0; font-size: 12px; line-height: 16px; color: var(--v2-text-muted); } .v2-board__create-row { display: grid; grid-template-columns: minmax(0, 1fr) 140px; gap: 12px; } .v2-board__field input, .v2-board__field select { min-height: 40px; border-radius: 4px; } @@ -13984,6 +13983,8 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor .v2-board__card-main { display: grid; gap: 4px; padding: 0; border: 0; background: none; text-align: left; cursor: pointer; min-width: 0; color: inherit; font: inherit; } .v2-board__card-kicker > :not(.v2-board__owner-line):not(.v2-board__card-spacer) { flex: none; } .v2-board__card-kicker { display: flex; align-items: center; gap: 6px; min-width: 0; font: 500 11px/16px var(--v2-font-mono); } +/* The frame fixes the id row at 16px; its 20px avatar overflows it instead of growing the card. */ +.v2-board__card-kicker, .v2-board__card-kicker .v2-board__owner-line { height: 16px; } .v2-board__card-spacer { flex: 1 1 auto; } .v2-board__card-kicker .v2-board__owner-line { min-width: 0; flex: 0 1 auto; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-tertiary); } .v2-board__card-title { From 925f1976100cedf94c2ff2cb2154371ee2c5b20b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:03:25 -0700 Subject: [PATCH 3/3] fix(board): don't clip the card avatar, drop unused Trans import (TASK-743) Co-Authored-By: Claude Sonnet 5.5 Co-authored-by: otto Agent-Model: claude/claude-opus-5-5/default --- frontend/src/v2/components/V2PodBoard.tsx | 2 +- frontend/src/v2/v2.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/v2/components/V2PodBoard.tsx b/frontend/src/v2/components/V2PodBoard.tsx index 646034b6d..b2f675cb0 100644 --- a/frontend/src/v2/components/V2PodBoard.tsx +++ b/frontend/src/v2/components/V2PodBoard.tsx @@ -14,7 +14,7 @@ // behaviorally interchangeable while v1 winds down. import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { Trans, useTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import AddIcon from '@mui/icons-material/Add'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import SearchIcon from '@mui/icons-material/Search'; diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index a7e6e809f..012e44a1a 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -13986,7 +13986,7 @@ details[open] > .v2-settings__summary > .v2-settings__icon:last-child { transfor /* The frame fixes the id row at 16px; its 20px avatar overflows it instead of growing the card. */ .v2-board__card-kicker, .v2-board__card-kicker .v2-board__owner-line { height: 16px; } .v2-board__card-spacer { flex: 1 1 auto; } -.v2-board__card-kicker .v2-board__owner-line { min-width: 0; flex: 0 1 auto; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-tertiary); } +.v2-board__card-kicker .v2-board__owner-line { min-width: 0; flex: 0 1 auto; overflow: visible; white-space: nowrap; font: 500 11px/16px var(--v2-font-mono); color: var(--v2-text-tertiary); } .v2-board__card-title { font: 500 13px/18px var(--v2-font); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;