diff --git a/apps/desktop/src/renderer/styles/task-ledger.css b/apps/desktop/src/renderer/styles/task-ledger.css index b459c32cc2..d34f89d3b0 100644 --- a/apps/desktop/src/renderer/styles/task-ledger.css +++ b/apps/desktop/src/renderer/styles/task-ledger.css @@ -22,14 +22,24 @@ min-height: 0; overflow-y: auto; padding: var(--space-2); + /* The compact row layout keys off THIS panel's width, not the viewport: the + right workbar resizes down to SESSION_WORKBAR_MIN_WIDTH (320px) inside an + otherwise wide window, where a viewport media query would never fire. */ + container: maka-task-ledger / inline-size; } .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 +55,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); @@ -128,8 +139,12 @@ outline-offset: 1px; } -@media (max-width: 620px) { - .maka-task-ledger-row { grid-template-columns: 14px minmax(32px, auto) minmax(0, 1fr); } +/* Below ~420px of panel width the four-column grid can no longer hold the + deepest clamped indent (128px at depth 6) plus key + subject + status/meta, + so status/detail drop under the subject and the subject column may shrink to + zero. 420px keeps the 480px default wide while covering the 320px floor. */ +@container maka-task-ledger (max-width: 420px) { + .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 8df2bef6ba..9beeccf539 100644 --- a/apps/desktop/stories/session-workbar.stories.tsx +++ b/apps/desktop/stories/session-workbar.stories.tsx @@ -200,6 +200,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', @@ -1228,6 +1242,107 @@ 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. Pinned at the 320px workbar floor +// (SESSION_WORKBAR_MIN_WIDTH), the width where the depth-6 indent would +// overflow a four-column row — the panel container query must collapse to the +// compact layout so the deepest row still fits. +export const TasksDeepNesting: Story = { + decorators: [bridge({ tasks: deepTaskChain })], + render: () => , + play: async ({ canvasElement }) => { + 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); + }); + }, +}; + // 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 = { 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} > -