From 07ea4158f247e3c4c15da8b5f3f243bdb625120f Mon Sep 17 00:00:00 2001 From: Xiao Liu Date: Sun, 30 Aug 2026 05:36:26 +0800 Subject: [PATCH 1/3] test(desktop): story-cover task panel terminal and deep-nesting states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eighth surface under #3944 (one surface per PR): extend the 任务 panel (TaskLedgerPanel, via Product/Session Workbar) with the states the existing Tasks / TasksEmpty / TasksLoadFailed stories never reach. - TasksRecentlyFinished — the 最近结束 section with a failed task (+ reason) and a cancelled task, plus four finished tasks so the three-item cap drops the oldest. - TasksDeepNesting — an eight-level subtask chain; the play asserts the deepest row renders (reachability). The indent clamp (`--task-depth`) is a computed-style contract left to focused tests (review feedback). pending / in_progress / blocked / completed, owners, and shallow nesting were already covered by Tasks; failed, cancelled, the recent-finished cap, and deep nesting were not. stale / responding / unread / project-grouping have no UI in this panel and are left out. Refs #3944, #3893 Generated-by: Claude Code --- .../stories/session-workbar.stories.tsx | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx index 2efa3082ea..d401c1c848 100644 --- a/apps/desktop/stories/session-workbar.stories.tsx +++ b/apps/desktop/stories/session-workbar.stories.tsx @@ -175,6 +175,20 @@ const tasks: Task[] = [ }), ]; +// An eight-level parent→child chain, all still active, to exercise the task +// panel's depth clamp (`Math.min(depth, 6)`): a level-8 row indents no further +// than level 7. +const deepTaskChain: Task[] = Array.from({ length: 8 }, (_, index) => { + const level = index + 1; + return task({ + id: `td-${level}`, + key: `T${Array.from({ length: level }, () => '1').join('.')}`, + subject: `第 ${level} 层子任务,验证深层缩进仍可完整显示`, + parentId: level === 1 ? undefined : `td-${level - 1}`, + status: level === 8 ? 'in_progress' : 'pending', + }); +}); + const artifacts: ArtifactRecord[] = [ { id: 'artifact-patch', @@ -889,6 +903,99 @@ export const TasksLoadFailed: Story = { render: () => , }; +// Real path: 任务工作栏 → 任务 after a run finished with mixed outcomes. The +// 最近结束 section (collapsed by default) holds the terminal tasks — a failed +// one with its reason and a cancelled one — and caps at three, so a fourth +// finished task is dropped rather than growing the list. +export const TasksRecentlyFinished: Story = { + decorators: [ + bridge({ + tasks: [ + task({ + id: 'trf-active', + key: 'T1', + subject: '巡检发布前检查项', + status: 'in_progress', + owner: { actor: 'main_agent', runId: 'run-trf' }, + }), + task({ + id: 'trf-old', + key: 'T2', + subject: '归档上一轮实验数据', + status: 'completed', + completionEvidence: '已归档到对象存储。', + endedAt: NOW - 300_000, + updatedAt: NOW - 300_000, + }), + task({ + id: 'trf-notes', + key: 'T3', + subject: '生成变更说明', + status: 'completed', + endedAt: NOW - 120_000, + updatedAt: NOW - 120_000, + }), + task({ + id: 'trf-smoke', + key: 'T4', + subject: '回归冒烟用例', + status: 'failed', + failureReason: '两个用例在 CI 上超时,任务标记为失败。', + endedAt: NOW - 90_000, + updatedAt: NOW - 90_000, + }), + task({ + id: 'trf-dupe', + key: 'T5', + subject: '取消重复的部署任务', + status: 'cancelled', + endedAt: NOW - 30_000, + updatedAt: NOW - 30_000, + }), + ], + }), + ], + render: () => , + play: async ({ canvasElement }) => { + // Four terminal tasks, but the recent section freezes its count at three. + const trigger = await waitFor(() => { + const el = canvasElement.querySelector('.maka-task-ledger-terminal-trigger'); + if (!el) throw new Error('recent-finished trigger not rendered yet'); + return el; + }); + await expect(trigger.textContent).toContain('3'); + await userEvent.click(trigger); + await waitFor(() => { + // Failed and cancelled outcomes both render, with the failed reason… + const failed = canvasElement.querySelector('.maka-task-ledger-row[data-status="failed"]'); + expect(failed?.textContent).toContain('两个用例在 CI 上超时'); + expect( + canvasElement.querySelector('.maka-task-ledger-row[data-status="cancelled"]'), + ).not.toBeNull(); + // …and the oldest finished task is the one the cap dropped. + expect(canvasElement.textContent).not.toContain('归档上一轮实验数据'); + }); + }, +}; + +// Real path: 任务工作栏 → 任务 on a deeply decomposed task. The row indent is +// clamped at depth 6, so an eighth-level subtask stays legible instead of +// marching off the panel. +export const TasksDeepNesting: Story = { + decorators: [bridge({ tasks: deepTaskChain })], + render: () => , + play: async ({ canvasElement }) => { + // The eight-level chain renders down to its deepest row (reachability). The + // exact indent clamp (`--task-depth`) is a computed-style contract left to + // focused tests, not asserted here (review feedback). + await waitFor(() => + expect( + canvasElement.querySelector('.maka-task-ledger-row[aria-level="8"]'), + ).not.toBeNull(), + ); + }, +}; + // Storybook cannot host the native WebContentsView, so these pin what the panel // itself draws — chrome and empty state — inside the real workbar shell. export const BrowserEmpty: Story = { From 0f2e1f70e8148c06e6d3a3ca084b16113864c7b5 Mon Sep 17 00:00:00 2001 From: liuxiaocs7 Date: Sun, 30 Aug 2026 16:09:43 +0800 Subject: [PATCH 2/3] fix(desktop): contain deeply nested task rows Keep nested task groups at the panel width, apply depth indentation only to row content, and assert the deepest task has no horizontal overflow. Generated-by: Codex --- .../src/renderer/styles/task-ledger.css | 21 ++++++++++++------- .../stories/session-workbar.stories.tsx | 21 ++++++++++++------- packages/ui/src/task-ledger-panel.tsx | 18 +++++++++------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/renderer/styles/task-ledger.css b/apps/desktop/src/renderer/styles/task-ledger.css index b459c32cc2..b47b0f041b 100644 --- a/apps/desktop/src/renderer/styles/task-ledger.css +++ b/apps/desktop/src/renderer/styles/task-ledger.css @@ -27,9 +27,15 @@ .maka-task-ledger-tree { display: grid; gap: var(--border-width-hairline); + min-width: 0; } .maka-task-ledger-row { + display: grid; + min-width: 0; +} + +.maka-task-ledger-row-content { font: var(--maka-text-supporting); display: grid; grid-template-columns: 14px minmax(34px, auto) minmax(100px, 1fr) auto; @@ -45,18 +51,19 @@ display: grid; grid-column: 1 / -1; gap: var(--border-width-hairline); + min-width: 0; } -.maka-task-ledger-row:hover { background: var(--state-hover-bg); } +.maka-task-ledger-row:hover > .maka-task-ledger-row-content { background: var(--state-hover-bg); } -.maka-task-ledger-row:focus-visible { +.maka-task-ledger-row:focus-visible > .maka-task-ledger-row-content { outline: none; box-shadow: inset 0 0 0 var(--focus-ring-width) var(--focus-ring); } -.maka-task-ledger-row[data-status="in_progress"] > svg { color: var(--status-running); } -.maka-task-ledger-row[data-status="blocked"] > svg { color: var(--warning); } -.maka-task-ledger-row[data-status="completed"] > svg { color: var(--success); } -.maka-task-ledger-row[data-status="failed"] > svg { color: var(--destructive); } +.maka-task-ledger-row[data-status="in_progress"] > .maka-task-ledger-row-content > svg { color: var(--status-running); } +.maka-task-ledger-row[data-status="blocked"] > .maka-task-ledger-row-content > svg { color: var(--warning); } +.maka-task-ledger-row[data-status="completed"] > .maka-task-ledger-row-content > svg { color: var(--success); } +.maka-task-ledger-row[data-status="failed"] > .maka-task-ledger-row-content > svg { color: var(--destructive); } .maka-task-ledger-key { font: var(--maka-text-supporting); @@ -129,7 +136,7 @@ } @media (max-width: 620px) { - .maka-task-ledger-row { grid-template-columns: 14px minmax(32px, auto) minmax(0, 1fr); } + .maka-task-ledger-row-content { grid-template-columns: 14px minmax(32px, auto) minmax(0, 1fr); } .maka-task-ledger-meta { grid-column: 3; justify-content: flex-start; } .maka-task-ledger-detail { grid-column: 3; white-space: normal; overflow-wrap: anywhere; } } diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx index d401c1c848..c23e8692cf 100644 --- a/apps/desktop/stories/session-workbar.stories.tsx +++ b/apps/desktop/stories/session-workbar.stories.tsx @@ -985,14 +985,19 @@ export const TasksDeepNesting: Story = { decorators: [bridge({ tasks: deepTaskChain })], render: () => , play: async ({ canvasElement }) => { - // The eight-level chain renders down to its deepest row (reachability). The - // exact indent clamp (`--task-depth`) is a computed-style contract left to - // focused tests, not asserted here (review feedback). - await waitFor(() => - expect( - canvasElement.querySelector('.maka-task-ledger-row[aria-level="8"]'), - ).not.toBeNull(), - ); + await waitFor(() => { + const panel = canvasElement.querySelector('.maka-task-ledger-panel'); + const deepestRowContent = canvasElement.querySelector( + '.maka-task-ledger-row[aria-level="8"] > .maka-task-ledger-row-content', + ); + if (!panel || !deepestRowContent) throw new Error('deep task row not rendered yet'); + + const panelRect = panel.getBoundingClientRect(); + const rowRect = deepestRowContent.getBoundingClientRect(); + expect(panel.scrollWidth).toBeLessThanOrEqual(panel.clientWidth); + expect(rowRect.left).toBeGreaterThanOrEqual(panelRect.left); + expect(rowRect.right).toBeLessThanOrEqual(panelRect.right); + }); }, }; diff --git a/packages/ui/src/task-ledger-panel.tsx b/packages/ui/src/task-ledger-panel.tsx index 068bcdb091..60154f8dd4 100644 --- a/packages/ui/src/task-ledger-panel.tsx +++ b/packages/ui/src/task-ledger-panel.tsx @@ -188,14 +188,16 @@ function TaskLedgerRow({ task, copy, level, position, setSize, children }: { data-status={task.status} style={{ '--task-depth': Math.min(depth, 6) } as CSSProperties} > -