From b09dd9482799642f687a6bd30b375cd1e832fa14 Mon Sep 17 00:00:00 2001 From: ihsiang Date: Wed, 23 Sep 2026 20:10:50 +0800 Subject: [PATCH] feat(web): restore the process-row polish on the grouped work details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(web): restore the process-row polish on the grouped work details The sweep highlight, shared row colors, expanded-row spacing, and the 12–24px font-size range were developed against the pre-revert work-details implementation. Re-apply them to the grouped implementation that replaced it. - TextShimmer keeps its active-driven span and now paints a masked sweep overlay in the theme's `--dsw-alias-label-shimmer` color, so the base text inherits the caller's color and hover changes. - DisclosureRow owns the row color: tertiary at rest, secondary on hover, with glyphs, titles, and summaries inheriting it; tool, skill, command, reasoning, and workflow rows drop their own hover darkening. - ChatView spaces process rows 8px apart and keeps 16px beside Assistant responses; the grouped header sits 8px above its first member. - The Settings font-size stepper accepts 12–24px, and the grouped header's leading box and glyph scale with the content font-size delta. * fix(web): restore blue running status and refine process disclosures * fix(web): finalize running snapshots and whale animation * fix(web): preserve disclosure layout and repair CI fixtures --- THIRD_PARTY_NOTICES.md | 1 + apps/web/tests/assembled-boot.ts | 15 +++ apps/web/tests/built-boot.expected.e2e.ts | 6 +- .../clickable-links-gallery/ui.expected.md | 4 +- .../expected/cordis-history/ui.expected.md | 12 +- .../expected/fork-mid-turn/ui.expected.md | 8 +- .../conversation-expanded.expected.md | 4 +- .../conversation.expected.md | 4 +- .../markdown-cjk-strong/ui.expected.md | 4 +- .../expected/markdown-images/ui.expected.md | 4 +- .../markdown-inline-code-links/ui.expected.md | 4 +- .../expected/math-rendering/ui.expected.md | 4 +- .../reference-composer/order.expected.md | 4 +- .../skill-user-invoke/ui-expanded.expected.md | 4 +- .../expected/skill-user-invoke/ui.expected.md | 4 +- .../stats-paged-history/ui.expected.md | 112 +++++++++--------- .../expected/steer-all/mid-steer.expected.md | 4 +- .../steer-all/settled-expanded.expected.md | 4 +- .../expected/steer-all/settled.expected.md | 4 +- apps/web/tests/goal-multi-turn-actions.e2e.ts | 29 +++++ apps/web/tests/idle-submission-handoff.e2e.ts | 12 +- apps/web/tests/lifecycle-chrome.e2e.ts | 2 +- apps/web/tests/live-interactions.e2e.ts | 2 +- apps/web/tests/reasoning-preview.e2e.ts | 47 +++++++- apps/web/tests/seeded-history.e2e.ts | 1 - apps/web/tests/shimmer.ts | 66 +++++++++++ .../mid-stream.expected.md | 4 +- apps/web/tests/support.ts | 8 +- apps/web/tests/turn-tail-actions.e2e.ts | 92 +++++++++++++- apps/web/tests/workflow-run.e2e.ts | 8 +- packages/client/ui-chat/README.i18n.yaml | 4 +- packages/client/ui-chat/README.md | 4 +- packages/client/ui-chat/README.zh.md | 4 +- packages/client/ui-chat/package.json | 7 +- .../src/client/chat/ChatGroupSeat.module.css | 25 ++-- .../ui-chat/src/client/chat/ChatGroupSeat.tsx | 8 +- .../ui-chat/src/client/chat/ChatNodeSeat.tsx | 1 + .../src/client/chat/ChatView.module.css | 71 ++++++++++- .../ui-chat/src/client/chat/ChatView.tsx | 2 + .../client/chat/GenericCommandCard.module.css | 12 -- .../src/client/chat/GenericCommandCard.tsx | 10 +- .../src/client/chat/MessageItem.module.css | 64 +++------- .../ui-chat/src/client/chat/MessageItem.tsx | 7 +- .../src/client/chat/ReasoningRow.module.css | 33 ------ .../ui-chat/src/client/chat/ReasoningRow.tsx | 10 +- .../ui-chat/src/client/chat/RunningStatus.tsx | 82 +++++++++++++ .../chat/TurnProcessNodeView.module.css | 14 +-- .../src/client/chat/TurnProcessNodeView.tsx | 40 +++---- .../chat/TurnTriggerNodeView.module.css | 18 +-- .../src/client/chat/lottie-player.d.ts | 6 + .../src/client/chat/whale-tail.lottie.json | 1 + .../src/client/contract/turn-process.ts | 4 +- .../conversation-nodes/README.i18n.yaml | 4 +- .../src/client/conversation-nodes/README.md | 8 +- .../client/conversation-nodes/README.zh.md | 8 +- packages/client/ui-chat/src/client/locale.ts | 14 +-- .../ui-chat/tests/chat-view.client.spec.tsx | 87 ++++++++------ .../tests/reasoning-row.client.spec.tsx | 6 +- .../tests/turn-tail-spacing.client.spec.ts | 4 +- packages/client/ui-chat/tsconfig.client.json | 4 +- .../src/client/PresentRow.module.css | 2 +- .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 5 +- packages/client/ui-primitives/README.zh.md | 5 +- .../src/DisclosureRow.module.css | 26 +++- .../ui-primitives/src/DisclosureRow.tsx | 12 +- .../ui-primitives/src/Shimmer.module.css | 88 ++++++++++++++ packages/client/ui-primitives/src/Shimmer.tsx | 66 +++++++++++ .../ui-primitives/src/TextShimmer.module.css | 27 ----- .../client/ui-primitives/src/TextShimmer.tsx | 23 ---- .../client/ui-primitives/src/icons/index.tsx | 6 +- packages/client/ui-primitives/src/index.ts | 4 +- .../tests/shimmer.client.spec.tsx | 41 +++++++ .../tests/text-shimmer-styles.client.spec.ts | 16 --- .../tests/text-shimmer.client.spec.tsx | 29 ----- packages/client/ui-skill/README.i18n.yaml | 4 +- packages/client/ui-skill/README.md | 2 +- packages/client/ui-skill/README.zh.md | 2 +- .../ui-skill/src/client/SkillRow.module.css | 39 ++---- .../client/ui-skill/src/client/SkillRow.tsx | 31 +++-- .../ui-skill/tests/skill-row.client.spec.tsx | 6 +- packages/client/ui-theme/README.i18n.yaml | 4 +- packages/client/ui-theme/README.md | 8 +- packages/client/ui-theme/README.zh.md | 8 +- .../ui-theme/src/styles/design-platform.css | 10 ++ .../src/styles/gradient-shadow-text.css | 4 +- .../client/ui-theme/src/theme-settings.ts | 2 +- .../ui-theme/tests/boot-theme.client.spec.ts | 4 +- .../client/ui-theme/tests/config.host.spec.ts | 10 +- .../tests/font-size-row.client.spec.tsx | 6 +- .../ui-theme/tests/theme.client.spec.ts | 18 +-- packages/client/ui-tool/README.i18n.yaml | 4 +- packages/client/ui-tool/README.md | 4 +- packages/client/ui-tool/README.zh.md | 4 +- .../client/tool/components/ToolRow.module.css | 49 ++------ .../src/client/tool/components/ToolRow.tsx | 30 ++--- .../tool/toolviews/bash-sample.module.css | 25 ++-- .../src/client/tool/toolviews/bash-sample.tsx | 24 ++-- .../ui-tool/tests/diff-card.client.spec.tsx | 5 +- .../tests/tool-row-styles.client.spec.ts | 10 +- .../ui-tool/tests/tool-row.client.spec.tsx | 7 +- .../src/client/WorkflowRunPanel.module.css | 16 +-- pnpm-lock.yaml | 18 ++- scripts/rescope-vendor.ts | 1 + scripts/test-dom-environment.ts | 37 +++++- .../web/auto-review-denial/ui.expected.md | 8 +- snapshots/web/bash-abort-row/ui.expected.md | 2 +- .../web/changed-files-turn/ui.expected.md | 4 +- .../web/deepseek-messages-chat/ui.expected.md | 4 +- .../web/default-workspace/ui.expected.md | 4 +- .../feedback-command/ack-expanded.expected.md | 4 +- .../web/feedback-command/ack.expected.md | 4 +- .../feedback-release/ack-expanded.expected.md | 4 +- .../web/feedback-release/ack.expected.md | 4 +- .../web/file-upload-round/ui.expected.md | 4 +- .../fresh-round-trip/ui-expanded.expected.md | 4 +- snapshots/web/fresh-round-trip/ui.expected.md | 4 +- .../ui-expanded.expected.md | 8 +- .../goal-multi-turn-actions/ui.expected.md | 9 +- .../reloaded-expanded.expected.md | 4 +- .../web/lifecycle-chrome/reloaded.expected.md | 4 +- .../web/live-interactions/loading.expected.md | 4 +- .../retry-expanded.expected.md | 4 +- .../web/live-interactions/retry.expected.md | 4 +- .../running-draft.expected.md | 4 +- snapshots/web/message-actions/ui.expected.md | 13 +- snapshots/web/minimal-preset/ui.expected.md | 4 +- .../plan-review/approved-expanded.expected.md | 4 +- .../web/plan-review/approved.expected.md | 4 +- snapshots/web/present-svg/ui.expected.md | 4 +- snapshots/web/present/ui.expected.md | 4 +- snapshots/web/ptc-round/ui.expected.md | 4 +- .../answered-expanded.expected.md | 4 +- .../question-composer/answered.expected.md | 4 +- .../question-composer/cancelled.expected.md | 4 +- .../web/queue-actions/collapsed.expected.md | 4 +- .../web/queue-actions/editing.expected.md | 4 +- .../web/queue-actions/layout.expected.md | 4 +- .../web/queue-actions/sending.expected.md | 4 +- snapshots/web/queue-actions/ui.expected.md | 4 +- .../web/queued-image/delivered.expected.md | 8 +- snapshots/web/queued-image/queued.expected.md | 4 +- .../seeded-history/command-row.expected.md | 8 +- .../seeded-history/feedback-row.expected.md | 8 +- .../seeded-history/ui-expanded.expected.md | 8 +- snapshots/web/seeded-history/ui.expected.md | 8 +- snapshots/web/skill-tool-row/ui.expected.md | 4 +- snapshots/web/steering/mid-steer.expected.md | 4 +- .../web/steering/settled-expanded.expected.md | 4 +- snapshots/web/steering/settled.expected.md | 4 +- .../subagent-conversation/nested.expected.md | 4 +- .../sidebar-chat.expected.md | 4 +- .../ui-expanded.expected.md | 8 +- .../web/subagent-conversation/ui.expected.md | 8 +- .../offline-composer.expected.md | 4 +- .../turn-tail-actions/completed.expected.md | 4 +- .../web/turn-tail-actions/focused.expected.md | 4 +- .../web/turn-tail-actions/running.expected.md | 4 +- .../web/turn-tail-actions/settled.expected.md | 5 +- .../usage-expanded.expected.md | 4 +- snapshots/web/web-search-round/ui.expected.md | 4 +- snapshots/web/workflow-run/ui.expected.md | 4 +- tsconfig.host.json | 1 + 163 files changed, 1280 insertions(+), 783 deletions(-) create mode 100644 apps/web/tests/shimmer.ts create mode 100644 packages/client/ui-chat/src/client/chat/RunningStatus.tsx create mode 100644 packages/client/ui-chat/src/client/chat/lottie-player.d.ts create mode 100644 packages/client/ui-chat/src/client/chat/whale-tail.lottie.json create mode 100644 packages/client/ui-primitives/src/Shimmer.module.css create mode 100644 packages/client/ui-primitives/src/Shimmer.tsx delete mode 100644 packages/client/ui-primitives/src/TextShimmer.module.css delete mode 100644 packages/client/ui-primitives/src/TextShimmer.tsx create mode 100644 packages/client/ui-primitives/tests/shimmer.client.spec.tsx delete mode 100644 packages/client/ui-primitives/tests/text-shimmer-styles.client.spec.ts delete mode 100644 packages/client/ui-primitives/tests/text-shimmer.client.spec.tsx diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 862299b9a2..3edabbb639 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -96,6 +96,7 @@ External packages installed for runtime use or distributed inside the prebuilt b | [`katex`](https://github.com/KaTeX/KaTeX) | MIT | | [`koffi`](https://github.com/Koromix/koffi) | MIT | | [`lexical`](https://github.com/facebook/lexical) | MIT | +| [`lottie-web`](https://github.com/airbnb/lottie-web) | MIT | | [`mdast-util-from-markdown`](https://github.com/syntax-tree/mdast-util-from-markdown) | MIT | | [`mdast-util-gfm`](https://github.com/syntax-tree/mdast-util-gfm) | MIT | | [`mdast-util-math`](https://github.com/syntax-tree/mdast-util-math) | MIT | diff --git a/apps/web/tests/assembled-boot.ts b/apps/web/tests/assembled-boot.ts index b7ab90a8e2..34f67f4a2c 100644 --- a/apps/web/tests/assembled-boot.ts +++ b/apps/web/tests/assembled-boot.ts @@ -218,6 +218,7 @@ export function installAssembledBootEnv(): void { }) } let fontsDescriptor: PropertyDescriptor | undefined + let canvasContextDescriptor: PropertyDescriptor | undefined beforeEach(() => { fontsDescriptor = Object.getOwnPropertyDescriptor(document, 'fonts') Object.defineProperty(document, 'fonts', { configurable: true, value: new EventTarget() }) @@ -233,6 +234,18 @@ export function installAssembledBootEnv(): void { vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => setTimeout(() => { callback(0) }, 0) as unknown as number) vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) }) + vi.stubGlobal('matchMedia', (media: string) => Object.assign(new EventTarget(), { + media, matches: false, onchange: null, addListener: () => {}, removeListener: () => {}, + })) + // Lottie's SVG player creates one transparent Canvas placeholder while importing. + canvasContextDescriptor = Object.getOwnPropertyDescriptor(HTMLCanvasElement.prototype, 'getContext') + Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', { + configurable: true, + value: (contextId: string) => { + if (contextId !== '2d') throw new Error(`assembled boot: unsupported Canvas context ${contextId}`) + return { fillStyle: '', fillRect: () => {} } + }, + }) }) afterEach(async () => { @@ -263,6 +276,8 @@ export function installAssembledBootEnv(): void { delete ownNavigator.languages delete ownNavigator.language vi.unstubAllGlobals() + if (canvasContextDescriptor === undefined) Reflect.deleteProperty(HTMLCanvasElement.prototype, 'getContext') + else Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', canvasContextDescriptor) if (fontsDescriptor === undefined) Reflect.deleteProperty(document, 'fonts') else Object.defineProperty(document, 'fonts', fontsDescriptor) if (failures.length > 0) throw new AggregateError(failures, 'assembled boot teardown failed') diff --git a/apps/web/tests/built-boot.expected.e2e.ts b/apps/web/tests/built-boot.expected.e2e.ts index 7ab01184fc..f645214133 100644 --- a/apps/web/tests/built-boot.expected.e2e.ts +++ b/apps/web/tests/built-boot.expected.e2e.ts @@ -129,7 +129,8 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn const writeRow = mutationRows.find(row => row.getAttribute('data-variant') === 'write' && row.textContent?.includes('new-demo.txt')) if (writeRow === undefined) throw new Error('fixture write row missing') - expect(within(writeRow).getAllByText('+1 -0', { exact: true })).toHaveLength(1) + expect(within(writeRow).getAllByText('+1', { exact: true })).toHaveLength(1) + expect(within(writeRow).getAllByText('-0', { exact: true })).toHaveLength(1) expect(writeRow.querySelector('[data-diff]')).toBeNull() for (const row of mutationRows) { const toggle = row.querySelector('[data-expandable]') @@ -140,7 +141,8 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn const writeDiff = writeRow.querySelector('[data-diff]') if (writeDiff === null) throw new Error('expanded fixture write diff missing') within(writeDiff).getByText('hello fixture', { exact: true }) - expect(within(writeRow).getAllByText('+1 -0', { exact: true })).toHaveLength(1) + expect(within(writeRow).getAllByText('+1', { exact: true })).toHaveLength(1) + expect(within(writeRow).getAllByText('-0', { exact: true })).toHaveLength(1) expect(writeDiff.textContent).not.toContain('+1 -0') // The web render intent reaches the assembled boot graph: the fixture's diff --git a/apps/web/tests/expected/clickable-links-gallery/ui.expected.md b/apps/web/tests/expected/clickable-links-gallery/ui.expected.md index 25c6502dad..a3494849d0 100644 --- a/apps/web/tests/expected/clickable-links-gallery/ui.expected.md +++ b/apps/web/tests/expected/clickable-links-gallery/ui.expected.md @@ -7,8 +7,8 @@ - tab "Trajectory" - text: "Assemble the link gallery: write the report and styles, inspect the sources, and summarize. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Wrote files, called tools, searched code, etc." [expanded] - button "Write site/report.html +1 -0": - text: Write diff --git a/apps/web/tests/expected/cordis-history/ui.expected.md b/apps/web/tests/expected/cordis-history/ui.expected.md index 45381454fa..3de6c560dd 100644 --- a/apps/web/tests/expected/cordis-history/ui.expected.md +++ b/apps/web/tests/expected/cordis-history/ui.expected.md @@ -11,8 +11,8 @@ - button "Jump to turn 3" - text: "Use only Cordis tools. First call cordis_inspect_self with no arguments. Then call cordis_define with plugin kind \"new\", idPrefix \"snap\", name \"snapshot noop\", purpose \"does nothing, for the snapshot\", code.host exactly \"return { name: \\\"snapshot-noop\\\", apply(ctx) {} }\" and code.client exactly \"return { inject: [\\\"slots\\\"], apply(ctx) { ctx.slots.register({ name: \\\"shell.overlay\\\", id: \\\"snapshot-probe\\\" }, () => React.createElement(\\\"div\\\", { \\\"data-snapshot-probe\\\": \\\"loaded\\\" })) } }\". Read its returned pluginId and packageId, then call cordis_run with those exact IDs and mode \"run\". After the run request returns, reply exactly CORDIS_UI_READY and stop. 9/1 {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Called tools" [expanded] - button "Think" - button "Inspect plugins 0 dynamic plugins" @@ -42,8 +42,8 @@ - button "Plugin status updated 9/1 {{clock}}": - text: Plugin status updated - time: 9/1 {{clock}} -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: The Cordis Plugin is running. - button "Copy" - button "Good response" @@ -51,8 +51,8 @@ - button "Branch into a new conversation" - text: 9/1 {{clock}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. 9/1 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Called tools" [expanded] - text: Stop Cordis Plugin snap-1 - button "Inspect" diff --git a/apps/web/tests/expected/fork-mid-turn/ui.expected.md b/apps/web/tests/expected/fork-mid-turn/ui.expected.md index 98f72fecb8..4cecf35241 100644 --- a/apps/web/tests/expected/fork-mid-turn/ui.expected.md +++ b/apps/web/tests/expected/fork-mid-turn/ui.expected.md @@ -1,11 +1,11 @@ - text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - text: FORK_BRANCH_USER Summarize what this branch knows about the reads. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: FORK_BRANCH_FIRST this branch has no read results, so I would check what the parent completed before retrying. FORK_BRANCH_DONE. - button "Copy" - button "Good response" diff --git a/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md b/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md index 5f580db1a6..4abdc53e16 100644 --- a/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md +++ b/apps/web/tests/expected/github-ready-review/conversation-expanded.expected.md @@ -19,8 +19,8 @@ - button "GitHub event received {{clock}}": - text: GitHub event received - time: {{clock}} -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: "Review complete: no actionable findings." - button "Copy" - button "Good response" diff --git a/apps/web/tests/expected/github-ready-review/conversation.expected.md b/apps/web/tests/expected/github-ready-review/conversation.expected.md index 5f580db1a6..4abdc53e16 100644 --- a/apps/web/tests/expected/github-ready-review/conversation.expected.md +++ b/apps/web/tests/expected/github-ready-review/conversation.expected.md @@ -19,8 +19,8 @@ - button "GitHub event received {{clock}}": - text: GitHub event received - time: {{clock}} -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: "Review complete: no actionable findings." - button "Copy" - button "Good response" diff --git a/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md b/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md index e113cb6b3d..2eda5d3afb 100644 --- a/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md +++ b/apps/web/tests/expected/markdown-cjk-strong/ui.expected.md @@ -7,8 +7,8 @@ - tab "Trajectory" - text: Render adjacent CJK strong emphasis. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - heading "CJK strong emphasis" [level=2] - paragraph: - strong: 注意: diff --git a/apps/web/tests/expected/markdown-images/ui.expected.md b/apps/web/tests/expected/markdown-images/ui.expected.md index 31dee5c21a..d6bea9a4f8 100644 --- a/apps/web/tests/expected/markdown-images/ui.expected.md +++ b/apps/web/tests/expected/markdown-images/ui.expected.md @@ -7,8 +7,8 @@ - tab "Trajectory" - text: Show the Markdown image policy. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - heading "Markdown images" [level=2] - paragraph: - 'button "View full image: Remote test image"': diff --git a/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md b/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md index bf25c8d4ae..c9d932b478 100644 --- a/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md +++ b/apps/web/tests/expected/markdown-inline-code-links/ui.expected.md @@ -6,8 +6,8 @@ - tab "Trajectory" - text: Show the local preview URL. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - heading "Inline code links" [level=2] - paragraph: - text: "Preview:" diff --git a/apps/web/tests/expected/math-rendering/ui.expected.md b/apps/web/tests/expected/math-rendering/ui.expected.md index 3fb277318a..3d4b08ccd1 100644 --- a/apps/web/tests/expected/math-rendering/ui.expected.md +++ b/apps/web/tests/expected/math-rendering/ui.expected.md @@ -7,8 +7,8 @@ - tab "Trajectory" - text: Render this mathematical proof. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - heading "Math rendering" [level=2] - paragraph: - text: Inline dollar diff --git a/apps/web/tests/expected/reference-composer/order.expected.md b/apps/web/tests/expected/reference-composer/order.expected.md index 589d2954d1..779c3e1f20 100644 --- a/apps/web/tests/expected/reference-composer/order.expected.md +++ b/apps/web/tests/expected/reference-composer/order.expected.md @@ -7,8 +7,8 @@ - tab "Trajectory" - text: Research notes what changed? Referenced session · Research notes {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md index 930e9eb385..09a6526be6 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui-expanded.expected.md @@ -10,8 +10,8 @@ - button "meeting notes-this-is-a-very-long-filename-for-testing-user-message-file-references-and-preview-layout.md" - text: and confirm the fixture wiring {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: USER_INVOKE_REPLY acknowledged; following the injected skill. - button "Copy" - button "Good response" diff --git a/apps/web/tests/expected/skill-user-invoke/ui.expected.md b/apps/web/tests/expected/skill-user-invoke/ui.expected.md index 930e9eb385..09a6526be6 100644 --- a/apps/web/tests/expected/skill-user-invoke/ui.expected.md +++ b/apps/web/tests/expected/skill-user-invoke/ui.expected.md @@ -10,8 +10,8 @@ - button "meeting notes-this-is-a-very-long-filename-for-testing-user-message-file-references-and-preview-layout.md" - text: and confirm the fixture wiring {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: USER_INVOKE_REPLY acknowledged; following the injected skill. - button "Copy" - button "Good response" diff --git a/apps/web/tests/expected/stats-paged-history/ui.expected.md b/apps/web/tests/expected/stats-paged-history/ui.expected.md index 5af4333df1..e6a96742a4 100644 --- a/apps/web/tests/expected/stats-paged-history/ui.expected.md +++ b/apps/web/tests/expected/stats-paged-history/ui.expected.md @@ -36,8 +36,8 @@ - button "Jump to turn 28" - text: m1 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r1 - button "Copy" - button "Good response" @@ -45,8 +45,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m2 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r2 - button "Copy" - button "Good response" @@ -54,8 +54,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m3 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r3 - button "Copy" - button "Good response" @@ -63,8 +63,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m4 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r4 - button "Copy" - button "Good response" @@ -72,8 +72,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m5 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r5 - button "Copy" - button "Good response" @@ -81,8 +81,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m6 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r6 - button "Copy" - button "Good response" @@ -90,8 +90,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m7 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r7 - button "Copy" - button "Good response" @@ -99,8 +99,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m8 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r8 - button "Copy" - button "Good response" @@ -108,8 +108,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m9 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r9 - button "Copy" - button "Good response" @@ -117,8 +117,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m10 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r10 - button "Copy" - button "Good response" @@ -126,8 +126,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m11 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r11 - button "Copy" - button "Good response" @@ -135,8 +135,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m12 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r12 - button "Copy" - button "Good response" @@ -144,8 +144,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m13 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r13 - button "Copy" - button "Good response" @@ -153,8 +153,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m14 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r14 - button "Copy" - button "Good response" @@ -162,8 +162,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m15 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r15 - button "Copy" - button "Good response" @@ -171,8 +171,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m16 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r16 - button "Copy" - button "Good response" @@ -180,8 +180,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m17 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r17 - button "Copy" - button "Good response" @@ -189,8 +189,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m18 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r18 - button "Copy" - button "Good response" @@ -198,8 +198,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m19 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r19 - button "Copy" - button "Good response" @@ -207,8 +207,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m20 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r20 - button "Copy" - button "Good response" @@ -216,8 +216,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m21 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r21 - button "Copy" - button "Good response" @@ -225,8 +225,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m22 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r22 - button "Copy" - button "Good response" @@ -234,8 +234,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m23 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r23 - button "Copy" - button "Good response" @@ -243,8 +243,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m24 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r24 - button "Copy" - button "Good response" @@ -252,8 +252,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m25 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r25 - button "Copy" - button "Good response" @@ -261,8 +261,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m26 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r26 - button "Copy" - button "Good response" @@ -270,8 +270,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m27 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r27 - button "Copy" - button "Good response" @@ -279,8 +279,8 @@ - button "Branch into a new conversation" - text: 7/25 {{clock}} m28 7/25 {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: r28 - button "Copy" - button "Good response" diff --git a/apps/web/tests/expected/steer-all/mid-steer.expected.md b/apps/web/tests/expected/steer-all/mid-steer.expected.md index 6a14e1ea0f..55222e9624 100644 --- a/apps/web/tests/expected/steer-all/mid-steer.expected.md +++ b/apps/web/tests/expected/steer-all/mid-steer.expected.md @@ -8,13 +8,13 @@ - tab "Trajectory" - text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}} - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - button "Analyzing the request" - text: "Interjection: include the word BANANA in your final reply." - button "Copy" - text: "Interjection: include the word ORANGE in your final reply." - button "Copy" +- status: Deep diving +- text: Deep diving for {{duration}} - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/expected/steer-all/settled-expanded.expected.md b/apps/web/tests/expected/steer-all/settled-expanded.expected.md index 3e00422aa2..5282565384 100644 --- a/apps/web/tests/expected/steer-all/settled-expanded.expected.md +++ b/apps/web/tests/expected/steer-all/settled-expanded.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - button "Asked questions" [expanded] - button "Think" - button "Ask question 1/1 answered" diff --git a/apps/web/tests/expected/steer-all/settled.expected.md b/apps/web/tests/expected/steer-all/settled.expected.md index f419491430..c92baa21e1 100644 --- a/apps/web/tests/expected/steer-all/settled.expected.md +++ b/apps/web/tests/expected/steer-all/settled.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - button "Asked questions" - text: "Interjection: include the word BANANA in your final reply. {{clock}}" - button "Copy" diff --git a/apps/web/tests/goal-multi-turn-actions.e2e.ts b/apps/web/tests/goal-multi-turn-actions.e2e.ts index 1361a476d3..be9e5fc341 100644 --- a/apps/web/tests/goal-multi-turn-actions.e2e.ts +++ b/apps/web/tests/goal-multi-turn-actions.e2e.ts @@ -155,6 +155,32 @@ describe('web e2e: Goal keeps one assistant action row per completed turn', () = expect(sessionEvents.flatMap(event => event.type === 'request/header' ? [event.data.reason] : [])).toEqual(['initial', 'series']) await expect.poll(() => page.locator('[data-turn-process]').count(), { timeout: 15_000 }).toBe(2) + const trigger = page.locator('[data-turn-trigger]').first() + const process = page.locator('[data-turn-process]').first() + const originalFontSize = await page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size')) + await process.click() + try { + for (const fontSize of [14, 17, 24]) { + await scaffold!.ctx.settings.update('ui-theme', { fontSize }) + await expect.poll(() => page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size'))) + .toBe(`${String(fontSize)}px`) + const group = page.locator('[data-process-activity]:visible').first() + const groupFontSize = await group.evaluate(element => getComputedStyle(element).fontSize) + expect(groupFontSize).toBe(`${String(fontSize)}px`) + expect(await process.evaluate(element => getComputedStyle(element).fontSize)).toBe(groupFontSize) + expect(await trigger.getByText('Continuing goal', { exact: true }) + .evaluate(element => getComputedStyle(element).fontSize)).toBe(groupFontSize) + expect(await process.evaluate(element => element.getBoundingClientRect().top)) + .toBe(await trigger.evaluate(element => element.getBoundingClientRect().bottom) + 16) + expect(await group.evaluate(element => element.getBoundingClientRect().top)) + .toBe(await process.evaluate(element => element.getBoundingClientRect().bottom) + 16) + } + } finally { + await scaffold!.ctx.settings.update('ui-theme', { fontSize: Number.parseFloat(originalFontSize) }) + await expect.poll(() => page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size'))) + .toBe(originalFontSize) + await process.click() + } expect(await page.getByRole('button', { name: 'System prompt' }).count()).toBe(0) expect(await page.locator( '[data-chat-flow-kind="system-prompt"]', @@ -163,7 +189,10 @@ describe('web e2e: Goal keeps one assistant action row per completed turn', () = await expect.poll(() => branchButtons.count(), { timeout: 15_000 }).toBe(2) expect(await branchButtons.evaluateAll(buttons => buttons.map(button => button.getAttribute('aria-disabled')))) .toEqual([null, null]) + await page.mouse.move(0, 0) await branchButtons.last().focus() + await page.locator('[data-composer-input]').first().focus() + await expect.poll(() => page.getByRole('tooltip').count()).toBe(0) const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd) await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE) const expanded = await captureExpandedTurnProcessAria( diff --git a/apps/web/tests/idle-submission-handoff.e2e.ts b/apps/web/tests/idle-submission-handoff.e2e.ts index 9d27909d4d..2fb9930fcb 100644 --- a/apps/web/tests/idle-submission-handoff.e2e.ts +++ b/apps/web/tests/idle-submission-handoff.e2e.ts @@ -144,8 +144,10 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first if (order === 'transcript-first') { await expect.poll(() => gate.turnStartDelivery()).toBeDefined() - gate.releaseThrough(gate.turnStartDelivery()!) - await page.locator('[data-turn-process]').last().filter({ hasText: 'Deep diving' }).waitFor() + const start = gate.turnStartDelivery()! + if (start.event?.type !== 'turn/start') throw new Error('expected a buffered turn/start') + gate.releaseThrough(start) + await page.locator(`[data-chat-flow-kind="turn-process"][data-chat-turn="${String(start.event.data.turn)}"]`).waitFor({ state: 'attached' }) await expect.poll(() => placement(page)).toMatchObject({ echo: 1, dock: 0, durable: 0 }) trace.push({ phase: 'turn-start', ...await placement(page) }) await expect.poll(() => gate.admissionDelivery()).toBeDefined() @@ -166,8 +168,10 @@ it.skipIf(webSnapshotMode() === 'record').each(['inbox-first', 'transcript-first if (order === 'inbox-first') { await expect.poll(() => gate.turnStartDelivery()).toBeDefined() - gate.releaseThrough(gate.turnStartDelivery()!) - await page.locator('[data-turn-process]').last().filter({ hasText: 'Deep diving' }).waitFor() + const start = gate.turnStartDelivery()! + if (start.event?.type !== 'turn/start') throw new Error('expected a buffered turn/start') + gate.releaseThrough(start) + await page.locator(`[data-chat-flow-kind="turn-process"][data-chat-turn="${String(start.event.data.turn)}"]`).waitFor({ state: 'attached' }) await expect.poll(() => placement(page)).toMatchObject({ echo: 1, dock: 0, durable: 0 }) trace.push({ phase: 'turn-start', ...await placement(page) }) } diff --git a/apps/web/tests/lifecycle-chrome.e2e.ts b/apps/web/tests/lifecycle-chrome.e2e.ts index ecedf4ed73..f06f73ffea 100644 --- a/apps/web/tests/lifecycle-chrome.e2e.ts +++ b/apps/web/tests/lifecycle-chrome.e2e.ts @@ -344,7 +344,7 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', () await reasoningComplete.promise expect(await thinking.getAttribute('data-preview')).toBeNull() expect(await thinking.getByRole('button').getAttribute('aria-expanded')).toBe('false') - expect(await thinking.locator('[data-streaming]').isVisible()).toBe(false) + expect(await thinking.locator('[data-streaming]:not([inert] *)').isVisible()).toBe(false) } observedReasoning.resolve(undefined) return await settled diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts index 6592322148..ef9667049a 100644 --- a/apps/web/tests/live-interactions.e2e.ts +++ b/apps/web/tests/live-interactions.e2e.ts @@ -158,7 +158,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { // hang (prefix chunks delivered to the loop) before the stop click. await expect.poll(() => existsSync(marker), { timeout: 15_000 }).toBe(true) await expect.poll( - () => page.getByRole('status').filter({ hasText: 'Deep diving...' }).isVisible(), + () => page.getByRole('status').filter({ hasText: 'Deep diving' }).isVisible(), { timeout: 10_000 }, ).toBe(true) await page.locator('[data-streaming="true"]') diff --git a/apps/web/tests/reasoning-preview.e2e.ts b/apps/web/tests/reasoning-preview.e2e.ts index 9571b51d1c..7820173e55 100644 --- a/apps/web/tests/reasoning-preview.e2e.ts +++ b/apps/web/tests/reasoning-preview.e2e.ts @@ -8,6 +8,7 @@ import { captureStableAria, compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode, } from './scaffold.ts' import { connectFreshWorkspace, expandOwningTurnProcess, newEnglishPage, writeComposerDraft } from './support.ts' +import { expectSharedShimmer } from './shimmer.ts' const SUMMARY = `Next paragraph: ${'inspect the loaded context and pending tools '.repeat(8).trim()}` const DELTAS = ['First paragraph', `\nDetails\n\n\n${SUMMARY}`, '\nMore detail'] @@ -49,7 +50,7 @@ it('shows completed paragraph first lines across blank lines with a right-edge f const browser = await chromium.launch() try { const page = await newEnglishPage(browser) - const console = watchConsole(page) + const pageConsole = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await connectFreshWorkspace(page, scaffold.workspaceCwd) await page.setViewportSize({ width: 480, height: 1000 }) @@ -68,9 +69,32 @@ it('shows completed paragraph first lines across blank lines with a right-edge f first.proceed.resolve(undefined) await second.arrived.promise - const preview = reasoning.locator('[data-streaming]') + const preview = reasoning.locator('[data-streaming]:not([inert] *)') await expect.poll(() => preview.textContent()).toBe('First paragraph') expect(await preview.isVisible()).toBe(true) + for (const fontSize of [14, 24]) { + await scaffold.ctx.settings.update('ui-theme', { fontSize }) + await expect.poll(() => page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size'))) + .toBe(`${String(fontSize)}px`) + const geometry = await preview.evaluate((element) => { + const text = element.firstElementChild + const header = element.closest('[data-disclosure-row]')! + const range = document.createRange() + range.selectNodeContents(text!) + return { + fadeStart: element.getBoundingClientRect().right - 48, + textRight: range.getBoundingClientRect().right, + previewRight: element.getBoundingClientRect().right, + headerRight: header.getBoundingClientRect().right, + headerHeight: header.getBoundingClientRect().height, + } + }) + expect(geometry.textRight).toBeLessThanOrEqual(geometry.fadeStart) + expect(geometry.previewRight).toBe(geometry.headerRight) + expect(geometry.headerHeight).toBe(24 + fontSize - 14) + } + await scaffold.ctx.settings.update('ui-theme', { fontSize: 14 }) + await expect.poll(() => page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size'))).toBe('14px') await preview.evaluate((element) => { element.setAttribute('data-retained-preview', 'true') }) second.proceed.resolve(undefined) @@ -79,15 +103,30 @@ it('shows completed paragraph first lines across blank lines with a right-edge f expect(await preview.getAttribute('data-retained-preview')).toBe('true') expect(await preview.evaluate(element => getComputedStyle(element).maskImage)).toContain('linear-gradient') expect(await preview.evaluate(element => element.scrollWidth > element.clientWidth)).toBe(true) + await expectSharedShimmer(reasoning) + const group = page.locator('[data-process-activity]').filter({ has: page.locator('[data-shimmer="true"]') }) + await expectSharedShimmer(group) + await group.hover() + await expectSharedShimmer(group) expect(await reasoning.getByRole('button').getAttribute('aria-expanded')).toBe('false') await compareOrRefreshGolden(UI_EXPECTED, await captureStableAria(page, '[data-variant="think"]', scaffold.workspaceCwd), webSnapshotMode()) + await group.click() + expect(await group.getAttribute('aria-expanded')).toBe('false') + await expectSharedShimmer(group) + await page.emulateMedia({ reducedMotion: 'reduce' }) + try { + expect(await group.locator('[inert]').evaluate(element => getComputedStyle(element).display)).toBe('none') + expect(await group.innerText()).toBe('Analyzing the request') + } finally { + await page.emulateMedia({ reducedMotion: null }) + } third.proceed.resolve(undefined) await settled await page.getByText('Done', { exact: true }).waitFor() - expect(console.pageErrors).toEqual([]) - expect(console.warnings).toEqual([]) + expect(pageConsole.pageErrors).toEqual([]) + expect(pageConsole.warnings).toEqual([]) } finally { adapter.release() await browser.close() diff --git a/apps/web/tests/seeded-history.e2e.ts b/apps/web/tests/seeded-history.e2e.ts index 8419aae826..5c24765398 100644 --- a/apps/web/tests/seeded-history.e2e.ts +++ b/apps/web/tests/seeded-history.e2e.ts @@ -372,7 +372,6 @@ describe('web e2e: seeded history renders through cold resume', () => { const processBottom = await process.evaluate(element => element.getBoundingClientRect().bottom) const answerTop = await page.getByText('DONE', { exact: true }).evaluate(element => element.getBoundingClientRect().top) - // Collapsed control row keeps its own 8px margin plus the 8px flow gap. expect(answerTop).toBe(processBottom + 16) expect(await page.getByText('Context compacted', { exact: true }).count()).toBe(0) // Tool cards render from logged tool/call + tool/result alone (views are diff --git a/apps/web/tests/shimmer.ts b/apps/web/tests/shimmer.ts new file mode 100644 index 0000000000..660b811125 --- /dev/null +++ b/apps/web/tests/shimmer.ts @@ -0,0 +1,66 @@ +/** Browser geometry for one moving mask over text beside retained row icons. */ +import type { Locator } from 'playwright' +import { expect } from 'vitest' + +/** + * Check glyph alignment and scroll overflow across a running row's shared sweep. + * @param row - visible row containing exactly one active shimmer. + * @returns after restoring the row's animation playback. + */ +export async function expectSharedShimmer(row: Locator): Promise { + const shimmer = row.locator('[data-shimmer="true"]') + expect(await shimmer.count()).toBe(1) + const frames = await shimmer.evaluate((root) => { + const base = root.firstElementChild! + const decoration = root.querySelector(':scope > [inert]')! + const mask = decoration.firstElementChild! + const highlight = mask.firstElementChild! + const animations = root.getAnimations({ subtree: true }) + .filter(animation => animation instanceof CSSAnimation && animation.animationName.includes('dsh-row-shimmer-')) + const original = animations.map(animation => ({ animation, time: animation.currentTime, state: animation.playState })) + try { + return [400, 800, 1200, 1301, 1799, 1801].map((time) => { + for (const animation of animations) { + animation.pause() + animation.currentTime = time + } + const originals = [...base.querySelectorAll('svg, span, button')] + const copies = [...highlight.querySelectorAll('svg, span, button')] + return { + animations: animations.length, + mask: getComputedStyle(mask).maskImage, + hidden: decoration.getAttribute('aria-hidden'), + maskLeft: mask.getBoundingClientRect().left, + width: root.getBoundingClientRect().width, + clientWidth: root.clientWidth, + scrollWidth: root.scrollWidth, + icons: base.querySelectorAll('svg').length, + copyCount: copies.length, + baseCount: originals.length, + glyphOffsets: originals.map((element, index) => { + const a = element.getBoundingClientRect() + const b = copies[index]!.getBoundingClientRect() + return [a.left - b.left, a.top - b.top, a.width - b.width, a.height - b.height] + }).flat(), + } + }) + } finally { + for (const { animation, time, state } of original) { + animation.currentTime = time + if (state === 'running') animation.play() + } + } + }) + for (const frame of frames) { + expect(frame.animations).toBe(2) + expect(frame.mask).toContain('linear-gradient') + expect(frame.hidden).toBe('true') + expect(frame.width).toBeGreaterThan(0) + expect(frame.scrollWidth).toBe(frame.clientWidth) + expect(frame.icons).toBe(0) + expect(frame.copyCount).toBe(frame.baseCount) + expect(frame.glyphOffsets.every(offset => Math.abs(offset) < 0.1)).toBe(true) + } + expect(frames[0]!.maskLeft).toBeLessThan(frames[1]!.maskLeft) + expect(frames[1]!.maskLeft).toBeLessThan(frames[2]!.maskLeft) +} diff --git a/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md b/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md index a2443a2e47..743e96c4f8 100644 --- a/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md +++ b/apps/web/tests/snapshots/streaming-fence-highlight/mid-stream.expected.md @@ -8,12 +8,12 @@ - tab "Trajectory" - text: Stream one TypeScript fence for the highlighting snapshot. {{clock}} - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - text: ts - button "Wrap lines" [pressed] - button "Copy" - code: "const first: number = 1 const second = \"two\" let tail" +- status: Deep diving +- text: Deep diving for {{duration}} - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts index 884f160418..0918814111 100644 --- a/apps/web/tests/support.ts +++ b/apps/web/tests/support.ts @@ -85,7 +85,7 @@ export async function expandTurnProcesses(page: Page): Promise { } /** - * Expand the Turn process and secondary group containing a hidden descendant. + * Expand the settled Turn process and secondary group containing a hidden descendant. * @param page - page containing the Chat view. * @param target - descendant whose outer process disclosures should open. */ @@ -94,8 +94,10 @@ export async function expandOwningTurnProcess(page: Page, target: Locator): Prom const turn = await target.evaluate(element => element.closest('[data-chat-turn]')?.dataset.chatTurn) if (turn !== undefined) { const control = page.locator(`[data-turn-process="${turn}"]`) - await control.waitFor({ state: 'visible', timeout: 10_000 }) - if (await control.getAttribute('aria-expanded') === 'false') await control.click() + if (await control.count() > 0) { + await control.waitFor({ state: 'visible', timeout: 10_000 }) + if (await control.getAttribute('aria-expanded') === 'false') await control.click() + } } const group = target.locator('xpath=ancestor::*[@data-chat-group-key][1]') const header = group.locator('[data-process-activity]').first() diff --git a/apps/web/tests/turn-tail-actions.e2e.ts b/apps/web/tests/turn-tail-actions.e2e.ts index 7a7bfe9293..d4bbccbc65 100644 --- a/apps/web/tests/turn-tail-actions.e2e.ts +++ b/apps/web/tests/turn-tail-actions.e2e.ts @@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url' import { join } from 'node:path' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' +import { expectSharedShimmer } from './shimmer.ts' import { afterEach, describe, expect, it, onTestFailed, vi } from 'vitest' import type { ReplayOverrideDoc } from '@deepseek-ai/dsh-llm-replay' import type { SessionEvent } from '@deepseek-ai/dsh-session' @@ -144,16 +145,84 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => { // so the first step's message and tool result are already durable. await expect.poll(() => existsSync(marker), { timeout: 20_000 }).toBe(true) const runningProcess = page.locator('[data-turn-process]') - expect(await runningProcess.count()).toBe(1) - expect(await runningProcess.isDisabled()).toBe(true) - expect(await runningProcess.getAttribute('aria-expanded')).toBe('true') + expect(await runningProcess.count()).toBe(0) await expect.poll( - () => page.getByRole('status').filter({ hasText: 'Deep diving...' }).isVisible(), + () => page.getByRole('status').filter({ hasText: 'Deep diving' }).isVisible(), { timeout: 10_000 }, ).toBe(true) await page.locator('[data-streaming="true"]') .getByText('partial', { exact: true }) .waitFor({ timeout: 10_000 }) + const runningStatus = page.locator('[data-chat-running]') + expect(await runningStatus.innerText()).toMatch(/Deep diving for \d+s/) + await expectSharedShimmer(runningStatus) + expect(await runningStatus.evaluate(element => getComputedStyle(element).fontSize)).toBe('12px') + const contentBottom = await page.locator('[data-streaming="true"]').evaluate(element => + element.closest('[data-chat-flow-kind="assistant-step"]')!.getBoundingClientRect().bottom) + expect(await runningStatus.evaluate(element => element.getBoundingClientRect().top)).toBe(contentBottom + 32) + const originalFontSize = await page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size')) + try { + for (const fontSize of [14, 17, 24]) { + await scaffold!.ctx.settings.update('ui-theme', { fontSize }) + await expect.poll(() => page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size'))) + .toBe(`${String(fontSize)}px`) + const layout = await runningStatus.evaluate((status) => { + const icon = status.querySelector('svg')! + const label = status.querySelector('[data-shimmer] > span > span')! + const group = document.querySelector('[data-process-activity]')! + const groupIcon = group.querySelector('[data-step-process-icon]')! + return { + iconSize: icon.getBoundingClientRect().width, + groupIconSize: groupIcon.getBoundingClientRect().width, + gap: label.getBoundingClientRect().left - icon.getBoundingClientRect().right, + iconColor: getComputedStyle(icon).color, + textColor: getComputedStyle(label).color, + } + }) + expect(layout.iconSize).toBe(layout.groupIconSize - 2) + expect(layout.gap).toBe(6) + expect(layout.iconColor).toBe(layout.textColor) + } + } finally { + await scaffold!.ctx.settings.update('ui-theme', { fontSize: Number.parseFloat(originalFontSize) }) + await expect.poll(() => page.evaluate(() => document.body.style.getPropertyValue('--dsh-content-font-size'))) + .toBe(originalFontSize) + } + const originalDarkTheme = await page.locator('body').getAttribute('data-ds-dark-theme') + try { + await page.evaluate(() => { document.body.setAttribute('data-ds-dark-theme', '') }) + const colors = await runningStatus.evaluate((element) => { + const context = document.createElement('canvas').getContext('2d', { willReadFrequently: true })! + const rgb = (color: string) => { + context.fillStyle = color + context.fillRect(0, 0, 1, 1) + return [...context.getImageData(0, 0, 1, 1).data].slice(0, 3).map(channel => channel / 255) + } + const luminance = (channels: number[]) => channels + .map(channel => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4) + .reduce((sum, channel, index) => sum + channel * [0.2126, 0.7152, 0.0722][index]!, 0) + const base = rgb(getComputedStyle(element).color) + const sweep = rgb(getComputedStyle(element.querySelector('[data-shimmer] > [inert] > span')!).color) + const max = Math.max(...base) + const min = Math.min(...base) + return { + baseLight: luminance(base), + sweepLight: luminance(sweep), + saturation: (max - min) / (1 - Math.abs(max + min - 1)), + blueSweep: sweep[2]! > sweep[1]! && sweep[1]! > sweep[0]!, + } + }) + expect(colors.saturation).toBeLessThan(0.7) + expect(colors.baseLight).toBeGreaterThan(0.25) + expect(colors.sweepLight).toBeGreaterThan(colors.baseLight * 1.3) + expect(colors.blueSweep).toBe(true) + await expectSharedShimmer(runningStatus) + } finally { + await page.evaluate((value) => { + if (value === null) document.body.removeAttribute('data-ds-dark-theme') + else document.body.setAttribute('data-ds-dark-theme', value) + }, originalDarkTheme) + } // Only the user bubble owns a footer (clock + copy; user bubbles carry no // branch action): the narration is not the answer yet. const copyButtons = page.getByRole('button', { name: 'Copy' }) @@ -170,6 +239,19 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => { await settled expect(sessionEvents.filter(e => e.type === 'turn/end').map(e => e.data.reason.kind)).toEqual(['aborted']) await page.locator('[data-turn-process]').waitFor({ timeout: 10_000 }) + const stopped = page.getByRole('button', { name: 'Stopped', exact: true }) + expect(await stopped.isEnabled()).toBe(true) + expect(await stopped.evaluate(element => getComputedStyle(element).fontSize)).toBe('14px') + expect(await stopped.getAttribute('aria-expanded')).toBe('false') + expect(await runningStatus.count()).toBe(0) + const processGroup = page.locator('[data-process-activity]').first() + expect(await processGroup.isVisible()).toBe(false) + await stopped.click() + expect(await stopped.getAttribute('aria-expanded')).toBe('true') + expect(await processGroup.isVisible()).toBe(true) + await stopped.click() + expect(await stopped.getAttribute('aria-expanded')).toBe('false') + expect(await processGroup.isVisible()).toBe(false) await expect.poll(() => copyButtons.count(), { timeout: 10_000 }).toBe(2) await expect.poll(() => page.locator('[data-streaming="true"]').count(), { timeout: 10_000 }).toBe(0) await copyButtons.last().focus() @@ -235,7 +317,7 @@ describe('web e2e: assistant IconActions wait for the turn to end', () => { element.closest('[data-chat-flow-kind="turn-process"]')?.getBoundingClientRect().bottom) const answerTop = await page.getByText('DONE', { exact: true }).evaluate(element => element.closest('[data-chat-flow-kind="assistant-step"]')?.getBoundingClientRect().top) - expect(answerTop).toBe((processBottom ?? 0) + 8) + expect(answerTop).toBe((processBottom ?? 0) + 16) await process.focus() const completed = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd) await compareOrRefreshGolden(COMPLETED_EXPECTED, completed, MODE) diff --git a/apps/web/tests/workflow-run.e2e.ts b/apps/web/tests/workflow-run.e2e.ts index 42c5b1d8fc..8c497e0db7 100644 --- a/apps/web/tests/workflow-run.e2e.ts +++ b/apps/web/tests/workflow-run.e2e.ts @@ -134,7 +134,7 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () = const disclosures = element.querySelectorAll('[data-disclosure-row]') const runHeader = disclosures[0] const phaseHeader = disclosures[1] - const phaseTitle = phaseHeader?.children.item(1) as HTMLElement | null + const phaseTitle = phaseHeader?.querySelector('[data-shimmer] > span > span') ?? null const phaseStatus = element.querySelector('[data-phase-status-text]') const originalPhaseTitle = phaseTitle?.textContent ?? '' if (phaseTitle !== null) phaseTitle.textContent = 'A phase name long enough to require ellipsis in the narrow layout' @@ -153,6 +153,10 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () = phaseHeight: phaseHeader?.getBoundingClientRect().height ?? 0, phaseTitleRight, phaseStatusLeft, + phaseStatusRight: phaseStatus?.getBoundingClientRect().right ?? 0, + phaseHeaderRight: phaseHeader?.getBoundingClientRect().right ?? 0, + contentGap: phaseTitle?.parentElement === null || phaseTitle?.parentElement === undefined + ? '' : getComputedStyle(phaseTitle.parentElement).columnGap, } }) expect(darkNarrow.clientWidth).toBe(356) @@ -165,6 +169,8 @@ describe.skipIf(MODE === 'record')('web e2e: durable workflow run in Chat', () = expect(darkNarrow.runHeight).toBe(32) expect(darkNarrow.phaseHeight).toBe(32) expect(darkNarrow.phaseTitleRight).toBeLessThanOrEqual(darkNarrow.phaseStatusLeft) + expect(darkNarrow.phaseStatusRight).toBe(darkNarrow.phaseHeaderRight) + expect(darkNarrow.contentGap).toBe('6px') await page.locator('[data-workflow-run]').evaluate((element) => { (element as HTMLElement).style.removeProperty('width') document.body.removeAttribute('data-ds-dark-theme') diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index 01fae81ea8..693a7fed3d 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-chat/README.md -README.md: cca209016220560b7f39831e11ebb677da515ea9 -README.zh.md: 0b97e490f5a535c3b871ec7c6344d4299976b683 +README.md: 463fd1d0ea3ad97623eabf1c4d57371fd7310927 +README.zh.md: 8e3d4455e74f55c872ec5011f0b8ac59935d3b3b diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index cca2090162..463fd1d0ea 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -40,7 +40,7 @@ Settings → General → Open chat links in selects the destination for ordinary ## Hidden Chat rows -Chat omits system-prompt, ordinary Context injection, and `permission` command rows in every work-details mode. The filter changes neither recorded Session events nor Trajectory inspection. Non-human Turn triggers remain independent notices; other command rows remain in Chat. +Chat omits system-prompt, ordinary Context injection, and `permission` command rows in every work-details mode. The filter changes neither recorded Session events nor Trajectory inspection. Non-human Turn triggers remain independent, bordered notices with a contrasting neutral background; other command rows remain in Chat. When an Assistant attempt retires without a visible message, Chat hides its already-published Node instead of removing its key. A retry in the same Step reuses that key when visible content returns. This also applies when the loaded window lacks the Step start. @@ -99,6 +99,8 @@ Each group owns local `useDisclosure` state that survives mode changes while its The Chat-node slot injects a reset-bound `useDisclosure` Hook for reasoning and tools. Intermediate renderers forward it without subscribing; each invocation owns independent open state. Source callbacks retain their receiver and stable identity. When an enclosing Turn actually hides a process member, its seat resets those disclosures without replacing component keys or changing the Hook reference. Display-mode changes preserve their open state. +Process rows share one text and icon color in every Work details mode: tertiary at rest and secondary on hover, while error and warning text keeps its semantic color. Running group titles, reasoning previews, tool and command rows, the active retry line, and the bottom running status use the shared [Shimmer](../ui-primitives/README.md#component-catalog). Each row’s text shares one left-to-right highlight, including collapsed group titles; icons and hover chevrons retain their base appearance. Non-response rows sit 8px apart; anything beside an Assistant response is 16px away, and a closed Turn control keeps its answer 16px below the summary. Leading icons, disclosure chevrons, and their boxes scale with the content font-size delta. + ----- diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 0b97e490f5..8e3d4455e7 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -40,7 +40,7 @@ Chat 在节点列表外通过一个 `MarkdownDelegateProvider` 提供文件及 H ## Chat 隐藏的行 -Chat 在所有工作过程展示模式下都不显示系统提示词行、普通上下文注入和 `permission` 命令行。该过滤不改变已记录的 Session 事件或 Trajectory 查看能力。非人工轮次触发仍作为独立通知显示,其他命令行仍保留在 Chat 中。 +Chat 在所有工作过程展示模式下都不显示系统提示词行、普通上下文注入和 `permission` 命令行。该过滤不改变已记录的 Session 事件或 Trajectory 查看能力。非人工轮次触发仍作为独立通知显示,使用细边框和与周围有区分度的中性底色,其他命令行仍保留在 Chat 中。 Assistant 尝试结束且没有可见消息时,Chat 隐藏已发布的 Node,不移除其 key。同一 Step 的重试再次产生可见内容时,复用该 key。已加载窗口缺少 Step 起点时也遵循此规则。 @@ -99,6 +99,8 @@ Chat 通过 `uiConversation.groups` 注册过程 Group Definition。React 通过 Chat 节点 slot 为推理与工具注入绑定重置来源的 `useDisclosure` 钩子。中间 renderer 只透传、不订阅,每次调用拥有独立展开状态。来源回调保留接收对象及稳定引用。外层轮次实际隐藏过程成员时,所在节点重置这些开合状态,不替换组件 key,也不改变钩子引用。展示模式切换保留展开状态。 +过程行的文字与图标在所有工作过程展示模式下共用一种颜色:默认 tertiary、悬停 secondary,错误与警告文字保留语义色。运行中的分组标题、思考预览、工具与命令行、当前重试行以及底部运行状态使用共享的 [Shimmer](../ui-primitives/README.zh.md#component-catalog)。每行的文字共用一道从左到右的高光,包括折叠的分组标题;图标和悬停箭头保留底色。非回复条目之间相隔 8px;与 Assistant 回复相邻的条目相隔 16px,收起的整轮控件与其回答之间保持 16px。前置图标、折叠箭头及其容器随正文字号增量缩放。 + ----- diff --git a/packages/client/ui-chat/package.json b/packages/client/ui-chat/package.json index 61174be32d..ca98b7dc89 100644 --- a/packages/client/ui-chat/package.json +++ b/packages/client/ui-chat/package.json @@ -82,17 +82,18 @@ "@deepseek-ai/dsh-session": "workspace:*", "@deepseek-ai/dsh-session-stats": "workspace:*", "@deepseek-ai/dsh-session-turn-outline": "workspace:*", + "@deepseek-ai/dsh-settings": "workspace:*", "@deepseek-ai/dsh-subagent": "workspace:*", "@deepseek-ai/dsh-token-meter": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*", - "@deepseek-ai/dsh-util-workspace-path": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", + "@deepseek-ai/dsh-util-workspace-path": "workspace:*", "@tanstack/react-virtual": "^3.14.9", "@types/react": "~18.3.1", "@types/react-dom": "~18.3.0", + "lottie-web": "^5.13.0", "react": "^18.2.0", - "react-dom": "^18.2.0", - "@deepseek-ai/dsh-settings": "workspace:*" + "react-dom": "^18.2.0" }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~" diff --git a/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css b/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css index 56687cd84e..ad2e0379d2 100644 --- a/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css +++ b/packages/client/ui-chat/src/client/chat/ChatGroupSeat.module.css @@ -10,7 +10,7 @@ padding: 0; border: 0; background: none; - color: var(--dsw-alias-label-secondary); + color: var(--dsw-alias-label-tertiary); font: inherit; font-size: var(--dsh-content-font-size, 14px); text-align: left; @@ -19,18 +19,30 @@ } .title:hover { - color: var(--dsw-alias-label-primary); + color: var(--dsw-alias-label-secondary); } +/* The leading box and its glyph ride the body font-size delta so the icon + keeps its optical share of the row at every Settings size. */ .leading { position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center; - width: 16px; - height: 16px; - color: var(--dsw-alias-label-tertiary); + width: calc(16px + var(--dsh-content-font-delta, 0px)); + height: calc(16px + var(--dsh-content-font-delta, 0px)); + color: inherit; +} + +.leading svg { + width: calc(14px + var(--dsh-content-font-delta, 0px)); + height: calc(14px + var(--dsh-content-font-delta, 0px)); +} + +.leading svg[width="16"] { + width: calc(16px + var(--dsh-content-font-delta, 0px)); + height: calc(16px + var(--dsh-content-font-delta, 0px)); } .activityIcon, @@ -68,7 +80,7 @@ } .title[aria-expanded="true"] { - padding-bottom: 16px; + padding-bottom: 8px; } .body { @@ -121,7 +133,6 @@ margin-top: var(--dsh-chat-flow-gap, 8px); } .expandedBody { - --dsh-chat-flow-gap: 16px; max-height: none; overflow: visible; scrollbar-gutter: auto; diff --git a/packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx b/packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx index 2944f85c38..2ca89d0ef2 100644 --- a/packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx +++ b/packages/client/ui-chat/src/client/chat/ChatGroupSeat.tsx @@ -4,7 +4,7 @@ import { IconAgentPresetOutlineRegular, IconApiOutlineRegular, IconBrowseOutlineRegular, IconChevronDownOutlineRegular, IconChevronUpOutlineRegular, IconCodeOutlineRegular, IconEditOutlineRegular, IconGlobeOutlineRegular, IconPlanOutlineRegular, IconQuestionOutlineRegular, IconSearchOutlineRegular, IconSparkleRegular, - IconThinkOutlineRegular, TextShimmer, + IconThinkOutlineRegular, Shimmer, ShimmerText, } from '@deepseek-ai/dsh-client-ui-primitives' import type { GroupKey, NodeReference } from '@deepseek-ai/dsh-client-ui-conversation/client' import type { ProcessActivity } from '../contract/process-groups.ts' @@ -120,7 +120,9 @@ const ProcessGroupHeader = memo(function ProcessGroupHeader({ groupKey, useChatG {open ? : } - {title} + + {title} + ) }) @@ -141,7 +143,7 @@ export const ChatGroupSeat = memo(function ChatGroupSeat({ groupKey, useChatGrou const grouped = props.usePresentation(policy => turnLocation?.status !== 'open' || policy.stepGrouping !== 'none') const reason = turnLocation?.end?.data.reason.kind const alwaysOpen = presentation?.turnClosed === false || presentation?.hasInterleavedInput === true - || reason === 'aborted' || reason === 'error' + || reason === 'error' const spec = presentation?.spec const selectStored = useCallback((state: Readonly) => turn === undefined ? undefined : storedTurnProcessEntry(state, turn), [turn]) diff --git a/packages/client/ui-chat/src/client/chat/ChatNodeSeat.tsx b/packages/client/ui-chat/src/client/chat/ChatNodeSeat.tsx index 5bb003bdfa..5051844c96 100644 --- a/packages/client/ui-chat/src/client/chat/ChatNodeSeat.tsx +++ b/packages/client/ui-chat/src/client/chat/ChatNodeSeat.tsx @@ -153,6 +153,7 @@ export const ChatNodeSeat = memo(function ChatNodeSeat({ data-chat-node-key={routedNode.key} data-chat-group-part={groupPart} data-chat-flow-kind={routedNode.kind} + data-chat-flow-response={groupPart === 'response' || undefined} data-chat-turn={turn} data-turn-process-member={processMember || undefined} data-turn-process-hidden={processHidden || undefined} diff --git a/packages/client/ui-chat/src/client/chat/ChatView.module.css b/packages/client/ui-chat/src/client/chat/ChatView.module.css index a6b3955f8c..d652100e90 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.module.css +++ b/packages/client/ui-chat/src/client/chat/ChatView.module.css @@ -67,7 +67,17 @@ hidden and empty Seats do not contribute spacing. */ .column > :not([hidden]):not(.flowItem:empty) ~ :not([hidden]):not(.flowItem:empty) { - margin-top: var(--dsh-chat-flow-gap, 16px); + margin-top: var(--dsh-chat-flow-gap, 8px); +} + +/* Assistant responses have 16px clearance from adjacent flow items. */ +.flowItem[data-chat-flow-response] { + --dsh-chat-flow-gap: 16px; +} + +.column > [data-chat-flow-response]:not([hidden]):not(:empty) + + :not([hidden]):not(.flowItem:empty):not(.running) { + --dsh-chat-flow-gap: 16px; } /* Settled-flow identity boundary. It is neutral until it becomes the natural @@ -76,10 +86,11 @@ min-width: 0; } -/* A closed process reads as one summary immediately followed by its answer. - Expanded process rows return to the ordinary 16px rhythm. */ -.flowItem[data-turn-process-answer] { - --dsh-chat-flow-gap: 8px; +/* The Turn header has 16px clearance from its input and following content. */ +.flowItem[data-chat-flow-kind="turn-process"], +.flowItem[data-turn-process-answer], +.column > [data-chat-flow-kind="turn-process"] + :not([hidden]):not(.flowItem:empty):not(.running) { + --dsh-chat-flow-gap: 16px; } /* Empty Seats retain ordered layout positions for Turn lookup without @@ -101,6 +112,56 @@ line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px)); } +.running { + display: flex; + align-items: center; + justify-content: center; + column-gap: 16px; + --dsh-chat-flow-gap: 32px; + --dsw-alias-label-shimmer: var(--dsw-alias-label-deep-diving-shimmer); + color: var(--dsw-alias-label-deep-diving); + font-size: calc(var(--dsh-content-font-size, 14px) - 2px); + line-height: calc(22px + var(--dsh-content-font-delta, 0px)); +} + +.running::before, +.running::after { + content: ''; + flex: 0 0 32px; + border-top: 0.5px solid var(--dsw-alias-border-l3); +} + +.scroll:has(.running) { + padding-bottom: 24px; +} + +.runningContent { + display: inline-flex; + align-items: center; + min-width: 0; + gap: 6px; +} + +.runningText { + min-width: 0; +} + +.runningNumber { + font-family: var(--ds-font-family-code); + font-variant-numeric: tabular-nums; +} + +.runningIcon { + display: inline-flex; + flex: none; + width: calc(14px + var(--dsh-content-font-delta, 0px)); + height: calc(14px + var(--dsh-content-font-delta, 0px)); +} + +.runningIcon path[stroke] { + stroke: currentColor; +} + .openError { color: var(--dsw-alias-state-error-primary); font-size: var(--dsh-content-font-size-secondary, 13px); diff --git a/packages/client/ui-chat/src/client/chat/ChatView.tsx b/packages/client/ui-chat/src/client/chat/ChatView.tsx index 0514068aed..49d69a729e 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.tsx +++ b/packages/client/ui-chat/src/client/chat/ChatView.tsx @@ -18,6 +18,7 @@ import { ChatGroupSeat } from './ChatGroupSeat.tsx' import { chatRenderKey } from './render-entry.ts' import { assertNever } from '@deepseek-ai/dsh-util-values' import { TurnNavigator } from './TurnNavigator.tsx' +import { RunningStatus } from './RunningStatus.tsx' import { mergeTurnRailItems } from './turn-rail-items.ts' import { useChatScroll } from './use-chat-scroll.ts' import { fileMediaUrl, resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path' @@ -269,6 +270,7 @@ export function ChatView({ t={t} /> + {running && } {/* No pending placeholders: questions (ui-user-questions) and approvals (ApprovalPanel) both take over the composer, so a flow card would double-render the same wait. */} diff --git a/packages/client/ui-chat/src/client/chat/GenericCommandCard.module.css b/packages/client/ui-chat/src/client/chat/GenericCommandCard.module.css index a4bb6ef0cc..e05c9ff13c 100644 --- a/packages/client/ui-chat/src/client/chat/GenericCommandCard.module.css +++ b/packages/client/ui-chat/src/client/chat/GenericCommandCard.module.css @@ -7,13 +7,8 @@ flex-shrink: 0; } -.chevron { - color: var(--dsw-alias-label-secondary); -} - .title { font-weight: 400; - transition: color 100ms ease; } .separator { @@ -29,17 +24,10 @@ min-width: 0; overflow: hidden; flex: 1 1 auto; - color: var(--dsw-alias-label-tertiary); font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); text-overflow: ellipsis; white-space: nowrap; - transition: color 100ms ease; -} - -.row:hover .title, -.row:hover .summary:not([data-error]) { - color: var(--dsw-alias-label-primary); } .summary[data-error], diff --git a/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx b/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx index 0b0749881b..769e4977f4 100644 --- a/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx +++ b/packages/client/ui-chat/src/client/chat/GenericCommandCard.tsx @@ -1,6 +1,6 @@ import { memo, useCallback, useMemo, useState } from 'react' import type { ChatViewSlotProps, CommandRowOwnerProps } from '../contract/slots.ts' -import { DisclosureRow, IconApiOutlineRegular, TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives' +import { DisclosureRow, IconApiOutlineRegular, ShimmerText } from '@deepseek-ai/dsh-client-ui-primitives' import a11yCss from './accessibility.module.css' import css from './GenericCommandCard.module.css' @@ -42,12 +42,12 @@ export const GenericCommandCard = memo(function GenericCommandCard({ node, t, ru const toggle = useCallback(() => { setExpanded(value => !value) }, []) const collapsedContent = useMemo(() => ( <> - + - {summary} + {summary} - ), [running, state, summary]) + ), [state, summary]) const content = useMemo(() => open ?
{body}
: undefined, [body, open, state]) @@ -56,10 +56,8 @@ export const GenericCommandCard = memo(function GenericCommandCard({ node, t, ru {state === 'running' && {t('row.running')}} {state === 'error' && {t('row.failed')}} - - {t('message.retry.status', { label, retry: node.retry, maximum, seconds })} + + {status}
diff --git a/packages/client/ui-chat/src/client/chat/ReasoningRow.module.css b/packages/client/ui-chat/src/client/chat/ReasoningRow.module.css index f59c04e404..402a9effc1 100644 --- a/packages/client/ui-chat/src/client/chat/ReasoningRow.module.css +++ b/packages/client/ui-chat/src/client/chat/ReasoningRow.module.css @@ -9,7 +9,6 @@ } .row { - position: relative; overflow: hidden; } @@ -22,35 +21,10 @@ background: var(--dsw-alias-bg-base); } -.root[data-state='running'] .row::after { - content: ''; - position: absolute; - inset-block: 0; - left: 0; - width: 300px; - background: linear-gradient( - 90deg, - transparent 0%, - color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, - transparent 100% - ); - animation: dsh-reasoning-row-sweep 2.6s ease-out infinite; - pointer-events: none; -} - -@keyframes dsh-reasoning-row-sweep { - 0% { left: -300px; } - 90%, 100% { left: 100%; } -} - .leading { flex-shrink: 0; } -.chevron { - color: var(--dsw-alias-label-secondary); -} - .title { font-weight: 400; } @@ -71,7 +45,6 @@ min-width: 0; overflow: hidden; flex: 1 1 auto; - color: var(--dsw-alias-label-tertiary); font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px)); white-space: nowrap; @@ -102,9 +75,3 @@ padding: 4px 0 4px calc(22px + var(--dsh-content-font-delta, 0px)); min-width: 0; } - -@media (prefers-reduced-motion: reduce) { - .root[data-state='running'] .row::after { - animation: none; - } -} diff --git a/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx b/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx index 74a801d6dd..dd27a47b5c 100644 --- a/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx +++ b/packages/client/ui-chat/src/client/chat/ReasoningRow.tsx @@ -1,6 +1,6 @@ /** Assistant reasoning disclosure, independent of Tool-call presentation. */ import { memo, useMemo } from 'react' -import { DisclosureRow, IconThinkOutlineRegular, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives' +import { DisclosureRow, IconThinkOutlineRegular, MarkdownText, ShimmerText } from '@deepseek-ai/dsh-client-ui-primitives' import type { ChatViewSlotProps, UseDisclosure, UsePresentation } from '../contract/slots.ts' import { markdownLabels } from '../markdown-labels.ts' import a11yCss from './accessibility.module.css' @@ -59,9 +59,11 @@ export const ReasoningRow = memo(function ReasoningRow({ text, running, usePrese && (running || policy.settledReasoningPreview)) const collapsedContent = useMemo(() => ( <> - + - {summary} + + {summary} + ), [running, summary]) @@ -84,9 +86,9 @@ export const ReasoningRow = memo(function ReasoningRow({ text, running, usePrese rowClassName={css.row} leadingClassName={css.leading} titleClassName={css.title} - chevronClassName={css.chevron} icon={THINK_ICON} title={t('message.think')} + running={running} open={expanded} expandable expandOnRowClick diff --git a/packages/client/ui-chat/src/client/chat/RunningStatus.tsx b/packages/client/ui-chat/src/client/chat/RunningStatus.tsx new file mode 100644 index 0000000000..a76c7210b3 --- /dev/null +++ b/packages/client/ui-chat/src/client/chat/RunningStatus.tsx @@ -0,0 +1,82 @@ +/** Running Turn clock isolated from the transcript's render cycle. */ +import { memo, useEffect, useRef, useState } from 'react' +import { Shimmer, ShimmerText } from '@deepseek-ai/dsh-client-ui-primitives' +import lottie from 'lottie-web/build/player/lottie_light.js' +import type { ChatViewSlotProps } from '../contract/slots.ts' +import { formatLiveRunDuration, LIVE_RUN_CLOCK_INTERVAL_MS } from './message-chrome.ts' +import whaleTail from './whale-tail.lottie.json' with { type: 'json' } +import a11yCss from './accessibility.module.css' +import css from './ChatView.module.css' + +/** + * Show live elapsed time after the current Turn's content without announcing ticks. + * @param props - Chat timeline selector and localized copy. + * @returns the blue running indicator; mount only while the Session is running. + */ +export const RunningStatus = memo(function RunningStatus({ useChat, t }: Pick) { + const startTime = useChat(({ timeline }) => { + const latest = timeline.turnOrder.at(-1) + const turn = latest === undefined ? undefined : timeline.turns.get(latest) + return turn?.status === 'open' ? turn.start?.time : undefined + }) + const [now, setNow] = useState(Date.now) + useEffect(() => { + if (startTime === undefined) return + setNow(Date.now()) + const timer = setInterval(() => { setNow(Date.now()) }, LIVE_RUN_CLOCK_INTERVAL_MS) + return () => { clearInterval(timer) } + }, [startTime]) + const label = startTime === undefined ? t('chat.deepDiving') : t('chat.deepDivingFor', { + duration: formatLiveRunDuration(Math.max(1000, now - startTime), t), + }) + return ( +
+ {t('chat.deepDiving')} + + + + + {label.split(/(\d+)/).map((part, index) => ( + {part} + ))} + + + +
+ ) +}) + +/** Loop once per two text sweeps (3s), with a 0.3s delay; hold the initial pose for reduced motion. */ +function RunningWhaleTail() { + const container = useRef(null) + useEffect(() => { + if (container.current === null) return + const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)') + const animation = lottie.loadAnimation({ + container: container.current, + renderer: 'svg', + loop: true, + autoplay: false, + animationData: structuredClone(whaleTail), + rendererSettings: { focusable: false }, + }) + let playTimer: number | undefined + const syncPlayback = () => { + window.clearTimeout(playTimer) + animation.goToAndStop(0, true) + if (!reducedMotion.matches) { + playTimer = window.setTimeout(() => { animation.play() }, 300) + } + } + animation.addEventListener('DOMLoaded', syncPlayback) + reducedMotion.addEventListener('change', syncPlayback) + syncPlayback() + return () => { + window.clearTimeout(playTimer) + reducedMotion.removeEventListener('change', syncPlayback) + animation.removeEventListener('DOMLoaded', syncPlayback) + animation.destroy() + } + }, []) + return )} - {title} - {(keepContentWhenOpen || !open) && collapsedContent} + + {title} + {(keepContentWhenOpen || !open) && collapsedContent} +
{open && children} diff --git a/packages/client/ui-primitives/src/Shimmer.module.css b/packages/client/ui-primitives/src/Shimmer.module.css new file mode 100644 index 0000000000..5131ecd653 --- /dev/null +++ b/packages/client/ui-primitives/src/Shimmer.module.css @@ -0,0 +1,88 @@ +.root { + position: relative; + display: inline-grid; + width: max-content; + max-width: 100%; + min-width: 0; + vertical-align: top; + color: inherit; +} + +.content { + display: flex; + align-items: center; + min-width: 0; +} + +.text { + color: inherit; +} + +.text[data-shimmer-text]::after { + content: attr(data-shimmer-text); +} + +.decoration { + position: absolute; + inset: 0; + /* The moving mask must not enlarge any ancestor's scrollable area. */ + overflow: clip; + pointer-events: none; + user-select: none; +} + +.sweep { + position: absolute; + inset: 0; + overflow: hidden; + color: var(--dsw-alias-label-shimmer); + mask-image: linear-gradient(105deg, transparent 0%, black 40% 60%, transparent 100%); + transform: translateX(-100%); + animation-name: dsh-row-shimmer-sweep; +} + +/* Semantic base colors stay on the real content; the overlay supplies one tint. */ +.sweep * { + color: inherit !important; +} + +.sweep [data-shimmer-decoration] { + background: currentColor; +} + +/* Opposite transforms keep every glyph aligned while their shared mask moves. */ +.highlight { + width: 100%; + height: 100%; + transform: translateX(100%); + animation-name: dsh-row-shimmer-highlight; +} + +.sweep, +.highlight { + animation-duration: 1.5s; + animation-delay: 0.3s; + animation-timing-function: steps(48, end); + animation-iteration-count: infinite; +} + +@keyframes dsh-row-shimmer-sweep { + 0% { transform: translateX(-100%); } + 66.6667%, 100% { transform: translateX(100%); } +} + +@keyframes dsh-row-shimmer-highlight { + 0% { transform: translateX(100%); } + 66.6667%, 100% { transform: translateX(-100%); } +} + +@media (prefers-reduced-motion: reduce) { + .decoration { + display: none; + } + + .sweep, + .highlight { + animation: none; + } +} diff --git a/packages/client/ui-primitives/src/Shimmer.tsx b/packages/client/ui-primitives/src/Shimmer.tsx new file mode 100644 index 0000000000..3c6e02b40d --- /dev/null +++ b/packages/client/ui-primitives/src/Shimmer.tsx @@ -0,0 +1,66 @@ +/** One activity highlight across a row's text and separators. */ +import { createContext, memo, useContext, type ReactNode } from 'react' +import clsx from 'clsx' +import css from './Shimmer.module.css' + +const DecorativeCopy = createContext(false) + +/** Presentational content shared by the visible row and its inert highlight. */ +export interface ShimmerProps { + /** Text and layout elements; keep icons outside and use ShimmerText for text. No effects or element ids. */ + children: ReactNode + /** Whether the row's owning operation is running. */ + active: boolean + /** Additional class applied to the retained wrapper. */ + className?: string | undefined + /** Layout class applied equally to the base and decorative content. */ + contentClassName?: string | undefined +} + +/** + * Sweep one highlight across the visible width of a row's text; place icons outside. + * Children also render in an inert, aria-hidden decoration while active; keep + * state and subscriptions above this component and supply presentational nodes. + * The decoration is clipped to the row without extending its scrollable area. + * @param props - row content, activity, and layout classes. + * @returns the stable base row and optional masked decoration. + */ +export const Shimmer = memo(function Shimmer({ children, active, className, contentClassName }: ShimmerProps) { + return ( + + {children} + {/* React 18 emits inert as a string attribute; React 19 requires boolean true. */} + {active && ( + + )} + + ) +}) + +/** Text rendered once for selection and accessibility, with generated decoration. */ +export interface ShimmerTextProps { + /** Visible, already-localized text. */ + children: string + /** Typography and truncation class applied to both copies. */ + className?: string | undefined +} + +/** + * Render row text without duplicating its selectable or accessible content. + * @param props - text and owner styling. + * @returns text in the base row or generated text in its decoration. + */ +export const ShimmerText = memo(function ShimmerText({ children, className }: ShimmerTextProps) { + const decorative = useContext(DecorativeCopy) + return ( + + {decorative ? null : children} + + ) +}) diff --git a/packages/client/ui-primitives/src/TextShimmer.module.css b/packages/client/ui-primitives/src/TextShimmer.module.css deleted file mode 100644 index bdc2de9a44..0000000000 --- a/packages/client/ui-primitives/src/TextShimmer.module.css +++ /dev/null @@ -1,27 +0,0 @@ -.root[data-text-shimmer] { - background-image: linear-gradient( - 90deg, - currentColor calc(50% - var(--dsh-text-shimmer-spread)), - color-mix(in oklab, currentColor 50%, transparent), - currentColor calc(50% + var(--dsh-text-shimmer-spread)) - ); - background-position: 100% center; - background-repeat: no-repeat; - background-size: 250% 100%; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - animation: dsh-text-shimmer 1.5s cubic-bezier(0.33, 0, 0.67, 1) infinite; -} - -@keyframes dsh-text-shimmer { - 66.6667%, 100% { background-position: 0% center; } -} - -@media (prefers-reduced-motion: reduce) { - .root[data-text-shimmer] { - background-image: none; - -webkit-text-fill-color: currentColor; - animation: none; - } -} diff --git a/packages/client/ui-primitives/src/TextShimmer.tsx b/packages/client/ui-primitives/src/TextShimmer.tsx deleted file mode 100644 index 8249626435..0000000000 --- a/packages/client/ui-primitives/src/TextShimmer.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** Text-only activity animation with a stable span across lifecycle changes. */ -import { memo, useMemo, type CSSProperties } from 'react' -import clsx from 'clsx' -import css from './TextShimmer.module.css' - -/** Text and activity supplied by the owning row. */ -export interface TextShimmerProps { - children: string - active: boolean - className?: string | undefined -} - -/** - * Render text with an optional moving highlight; inactive text keeps the same node. - * @param props - localized text, running state, and owner styling. - * @returns the retained text span. - */ -export const TextShimmer = memo(function TextShimmer({ children, active, className }: TextShimmerProps) { - const style = useMemo(() => ({ - '--dsh-text-shimmer-spread': `${children.length * 8}px`, - } as CSSProperties), [children.length]) - return {children} -}) diff --git a/packages/client/ui-primitives/src/icons/index.tsx b/packages/client/ui-primitives/src/icons/index.tsx index 1629d28a35..cf7a39c381 100644 --- a/packages/client/ui-primitives/src/icons/index.tsx +++ b/packages/client/ui-primitives/src/icons/index.tsx @@ -449,9 +449,9 @@ export const IconEditOutlineMedium = (props: IconProps) => ( const IconThinkOutlineArtwork = ({ size = 16, className, strokeWidth }: WeightedIconProps) => ( ) diff --git a/packages/client/ui-primitives/src/index.ts b/packages/client/ui-primitives/src/index.ts index ab82a9f6a5..d8f4e1928f 100644 --- a/packages/client/ui-primitives/src/index.ts +++ b/packages/client/ui-primitives/src/index.ts @@ -7,8 +7,8 @@ export { StateDot } from './StateDot.tsx' export type { StateDotState } from './StateDot.tsx' export { DisclosureRow } from './DisclosureRow.tsx' export type { DisclosureRowProps } from './DisclosureRow.tsx' -export { TextShimmer } from './TextShimmer.tsx' -export type { TextShimmerProps } from './TextShimmer.tsx' +export { Shimmer, ShimmerText } from './Shimmer.tsx' +export type { ShimmerProps, ShimmerTextProps } from './Shimmer.tsx' export { Button } from './Button.tsx' export type { ButtonVariant } from './Button.tsx' export { Pill } from './Pill.tsx' diff --git a/packages/client/ui-primitives/tests/shimmer.client.spec.tsx b/packages/client/ui-primitives/tests/shimmer.client.spec.tsx new file mode 100644 index 0000000000..d2afcf0e71 --- /dev/null +++ b/packages/client/ui-primitives/tests/shimmer.client.spec.tsx @@ -0,0 +1,41 @@ +// @vitest-environment jsdom +import { cleanup, fireEvent, render } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { Shimmer, ShimmerText } from '../src/Shimmer.tsx' + +afterEach(cleanup) + +describe('Shimmer', () => { + it('retains real text and controls while one inert decoration covers icons and text', () => { + const open = vi.fn() + const row = (active: boolean, title: string) => ( + + + {title} + + + ) + const view = render(row(true, 'Read')) + const title = view.getByText('Read') + const button = view.getByRole('button', { name: 'file.ts' }) + const decoration = view.container.querySelector('[inert]')! + expect(decoration.getAttribute('aria-hidden')).toBe('true') + expect(decoration.querySelector('svg')).not.toBeNull() + expect([...decoration.querySelectorAll('[data-shimmer-text]')].map(node => node.getAttribute('data-shimmer-text'))) + .toEqual(['Read', 'file.ts']) + expect(view.container.textContent).toBe('Readfile.ts') + fireEvent.click(button) + expect(open).toHaveBeenCalledOnce() + + view.rerender(row(true, 'Reading')) + expect(view.getByText('Reading')).toBe(title) + expect(view.getByRole('button', { name: 'file.ts' })).toBe(button) + expect(view.container.querySelector('[inert]')).toBe(decoration) + + view.rerender(row(false, 'Read')) + expect(view.getByText('Read')).toBe(title) + expect(view.getByRole('button', { name: 'file.ts' })).toBe(button) + expect(view.container.querySelector('[inert]')).toBeNull() + expect(view.container.querySelectorAll('svg')).toHaveLength(1) + }) +}) diff --git a/packages/client/ui-primitives/tests/text-shimmer-styles.client.spec.ts b/packages/client/ui-primitives/tests/text-shimmer-styles.client.spec.ts deleted file mode 100644 index b416a1705f..0000000000 --- a/packages/client/ui-primitives/tests/text-shimmer-styles.client.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** CSS activity and reduced-motion rules that jsdom does not execute. */ -import { readFileSync } from 'node:fs' -import { describe, expect, it } from 'vitest' - -const css = readFileSync(new URL('../src/TextShimmer.module.css', import.meta.url), 'utf8') - -describe('TextShimmer styles', () => { - it('animates active text and disables motion and transparent fill for reduced motion', () => { - expect(css).toMatch(/\.root\[data-text-shimmer\]\s*\{[^}]*animation: dsh-text-shimmer/s) - expect(css).toMatch(/@keyframes dsh-text-shimmer\s*\{[^}]*background-position: 0% center/s) - const reduced = /@media \(prefers-reduced-motion: reduce\)\s*\{\s*\.root\[data-text-shimmer\]\s*\{([^}]+)\}/.exec(css)?.[1] - expect(reduced).toContain('background-image: none;') - expect(reduced).toContain('-webkit-text-fill-color: currentColor;') - expect(reduced).toContain('animation: none;') - }) -}) diff --git a/packages/client/ui-primitives/tests/text-shimmer.client.spec.tsx b/packages/client/ui-primitives/tests/text-shimmer.client.spec.tsx deleted file mode 100644 index 5f6efdf805..0000000000 --- a/packages/client/ui-primitives/tests/text-shimmer.client.spec.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// @vitest-environment jsdom -import { cleanup, render } from '@testing-library/react' -import { afterEach, describe, expect, it } from 'vitest' -import { TextShimmer } from '../src/TextShimmer.tsx' - -afterEach(cleanup) - -describe('TextShimmer', () => { - it('retains its text span across activity, text, and owner-class changes', () => { - const view = render(Run) - const span = view.getByText('Run') - expect(span.tagName).toBe('SPAN') - expect(span.dataset.textShimmer).toBe('true') - expect(span.classList.contains('title')).toBe(true) - expect(span.style.getPropertyValue('--dsh-text-shimmer-spread')).toBe('24px') - - view.rerender(End) - expect(view.getByText('End')).toBe(span) - expect(span.hasAttribute('data-text-shimmer')).toBe(false) - expect(span.classList.contains('title')).toBe(false) - expect(span.style.getPropertyValue('--dsh-text-shimmer-spread')).toBe('24px') - - view.rerender(Running) - expect(view.getByText('Running')).toBe(span) - expect(span.dataset.textShimmer).toBe('true') - expect(span.style.getPropertyValue('--dsh-text-shimmer-spread')).toBe('56px') - }) - -}) diff --git a/packages/client/ui-skill/README.i18n.yaml b/packages/client/ui-skill/README.i18n.yaml index c0203ad861..784ab58cab 100644 --- a/packages/client/ui-skill/README.i18n.yaml +++ b/packages/client/ui-skill/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-skill/README.md -README.md: 0234ce8fb6c2737ea242ac5590e471c3e4c91834 -README.zh.md: 5b039ec1c5bea818e6396f4896c05b8175beb08a +README.md: 9e9bc11f76bce8159e9f63a011c686c62db39776 +README.zh.md: 93d0f1ce318c404fcff512168d6092b21b91743e diff --git a/packages/client/ui-skill/README.md b/packages/client/ui-skill/README.md index 0234ce8fb6..9e9bc11f76 100644 --- a/packages/client/ui-skill/README.md +++ b/packages/client/ui-skill/README.md @@ -35,7 +35,7 @@ Ordinary-session candidates come from the `skills/list` Remote; the host serves The `preparing` stage shows only the skill icon and title, with no requested skill name or expansion. The `start` and `result` stages use the recorded arguments and output described below; preparation does not consult the installed catalog. -A collapsed row renders the skill glyph, `Skill` title, and requested skill name on the same secondary font-size axis, line height, row height, and glyph scaling as ordinary Tool rows. Every lifecycle state retains the ordinary skill glyph; running calls carry the transcript shimmer, while failures replace the name with the first error line and interruptions retain their explicit status text. A settled row expands into a bounded `Instructions` card containing the exact durable tool output, with the standard trajectory `Inspect` affordance when available; this expanded card keeps its own typography. The row derives its name, lifecycle, and body only from the frozen call/result slice supplied by ui-tool, never from the current catalog, so replay stays stable when installed skills or their descriptions change. +A collapsed row renders the skill glyph, `Skill` title, and requested skill name on the same secondary font-size axis, line height, row height, and glyph scaling as ordinary Tool rows. The header follows the shared [process-row text and icon colors](../ui-primitives/README.md#component-catalog). Every lifecycle state retains the ordinary skill glyph; running calls share one left-to-right shimmer across the title, separator, and skill name, while failures replace the name with the first error line and interruptions retain their explicit status text. A settled row expands into a bounded `Instructions` card containing the exact durable tool output, with the standard trajectory `Inspect` affordance when available; this expanded card keeps its own typography. The row derives its name, lifecycle, and body only from the frozen call/result slice supplied by ui-tool, never from the current catalog, so replay stays stable when installed skills or their descriptions change. Hovering over `/name` highlights the entire reference. Clicking a known skill opens its provider-supplied `SKILL.md` path in the right Sidebar while keeping the token editable. An uncached click shares the per-Session catalog fetch and opens when it completes, retaining the clicked Session address. Preset changes, connection resets, and plugin disposal cancel pending previews; a later click fetches the current catalog again. Skills without a file path remain invocable but have no file preview. diff --git a/packages/client/ui-skill/README.zh.md b/packages/client/ui-skill/README.zh.md index 5b039ec1c5..93d0f1ce31 100644 --- a/packages/client/ui-skill/README.zh.md +++ b/packages/client/ui-skill/README.zh.md @@ -35,7 +35,7 @@ kind: "package-reference" `preparing` 阶段只显示技能图标与标题,不显示请求加载的技能名称,也不能展开。`start` 和 `result` 阶段使用下述记录参数与输出;准备阶段不查询已安装目录。 -收起的行显示 skill 图标、`Skill` 标题与请求加载的 skill 名称,并与普通工具行使用相同的 secondary 字号轴、行高、行高度和图标缩放。所有生命周期状态都保留普通 skill 图标;运行中的调用带有 transcript(文本记录)的扫光效果,失败时以错误首行替换名称,中断则保留明确状态文本。已结算的行展开为一个尺寸受限的 `Instructions` 卡片,其中原样呈现持久化的工具输出;可用时还会提供标准轨迹的 `Inspect` 入口,展开卡片保留自身排版。该行的名称、生命周期与正文只派生自 ui-tool 提供的冻结调用/结果切片,绝不读取当前目录,因此即使已安装的 skill 或其描述发生变化,回放仍保持稳定。 +收起的行显示 skill 图标、`Skill` 标题与请求加载的 skill 名称,并与普通工具行使用相同的 secondary 字号轴、行高、行高度和图标缩放。标题行遵循共享的[过程行文字与图标颜色](../ui-primitives/README.zh.md#component-catalog)。所有生命周期状态都保留普通 skill 图标;运行中的调用以同一道高光从左到右扫过标题、分隔符与 skill 名称,失败时以错误首行替换名称,中断则保留明确状态文本。已结算的行展开为一个尺寸受限的 `Instructions` 卡片,其中原样呈现持久化的工具输出;可用时还会提供标准轨迹的 `Inspect` 入口,展开卡片保留自身排版。该行的名称、生命周期与正文只派生自 ui-tool 提供的冻结调用/结果切片,绝不读取当前目录,因此即使已安装的 skill 或其描述发生变化,回放仍保持稳定。 鼠标移入 `/name` 时,背景覆盖整个引用。点击已知 skill 会在右侧栏打开提供方给出的 `SKILL.md` 路径,同时保留文本的可编辑性。缓存未就绪时,点击复用该 Session 的目录请求,在完成后按点击时的 Session 地址打开预览。切换预设、重置连接和插件释放会取消待处理的预览;后续点击重新获取当前目录。没有文件路径的 skill 仍可调用,但没有文件预览。 diff --git a/packages/client/ui-skill/src/client/SkillRow.module.css b/packages/client/ui-skill/src/client/SkillRow.module.css index 9fad40a2d0..39a73a614c 100644 --- a/packages/client/ui-skill/src/client/SkillRow.module.css +++ b/packages/client/ui-skill/src/client/SkillRow.module.css @@ -12,33 +12,18 @@ align-items: center; height: calc(24px + var(--dsh-content-font-delta, 0px)); min-width: 0; + color: var(--dsw-alias-label-tertiary); + transition: color 100ms ease; +} + +.row:hover { + color: var(--dsw-alias-label-secondary); } .row[data-expandable] { cursor: pointer; } -.card[data-state='running'] .row::after, -.card[data-state='preparing'] .row::after { - content: ''; - position: absolute; - inset: 0 auto 0 0; - width: 300px; - background: linear-gradient( - 90deg, - transparent 0%, - color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, - transparent 100% - ); - animation: dsh-skill-row-sweep 2.6s ease-out infinite; - pointer-events: none; -} - -@keyframes dsh-skill-row-sweep { - 0% { left: -300px; } - 90%, 100% { left: 100%; } -} - .leading { position: relative; flex: none; @@ -48,7 +33,7 @@ align-items: center; justify-content: center; margin-right: 6px; - color: var(--dsw-alias-label-tertiary); + color: inherit; } .leading svg { @@ -57,7 +42,7 @@ } .chevron { - color: var(--dsw-alias-label-secondary); + color: inherit; } .iconIdle { @@ -86,7 +71,6 @@ flex: none; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); - color: var(--dsw-alias-label-secondary); } .separator { @@ -106,7 +90,6 @@ white-space: nowrap; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); - color: var(--dsw-alias-label-tertiary); } .errorSummary { @@ -210,11 +193,7 @@ } @media (prefers-reduced-motion: reduce) { - .card[data-state='running'] .row::after { - animation: none; - display: none; - } - + .row, .iconIdle, .chevronHover, .inspectButton { diff --git a/packages/client/ui-skill/src/client/SkillRow.tsx b/packages/client/ui-skill/src/client/SkillRow.tsx index 0800bc7812..1deb1b64b2 100644 --- a/packages/client/ui-skill/src/client/SkillRow.tsx +++ b/packages/client/ui-skill/src/client/SkillRow.tsx @@ -1,6 +1,6 @@ import { useState, type KeyboardEvent, type ReactNode } from 'react' import { - IconChevronDownOutlineRegular, IconInspectOutlineRegular, IconSkillOutlineRegular, + IconChevronDownOutlineRegular, IconInspectOutlineRegular, IconSkillOutlineRegular, Shimmer, ShimmerText, } from '@deepseek-ai/dsh-client-ui-primitives' import type { StartedToolCallViewProps, ToolCallViewProps } from '@deepseek-ai/dsh-client-ui-tool/client' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' @@ -85,7 +85,7 @@ function disclosureLeading(open: boolean, expandable: boolean): ReactNode { ) } -/** Visually hidden state copy for the color-only running sweep and error tone. */ +/** Accessible running and failure status. */ function stateStatus(state: SkillRowState, t: SkillRowProps['t']): string | null { switch (state) { case 'running': return t('row.running') @@ -95,7 +95,7 @@ function stateStatus(state: SkillRowState, t: SkillRowProps['t']): string | null } /** - * Render one `skill` tool call as an accent summary and instructions disclosure. + * Render one `skill` tool call as a process row and instructions disclosure. * @param props - keyed toolview payload plus the skill locale seat. * @returns the dedicated skill row. */ @@ -104,7 +104,9 @@ export function SkillRow(props: SkillRowProps) {
{props.t('row.preparing')} - {props.t('row.title')} + + {props.t('row.title')} +
return @@ -116,6 +118,7 @@ function StartedSkillRow({ block, inspect, t }: Exclude { setExpanded(value => !value) @@ -140,16 +143,18 @@ function StartedSkillRow({ block, inspect, t }: Exclude - {leading} {status !== null ? {status} : null} - {t('row.title')} - - - {summary} - + {leading} + + {t('row.title')} + + + {summary} + + {open ? (
diff --git a/packages/client/ui-skill/tests/skill-row.client.spec.tsx b/packages/client/ui-skill/tests/skill-row.client.spec.tsx index a34b8ff4e9..34b143d0ef 100644 --- a/packages/client/ui-skill/tests/skill-row.client.spec.tsx +++ b/packages/client/ui-skill/tests/skill-row.client.spec.tsx @@ -110,7 +110,7 @@ describe('SkillRow', () => { }))} />) const row = screen.getByRole('button', { name: 'skill 加载失败加载技能SkillError: missing resource' }) expect(view.container.querySelector('[data-tool="skill"]')?.getAttribute('data-state')).toBe('error') - expect(view.container.querySelector('[data-tool="skill"] > div > span:first-child svg')).not.toBeNull() + expect(view.container.querySelector('[data-tool="skill"] [role="button"] svg')).not.toBeNull() expect(view.container.querySelector('[data-tool="skill"] [data-state]')).toBeNull() expect(row.textContent).not.toContain('Check SKILL.md.') fireEvent.click(row) @@ -124,8 +124,8 @@ describe('SkillRow', () => { error: { name: 'InterruptedError', code: 'interrupted' }, }))} />) const stoppedSummary = stoppedView.getByText('skill 加载已中止') - expect(stoppedSummary.className).toContain('stoppedSummary') - expect(stoppedView.container.querySelector('[data-tool="skill"] > div > span:first-child svg')).not.toBeNull() + expect(stoppedSummary.parentElement?.className).toContain('stoppedSummary') + expect(stoppedView.container.querySelector('[data-tool="skill"] [role="button"] svg')).not.toBeNull() expect(stoppedView.container.querySelector('[data-tool="skill"] [data-state]')).toBeNull() cleanup() diff --git a/packages/client/ui-theme/README.i18n.yaml b/packages/client/ui-theme/README.i18n.yaml index 046b137c88..6361eb4b7b 100644 --- a/packages/client/ui-theme/README.i18n.yaml +++ b/packages/client/ui-theme/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-theme/README.md -README.md: 7248b7230afa28f817bec7c0eb26fd9512fff405 -README.zh.md: 6dc1db064dd9a6754f6c6475dde18ccf1a17841e +README.md: a2938f0c03d7f6b2c0202e67be1b66dff08516f3 +README.zh.md: d19649e26722f506c3142a095eb82b0a08235626 diff --git a/packages/client/ui-theme/README.md b/packages/client/ui-theme/README.md index 7248b7230a..a2938f0c03 100644 --- a/packages/client/ui-theme/README.md +++ b/packages/client/ui-theme/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-client-ui-theme` lets Web GUI users choose `light`, `dark`, or `system` and set conversation content text from 12 to 17 px in Settings. A loopback client stores both values in the `ui-theme` settings namespace, which the local provider persists in `$DSH_HOME/cordis.patch.yml` by default. The plugin resolves `system` through `prefers-color-scheme` and publishes immutable `ThemeSnapshot`s; ui-layout applies each snapshot to the document. The package also ships the `--dsw-*` token stylesheets and injects a synchronous bootstrap so the selected palette and font size apply before the shell loads. Third-party themes can register alias-token overrides through `ctx.theme`. +`dsh-client-ui-theme` lets Web GUI users choose `light`, `dark`, or `system` and set conversation content text from 12 to 24 px in Settings. A loopback client stores both values in the `ui-theme` settings namespace, which the local provider persists in `$DSH_HOME/cordis.patch.yml` by default. The plugin resolves `system` through `prefers-color-scheme` and publishes immutable `ThemeSnapshot`s; ui-layout applies each snapshot to the document. The package also ships the `--dsw-*` token stylesheets and injects a synchronous bootstrap so the selected palette and font size apply before the shell loads. Third-party themes can register alias-token overrides through `ctx.theme`. ## Table of Contents @@ -29,7 +29,7 @@ Users switch the color scheme and content font size from two rows in Settings (G ### Appearance and font size -The plugin registers Appearance preference cubes and a font-size stepper in the General section. The stepper accepts integer values from 12 through 17 px and defaults to 14 px. It changes conversation headings and base text by the same increment, including the user bubble and composer draft; flow-row titles, summaries, and tables follow one step under the body size, while small text and code keep fixed sizes. Each accepted change writes through the Host settings API. Rapid changes serialize in gesture order with namespace revisions, and a rejected latest write reloads the durable values. Non-loopback pages keep both choices process-local. +The plugin registers Appearance preference cubes and a font-size stepper in the General section. The stepper accepts integer values from 12 through 24 px and defaults to 14 px. It changes conversation headings and base text by the same increment, including the user bubble and composer draft; flow-row titles, summaries, and tables follow one step under the body size, while small text and code keep fixed sizes. Each accepted change writes through the Host settings API. Rapid changes serialize in gesture order with namespace revisions, and a rejected latest write reloads the durable values. Non-loopback pages keep both choices process-local. ### Registering a theme @@ -51,7 +51,9 @@ The service owns theme and font-size state and publishes snapshots. The ui-layou ### Stylesheets -`src/styles/` holds six sheets imported in order by ui-theme's dynamic client entry: `base.css`, `corner-shape.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` consumes the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them. Status marks use their own semantic state tokens. `design-platform.css` also owns the code-diff fill aliases and their static alpha palette entries, plus the `--dsw-alias-file-diff-*` code, gutter, and marker palette for file comparisons; `shiki.css` owns syntax colors. +`src/styles/` holds six sheets imported in order by ui-theme's dynamic client entry: `base.css`, `corner-shape.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. The client bundle compiles and injects them as plugin-owned global styles, so unload and HMR remove them with ui-theme. `scrollbar.css` consumes the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them. Status marks use their own semantic state tokens. `design-platform.css` also owns the code-diff fill aliases and their static alpha palette entries, plus the `--dsw-alias-file-diff-*` code, gutter, and marker palette for file comparisons; `shiki.css` owns syntax colors. `--dsw-alias-label-shimmer` supplies an overlay for the shared text shimmer: black at 40% alpha in the light palette and white at 45% alpha in the dark palette. `--dsw-alias-label-deep-diving` and `--dsw-alias-label-deep-diving-shimmer` supply the blue activity label and sweep; the dark palette uses a lighter, less saturated label with a brighter blue sweep. + +The `--dsw-alias-turn-trigger-*` tokens provide separate resting and hover backgrounds for Turn-trigger notices in each palette. `brand-font.css` exports the local Montserrat Light, Regular and Medium faces (normal style, weights 300, 400 and 500), with `montserrat-light.woff2`, `montserrat-regular.woff2`, `montserrat-medium.woff2` and its SIL Open Font License in `lib/styles/`. Desktop bundles the same stylesheet, font and license for offline welcome brand text; ordinary UI keeps its system font stack. diff --git a/packages/client/ui-theme/README.zh.md b/packages/client/ui-theme/README.zh.md index 6dc1db064d..d19649e267 100644 --- a/packages/client/ui-theme/README.zh.md +++ b/packages/client/ui-theme/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -`dsh-client-ui-theme` 让 Web GUI 用户在设置中选择 `light`、`dark` 或 `system`,并把会话正文字号设为 12 至 17 px。回环客户端把两个值存入 `ui-theme` 设置命名空间,本地提供方默认将其持久化到 `$DSH_HOME/cordis.patch.yml`。插件通过 `prefers-color-scheme` 解析 `system` 并发布不可变的 `ThemeSnapshot`;ui-layout 把每份快照应用到文档。本包还提供 `--dsw-*` token 样式表,并注入同步引导,使所选调色板与字号在外壳加载前生效。第三方主题可通过 `ctx.theme` 注册别名 token 覆盖。 +`dsh-client-ui-theme` 让 Web GUI 用户在设置中选择 `light`、`dark` 或 `system`,并把会话正文字号设为 12 至 24 px。回环客户端把两个值存入 `ui-theme` 设置命名空间,本地提供方默认将其持久化到 `$DSH_HOME/cordis.patch.yml`。插件通过 `prefers-color-scheme` 解析 `system` 并发布不可变的 `ThemeSnapshot`;ui-layout 把每份快照应用到文档。本包还提供 `--dsw-*` token 样式表,并注入同步引导,使所选调色板与字号在外壳加载前生效。第三方主题可通过 `ctx.theme` 注册别名 token 覆盖。 ## 目录 @@ -29,7 +29,7 @@ kind: "package-reference" ### 外观与字号 -插件在「通用」分区注册外观偏好方块与字号步进器。步进器接受 12 至 17 px 的整数,默认值为 14 px。它以相同增量调整会话标题与基础文本,包括用户气泡与 composer 草稿;流内行的标题、摘要与表格跟随比正文低一档的字号,小号文本和代码保持固定字号。每次通过的变更都经 Host settings API 写入。连续快速变更按操作顺序携带命名空间 revision 串行写入,最新写入被拒时重新加载持久值。非 loopback 页面把两个选择都保留在进程内。 +插件在「通用」分区注册外观偏好方块与字号步进器。步进器接受 12 至 24 px 的整数,默认值为 14 px。它以相同增量调整会话标题与基础文本,包括用户气泡与 composer 草稿;流内行的标题、摘要与表格跟随比正文低一档的字号,小号文本和代码保持固定字号。每次通过的变更都经 Host settings API 写入。连续快速变更按操作顺序携带命名空间 revision 串行写入,最新写入被拒时重新加载持久值。非 loopback 页面把两个选择都保留在进程内。 ### 注册主题 @@ -51,7 +51,9 @@ kind: "package-reference" ### 样式表 -`src/styles/` 下有六张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`corner-shape.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR(热模块替换)会随 ui-theme 一同移除。`scrollbar.css` 消费 `--dsw-alias-scrollbar-*` token,必须排在声明这些 token 的 `design-platform.css` 之后。状态标记使用各自的语义状态 token。`design-platform.css` 还负责代码差异底色的别名及其静态透明度色阶,以及文件对比所用的 `--dsw-alias-file-diff-*` 代码区、行号区和标记配色;`shiki.css` 负责语法颜色。 +`src/styles/` 下有六张样式表,由 ui-theme 的动态客户端 entry 依次导入:`base.css`、`corner-shape.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。客户端 bundle 将其编译并注入为插件持有的全局样式,因此卸载与 HMR(热模块替换)会随 ui-theme 一同移除。`scrollbar.css` 消费 `--dsw-alias-scrollbar-*` token,必须排在声明这些 token 的 `design-platform.css` 之后。状态标记使用各自的语义状态 token。`design-platform.css` 还负责代码差异底色的别名及其静态透明度色阶,以及文件对比所用的 `--dsw-alias-file-diff-*` 代码区、行号区和标记配色;`shiki.css` 负责语法颜色。`--dsw-alias-label-shimmer` 为共享文字扫光提供叠加色:浅色配色使用 40% alpha 的黑色,深色配色使用 45% alpha 的白色。 `--dsw-alias-label-deep-diving` 和 `--dsw-alias-label-deep-diving-shimmer` 提供蓝色活动文本和扫光颜色;深色主题使用更亮、饱和度更低的文字,并以更亮的蓝色扫光。 + +`--dsw-alias-turn-trigger-*` token 分别定义深浅主题下轮次触发通知的常态和悬停底色。 `brand-font.css` 导出本地 Montserrat Light、Regular 和 Medium 字体(正体、字重 300、400 和 500),`lib/styles/` 同时提供 `montserrat-light.woff2`、`montserrat-regular.woff2`、`montserrat-medium.woff2` 及其 SIL Open Font License。Desktop 将同一份样式表、字体和许可证打包,用于欢迎页品牌文字的离线显示;普通界面保留系统字体栈。 diff --git a/packages/client/ui-theme/src/styles/design-platform.css b/packages/client/ui-theme/src/styles/design-platform.css index 8f447398fc..a6510f2951 100644 --- a/packages/client/ui-theme/src/styles/design-platform.css +++ b/packages/client/ui-theme/src/styles/design-platform.css @@ -211,6 +211,8 @@ body { --dsw-alias-interactive-bg-hover-solid: var(--dsw-static-neutral-bluish-75); --dsw-alias-interactive-bg-hover: rgba(38, 49, 72, 0.06); --dsw-alias-label-caption: var(--dsw-static-neutral-bluish-400); + --dsw-alias-label-deep-diving: color-mix(in srgb, var(--dsw-static-deepseek-500) 70%, var(--dsw-static-blue-950)); + --dsw-alias-label-deep-diving-shimmer: color-mix(in srgb, var(--dsw-static-deepseek-500) 35%, var(--dsw-static-blue-950)); --dsw-alias-label-dimmed: var(--dsw-static-neutral-bluish-200); --dsw-alias-label-primary-bluish: var(--dsw-static-blue-900); --dsw-alias-label-primary-dimmed: var(--dsw-static-neutral-bluish-950); @@ -218,6 +220,7 @@ body { --dsw-alias-label-primary-inverted: var(--dsw-static-neutral-bluish-00); --dsw-alias-label-primary: var(--dsw-static-neutral-bluish-1000); --dsw-alias-label-secondary: var(--dsw-static-neutral-bluish-700); + --dsw-alias-label-shimmer: color-mix(in srgb, var(--dsw-static-neutral-1000) 40%, transparent); --dsw-alias-label-tertiary: var(--dsw-static-neutral-bluish-600); --dsw-alias-link: var(--dsw-static-deepseek-500); --dsw-alias-markdown-citation: var(--dsw-static-neutral-bluish-100); @@ -254,6 +257,8 @@ body { --dsw-alias-state-warn-tertiary: var(--dsw-static-amber-100); --dsw-alias-toast-bg: var(--dsw-static-neutral-bluish-800); --dsw-alias-tooltip-bg: var(--dsw-static-neutral-bluish-850); + --dsw-alias-turn-trigger-bg: var(--dsw-alias-markdown-code-block); + --dsw-alias-turn-trigger-bg-hover: var(--dsw-alias-interactive-bg-hover); --dsw-specific-bubble-highlight: var(--dsw-static-deepseek-200); --dsw-specific-bubble: var(--dsw-static-deepseek-50); --dsw-specific-input-major: var(--dsw-static-neutral-bluish-00); @@ -317,6 +322,8 @@ body[data-ds-dark-theme] { --dsw-alias-interactive-bg-hover-solid: var(--dsw-static-neutral-bluish-800); --dsw-alias-interactive-bg-hover: rgba(255, 255, 255, 0.08); --dsw-alias-label-caption: var(--dsw-static-neutral-bluish-600); + --dsw-alias-label-deep-diving: color-mix(in srgb, var(--dsw-static-deepseek-450) 55%, var(--dsw-static-neutral-bluish-400)); + --dsw-alias-label-deep-diving-shimmer: color-mix(in srgb, var(--dsw-static-blue-300) 65%, var(--dsw-static-deepseek-400)); --dsw-alias-label-dimmed: var(--dsw-static-neutral-bluish-750); --dsw-alias-label-primary-bluish: var(--dsw-static-neutral-bluish-50); --dsw-alias-label-primary-dimmed: var(--dsw-static-neutral-bluish-100); @@ -324,6 +331,7 @@ body[data-ds-dark-theme] { --dsw-alias-label-primary-inverted: var(--dsw-static-neutral-bluish-800); --dsw-alias-label-primary: var(--dsw-static-neutral-bluish-50); --dsw-alias-label-secondary: var(--dsw-static-neutral-bluish-300); + --dsw-alias-label-shimmer: color-mix(in srgb, var(--dsw-static-neutral-00) 45%, transparent); --dsw-alias-label-tertiary: var(--dsw-static-neutral-bluish-400); --dsw-alias-link: var(--dsw-static-deepseek-400); --dsw-alias-markdown-citation: var(--dsw-static-neutral-bluish-800); @@ -359,6 +367,8 @@ body[data-ds-dark-theme] { --dsw-alias-state-warn-secondary: var(--dsw-static-amber-400); --dsw-alias-state-warn-tertiary: var(--dsw-static-amber-900); --dsw-alias-toast-bg: var(--dsw-static-neutral-bluish-750); + --dsw-alias-turn-trigger-bg: var(--dsw-alias-interactive-bg-hover); + --dsw-alias-turn-trigger-bg-hover: var(--dsw-alias-interactive-bg-active); --dsw-alias-tooltip-bg: var(--dsw-static-neutral-bluish-750); --dsw-specific-bubble-highlight: var(--dsw-static-neutral-bluish-750); --dsw-specific-bubble: var(--dsw-static-neutral-bluish-850); diff --git a/packages/client/ui-theme/src/styles/gradient-shadow-text.css b/packages/client/ui-theme/src/styles/gradient-shadow-text.css index b6dc4db464..ae57254ad6 100644 --- a/packages/client/ui-theme/src/styles/gradient-shadow-text.css +++ b/packages/client/ui-theme/src/styles/gradient-shadow-text.css @@ -45,11 +45,11 @@ body[data-ds-dark-theme] { /* Markdown 阶梯整体按 0.875(正文 16 → 14)等比缩放,四舍五入到整数像素; 重新从 Figma 导出时需要保留这一缩放。 */ /* 正文字号调节:--dsh-content-font-size 由主题 bootstrap 与 ThemePresenter - 写在 body 上(整数 px,12–17,默认 14)。标题与正文变体按同一像素增量 + 写在 body 上(整数 px,12–24,默认 14)。标题与正文变体按同一像素增量 (--dsh-content-font-delta)随动,保持层级。低一档文本(think、工具/bash 行的标题与摘要、markdown 表格、流内次级说明等)统一读 --dsh-content-font-size-secondary:设置 ≤14 时为设置值 −1,>14 时为 - 设置值 −2(12→11、13→12、14→13、15→13、16→14、17→15)。式中 + 设置值 −2(12→11、13→12、14→13、15→13、16→14、17→15、24→22)。式中 max(13px,·) 的作用是让低设置取 −1 而非 −2,不是结果下限——12 设置下 层级触底 11px。small、code 属于密集次级文本,保持固定。 */ body { diff --git a/packages/client/ui-theme/src/theme-settings.ts b/packages/client/ui-theme/src/theme-settings.ts index 11f8789d6c..a9202ffa1b 100644 --- a/packages/client/ui-theme/src/theme-settings.ts +++ b/packages/client/ui-theme/src/theme-settings.ts @@ -24,7 +24,7 @@ export const DEFAULT_PREFERENCE: ThemePreference = 'system' export const FONT_SIZE_MIN = 12 /** Largest accepted content font size (px). */ -export const FONT_SIZE_MAX = 17 +export const FONT_SIZE_MAX = 24 /** Content font size when the user-settings document has no override (px). */ export const DEFAULT_FONT_SIZE = 14 diff --git a/packages/client/ui-theme/tests/boot-theme.client.spec.ts b/packages/client/ui-theme/tests/boot-theme.client.spec.ts index 6a0141ea87..574a54dd13 100644 --- a/packages/client/ui-theme/tests/boot-theme.client.spec.ts +++ b/packages/client/ui-theme/tests/boot-theme.client.spec.ts @@ -77,8 +77,8 @@ describe('theme bootstrap row', () => { it('writes the durable content font size and defaults it to 14px', () => { mockSystemDark(false) - executeBootstrap('light', 17) - expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('17px') + executeBootstrap('light', 24) + expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('24px') executeBootstrap('light') expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('14px') }) diff --git a/packages/client/ui-theme/tests/config.host.spec.ts b/packages/client/ui-theme/tests/config.host.spec.ts index a3309b9887..8123a71b31 100644 --- a/packages/client/ui-theme/tests/config.host.spec.ts +++ b/packages/client/ui-theme/tests/config.host.spec.ts @@ -28,11 +28,11 @@ describe('ui-theme host', () => { const configuration = await liveConfig(ctx, { Config, apply }) const { fiber } = configuration expect(plainConfig(configuration.fiber.config)).toEqual({ preference: DEFAULT_PREFERENCE, fontSize: 14 }) - await configuration.update({ preference: 'dark', fontSize: 16 }) - expect(plainConfig(configuration.fiber.config)).toEqual({ preference: 'dark', fontSize: 16 }) + await configuration.update({ preference: 'dark', fontSize: 24 }) + expect(plainConfig(configuration.fiber.config)).toEqual({ preference: 'dark', fontSize: 24 }) await expect(configuration.update({ preference: 'sepia' })).rejects.toThrow() await expect(configuration.update({ fontSize: 11 })).rejects.toThrow() - await expect(configuration.update({ fontSize: 18 })).rejects.toThrow() + await expect(configuration.update({ fontSize: 25 })).rejects.toThrow() await fiber.dispose() }) @@ -51,10 +51,10 @@ describe('ui-theme host', () => { expect(rowText(rows[0])).toContain('@media(prefers-color-scheme:dark)') expect(rowText(rows[1])).toContain('const preference = "system"') expect(rowText(rows[1])).toContain('"14px"') - await configuration.update({ preference: 'dark', fontSize: 17 }) + await configuration.update({ preference: 'dark', fontSize: 24 }) expect(rowText(collect(ctx)[0])).toContain('color-scheme:dark') expect(rowText(collect(ctx)[1])).toContain('const preference = "dark"') - expect(rowText(collect(ctx)[1])).toContain('"17px"') + expect(rowText(collect(ctx)[1])).toContain('"24px"') await fiber.dispose() expect(collect(ctx)).toEqual([{ kind: 'script', placement: 'head', text: 'window.afterTheme=true' }]) }) diff --git a/packages/client/ui-theme/tests/font-size-row.client.spec.tsx b/packages/client/ui-theme/tests/font-size-row.client.spec.tsx index f75138b44a..bc8f33062e 100644 --- a/packages/client/ui-theme/tests/font-size-row.client.spec.tsx +++ b/packages/client/ui-theme/tests/font-size-row.client.spec.tsx @@ -87,7 +87,11 @@ describe('FontSizeRow', () => { }) it('disables the outward arrow at each bound', () => { - mount(17) + const b = mount(23) + fireEvent.click(arrow('Increase font size')) + expect(b.setFontSize).toHaveBeenCalledWith(24) + act(() => { b.store.actions.sync(24, 1) }) + expect(screen.getByText('24')).toBeDefined() expect(arrow('Increase font size').disabled).toBe(true) expect(arrow('Decrease font size').disabled).toBe(false) cleanup() diff --git a/packages/client/ui-theme/tests/theme.client.spec.ts b/packages/client/ui-theme/tests/theme.client.spec.ts index a50ed3e7e2..d00b1d5b66 100644 --- a/packages/client/ui-theme/tests/theme.client.spec.ts +++ b/packages/client/ui-theme/tests/theme.client.spec.ts @@ -36,10 +36,10 @@ describe('ThemeRuntime', () => { it('seeds the initial font size from the boot-script body variable, ignoring junk', () => { // The Host boot script writes the durable size on body before any plugin // runs; the first snapshot must match it so activation never flashes 14. - document.body.style.setProperty('--dsh-content-font-size', '16px') + document.body.style.setProperty('--dsh-content-font-size', '24px') try { - expect(make().theme.getTheme().fontSize).toBe(16) - document.body.style.setProperty('--dsh-content-font-size', '99px') + expect(make().theme.getTheme().fontSize).toBe(24) + document.body.style.setProperty('--dsh-content-font-size', '25px') expect(make().theme.getTheme().fontSize).toBe(14) } finally { document.body.style.removeProperty('--dsh-content-font-size') @@ -48,19 +48,19 @@ describe('ThemeRuntime', () => { it('setFontSize switches, writes through the scope, and republishes; same value is a no-op', () => { const { theme, events, host } = make() - theme.setFontSize(17) - expect(theme.getTheme().fontSize).toBe(17) - expect(host.set).toHaveBeenCalledWith('fontSize', 17) + theme.setFontSize(24) + expect(theme.getTheme().fontSize).toBe(24) + expect(host.set).toHaveBeenCalledWith('fontSize', 24) expect(events).toHaveLength(1) - theme.setFontSize(17) + theme.setFontSize(24) expect(events).toHaveLength(1) expect(host.set).toHaveBeenCalledOnce() }) it('rejects out-of-range and fractional font sizes', () => { const { theme, events, host } = make() - for (const px of [11, 18, 14.5, Number.NaN]) { - expect(() => { theme.setFontSize(px) }).toThrow('outside 12..17') + for (const px of [11, 25, 14.5, Number.NaN]) { + expect(() => { theme.setFontSize(px) }).toThrow('outside 12..24') } expect(events).toHaveLength(0) expect(host.set).not.toHaveBeenCalled() diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index af3b5ca741..358fa4458f 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-tool/README.md -README.md: a00ed8e8c78dce29144a3ff89d2eb671b5f209fb -README.zh.md: c4a6f6183be082d8c21877b0c1c7052652bc7e2e +README.md: 1fcfa9ffda84b5e0bf76a46accbe486cfb9e9d04 +README.zh.md: 2806fa47ae4d8e48215bce9ca178984839633421 diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index a00ed8e8c7..1fcfa9ffda 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -49,7 +49,7 @@ The owner payload is `ToolCallOwnerProps`: `callId`, `toolName`, the `phase` dis Every registered view receives the explicit `preparing`, `start`, and `result` props declared in [the Tool slot types](src/client/contract/slots.ts). Shared rows use the same `ToolRow` in all stages. Their row model selects the title and combines any generic tool-name prefix with the available argument summary independently of lifecycle state. Tool-owned titles omit the English name. Preparation has no argument summary, and the shared argument parser returns no call without parsing JSON. Custom renderers such as Bash, Skill, and Cordis handle preparation separately; their argument-dependent components accept `StartedToolCallViewProps`. -This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and PTC dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Running and settled foreground standard `bash`/`pwsh` and `terminal_send` calls use terminal cards at the root and in PTC dispatch children, subject to the same argument, result, and error checks. Persistent `bash`/`pwsh` calls use terminal cards only while running. Shell output ending in a recognized spill-policy notice uses expandable generic output in shell rows and generic output in Details; a displaced or omitted exit marker cannot establish success. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable, while background acknowledgements remain collapsed. A native or PTC dispatch failure carrying `AUTO_REVIEW_DENIED` shows the Auto review verdict in its collapsed row and one normalized not-executed reason when expanded; a missing or whitespace-only reason uses localized fallback text. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`. +This package owns the generic fallback and the built-in shell/pwsh, read, read_image, write/edit, running `str_replace_editor` `create`/`str_replace`, grep/glob, web, todo, question, and PTC dispatch presentations. Structured cards derive directly from first-party raw event fields; Host `presentCall` and `presentResult` values never enter the Client. Tool headers follow the shared [process-row colors](../ui-primitives/README.md#component-catalog) for names, summaries, file paths, ordinary counts, glyphs, and chevrons in every Work details mode, whether details are collapsed, expanded, or unavailable; error and warning summaries keep their semantic colors on hover. Running atomic rows share one left-to-right shimmer across their titles, separators, summaries, and suffixes, while settled rows stay static. Running and settled foreground standard `bash`/`pwsh` and `terminal_send` calls use terminal cards at the root and in PTC dispatch children, subject to the same argument, result, and error checks. Persistent `bash`/`pwsh` calls use terminal cards only while running. Shell output ending in a recognized spill-policy notice uses expandable generic output in shell rows and generic output in Details; a displaced or omitted exit marker cannot establish success. Settled persistent-shell results stay generic because reset and partial-output diagnostics do not always describe one process exit status; root persistent results are expandable, while background acknowledgements remain collapsed. A native or PTC dispatch failure carrying `AUTO_REVIEW_DENIED` shows the Auto review verdict in its collapsed row and one normalized not-executed reason when expanded; a missing or whitespace-only reason uses localized fallback text. A successful question row pairs call questions with result answers by their stable ids and shows readable question/answer lines when expanded. A cancelled or interrupted row shows its verdict and original questions without inventing answers. Unsupported, malformed, or ambiguous inputs fall back to flattened Tool input/result text. `ui-skill` demonstrates a business-owned registration for `skill`. ----- @@ -72,7 +72,7 @@ Tool owner props forward Chat's stable `useDisclosure` Hook through root and nes Every card is read in place in the call tree; there is no second, full-height presentation of a selected call. Row renderers share one pure card model for each terminal, read, diff, search, and web card, and the image card's gallery renders through the tool-owned `tool.call.images` slot. These models validate raw call arguments, result content, failure state, persisted metadata, PTC dispatch `parentCallId`, and Session path facts. Unsupported or malformed inputs use flattened Tool result text. A file-path summary opens the file through the owner's `openFile`, which the chat view routes to the right Sidebar's text preview; `inspect` opens the trajectory view and is absent when that View is unavailable; cards then omit Inspect. Card-specific limits and fallback rules for the terminal, diff, read, search, and web cards remain in [the ui-primitives README](../ui-primitives/README.md); the image card's model in this package carries its own fallback rules. -Chat diff cards keep nine rows before folding, enough for a file header, one removed/added pair, and three context lines on either side. The tool row shows the primitive's exact or coarse-replacement counts; the expanded card contains the diff body without a totals footer. +Chat diff cards keep nine rows before folding, enough for a file header, one removed/added pair, and three context lines on either side. The tool row shows the primitive's exact or coarse-replacement counts; the expanded card contains the diff body without a totals footer. Hovering the tool header colors its added count green and removed count red; both retain those colors while the card is expanded. An Auto denial takes precedence over keyed specialized views. Its generic row preserves the call identity, omits raw arguments, and normalizes the stored reason only for display: trim surrounding whitespace and collapse line separators to spaces, with localized fallback for an empty result. Session and SDK error details keep the original reason. diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index c4a6f6183b..2806fa47ae 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -49,7 +49,7 @@ owner 载荷为 `ToolCallOwnerProps`:`callId`、`toolName`、`phase` 判别字 每个注册视图都接收[工具 slot 类型](src/client/contract/slots.ts)声明的显式 `preparing`、`start` 和 `result` props。通用行在三个阶段使用同一个 `ToolRow`。行模型统一选择标题,并组合通用工具名前缀与已有参数摘要,不按生命周期阶段改变前缀;专用标题不附带英文名。准备阶段没有参数摘要,共享参数解析入口直接返回无调用,不解析 JSON。Bash、Skill、Cordis 等自定义 renderer 分别处理准备态,其依赖参数的组件接收 `StartedToolCallViewProps`。 -本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、运行中的 `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 PTC dispatch 的内置展示。结构化卡片直接从第一方原始 event 字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。运行中与已完成的前台标准 `bash`/`pwsh` 和 `terminal_send` 调用,无论位于根还是 PTC dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。持久 `bash`/`pwsh` 调用仅在运行中使用 terminal 卡片。以已识别的 spill 策略提示结尾的 shell 输出,在 shell 行中使用可展开的 generic 输出,在 Details 中使用 generic 输出;位置被改变或被省略的退出标记无法证明成功。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开,后台启动回执则保持折叠。带有 `AUTO_REVIEW_DENIED` 的原生或 PTC dispatch 失败会在折叠行显示 Auto review 裁决,展开时显示一行归一化后的“未执行”原因;原因缺失或只有空白时使用本地化 fallback 文案。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 +本包拥有 generic fallback,以及 shell/pwsh、read、read_image、write/edit、运行中的 `str_replace_editor` `create`/`str_replace`、grep/glob、web、todo、question 与 PTC dispatch 的内置展示。结构化卡片直接从第一方原始事件字段派生;Host `presentCall` 与 `presentResult` 值不会进入 Client。工具标题行的名称、摘要、文件路径、普通数量、图标与箭头在所有工作过程展示模式下均遵循共享的[过程行颜色](../ui-primitives/README.zh.md#component-catalog),无论详情是收起、展开还是不可展开;错误和警告摘要在悬停时保留语义色。运行中的原子工具行以同一道高光从左到右扫过标题、分隔符、摘要与后缀;已完成行保持静态。运行中与已完成的前台标准 `bash`/`pwsh` 和 `terminal_send` 调用,无论位于根还是 PTC dispatch 子调用中,都在通过相同的参数、结果和错误检查后使用 terminal 卡片。持久 `bash`/`pwsh` 调用仅在运行中使用 terminal 卡片。以已识别的 spill 策略提示结尾的 shell 输出,在 shell 行中使用可展开的 generic 输出,在 Details 中使用 generic 输出;位置被改变或被省略的退出标记无法证明成功。已完成的持久 shell 结果保持 generic 展示,因为 reset 与部分输出诊断不一定描述单个进程的退出状态;根调用的持久 shell 结果可展开,后台启动回执则保持折叠。带有 `AUTO_REVIEW_DENIED` 的原生或 PTC dispatch 失败会在折叠行显示 Auto review 裁决,展开时显示一行归一化后的“未执行”原因;原因缺失或只有空白时使用本地化 fallback 文案。成功的问题行按稳定 id 配对调用中的问题与结果中的回答,展开后显示可读的问答行。已取消或已中断的问题行显示其裁决与原始问题,不虚构回答。不受支持、格式错误或含糊的输入回退为压平的工具输入/结果文本。`ui-skill` 展示了业务包自行拥有的 `skill` 注册项。 ----- @@ -72,7 +72,7 @@ Tool 所有者属性将 Chat 注入的稳定 `useDisclosure` 钩子传给根调 每张卡片都直接在调用树中查看;选中调用后不会再显示第二个全高视图。行 renderer 为 terminal、read、diff、search 和 web 卡片各复用同一个纯 card model,image 卡片的图库经由工具自有 `tool.call.images` slot 渲染。这些 model 校验原始调用参数、结果内容、失败状态、持久 metadata、PTC dispatch 的 `parentCallId` 与会话路径信息。不受支持或格式错误的输入使用压平的工具结果文本。文件路径摘要经属主的 `openFile` 打开文件,chat 视图把它路由到右侧 Sidebar 的文本预览;`inspect` 打开轨迹视图;该视图不可用时不提供此回调,卡片随之隐藏 Inspect。terminal、diff、read、search 与 web 卡片的上限与 fallback 规则仍由 [ui-primitives README](../ui-primitives/README.zh.md) 负责;image 卡片的 fallback 规则由本包内的 card model 自行承载。 -Chat diff 卡片在折叠前保留九行,足以容纳文件标题、一对删除与新增行及两侧各三行上下文。工具行显示原语提供的精确或粗粒度替换统计;展开卡片包含差异正文,不显示底部统计。 +Chat diff 卡片在折叠前保留九行,足以容纳文件标题、一对删除与新增行及两侧各三行上下文。工具行显示原语提供的精确或粗粒度替换统计;展开卡片包含差异正文,不显示底部统计。悬停工具标题行时,新增数量显示绿色、删除数量显示红色;卡片展开期间保留这两种颜色。 Auto 拒绝优先于按工具名选择的专门视图。其通用行保留调用身份、省略原始参数,并且只在显示时归一化存储的理由:去除首尾空白,把行分隔符折叠为空格,结果为空时使用本地化通用理由。Session 与 SDK 错误详情保留原始理由。 diff --git a/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css b/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css index 5e54eeb7e5..a33507500d 100644 --- a/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css +++ b/packages/client/ui-tool/src/client/tool/components/ToolRow.module.css @@ -15,28 +15,12 @@ flex-shrink: 0; } -/* Cordis lifecycle tools retain their generic row mechanics while carrying a - shared product accent and tool-owned action title. */ -.root[data-tool^='cordis_'] .leading, -.root[data-tool^='cordis_'] .title { - color: var(--dsw-alias-state-business-primary); -} - .root[data-tool^='cordis_'] .title { font-weight: 500; } -.root[data-tool^='cordis_'] .sep { - background: var(--dsw-alias-state-business-primary); -} - -.chevron { - color: var(--dsw-alias-label-secondary); -} - .title { font-weight: 400; - transition: color 100ms ease; } .sep { @@ -56,8 +40,6 @@ white-space: nowrap; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); - color: var(--dsw-alias-label-tertiary); - transition: color 100ms ease; } /* Trailing summary fragment kept out of .summary's ellipsis, for a count whose @@ -71,31 +53,29 @@ white-space: nowrap; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); - color: var(--dsw-alias-label-tertiary); - transition: color 100ms ease; -} - -.row:hover .title, -.row:hover .summary:not(.errorSummary):not(.stoppedSummary), -.row:hover .summarySuffix { - color: var(--dsw-alias-label-primary); } /* Diff-row +/- totals: the code font, matching the diff body the numbers summarize; a wider gap keeps the digits from reading as part of the path. Two px under the secondary tier (still riding the axis): mono digits read - optically larger than the sans path at the same size. Caption, one step - dimmer than the suffix's tertiary, so the digits stay behind the path. The + optically larger than the sans path at the same size. The half-pixel nudge closes the baseline gap the size difference leaves under the row's box-centering (a transform, so flex layout is untouched). */ .diffStat { margin-left: 10px; font-family: var(--ds-font-family-code); font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 2px); - color: var(--dsw-alias-label-caption); transform: translateY(0.5px); } +.row:is(:hover, [aria-expanded='true']) .diffAdded { + color: var(--dsw-alias-state-success-primary); +} + +.row:is(:hover, [aria-expanded='true']) .diffRemoved { + color: var(--dsw-alias-state-error-primary); +} + /* File-tool path: same type as .summary, with a persistent link affordance. Dotted dimmed underline: visible enough to say "clickable", light enough that a long path doesn't read as one heavy rule under the row. Shrink-to-fit @@ -115,19 +95,12 @@ text-align: left; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); - color: var(--dsw-alias-label-secondary); + color: inherit; text-decoration: underline dotted; - text-decoration-color: var(--dsw-alias-label-tertiary); + text-decoration-color: currentColor; text-decoration-thickness: 1px; text-underline-offset: 3px; cursor: pointer; - transition: color 100ms ease; -} - -.row:hover .fileLink, -.fileLink:hover { - color: var(--dsw-alias-label-primary); - text-decoration-color: currentColor; } /* Error row's collapsed summary: the failure's first line in the error color. */ diff --git a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx index 618344b4c7..8855a32105 100644 --- a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx +++ b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx @@ -2,7 +2,7 @@ import { memo, useCallback, useMemo, type KeyboardEvent, type MouseEvent, type R import clsx from 'clsx' import { CodeBlock, DiffBlock, DisclosureRow, IconInspectOutlineRegular, ReadBlock, SearchBlock, - TerminalBlock, TextShimmer, WebBlock, + TerminalBlock, ShimmerText, WebBlock, diffTotals, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsRenderSlots, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' @@ -55,6 +55,7 @@ export interface ToolRowProps { errorSummary?: string | null | undefined /** Terminal card; card fields are mutually exclusive and replace text sections. */ terminal?: TerminalCardModel | null | undefined + /** Diff card with inline totals colored on header hover and while expanded. */ diff?: DiffCardModel | null | undefined read?: ReadCardModel | null | undefined /** @@ -175,11 +176,7 @@ export const ToolRow = memo(function ToolRow({ const summaryText = failureLine ?? normalSummary // The tool row keeps the diff's +/- totals visible while its body is collapsed. // An explicit summarySuffix overrides the diff totals. - const diffStat = useMemo(() => { - if (diffBody === null) return null - const { added, removed } = diffTotals(diffBody.card.diffs) - return `+${added} -${removed}` - }, [diffBody]) + const diffStat = useMemo(() => diffBody === null ? null : diffTotals(diffBody.card.diffs), [diffBody]) const settledWithCue = state === 'error' || state === 'stopped' const suffix = settledWithCue ? null : summarySuffix ?? diffStat const openFile = useMemo(() => filePath !== undefined && onOpenFile !== undefined && !settledWithCue @@ -203,7 +200,7 @@ export const ToolRow = memo(function ToolRow({ /* An empty summary drops the separator with it (a row that is only its title shows no trailing dot). */ <> - + {openFile !== undefined ? ( ) : ( - {summaryText} + {summaryText} )} - {suffix !== null && ( - {suffix} - )} + {suffix !== null && (typeof suffix === 'string' + ? {suffix} + : ( + + {`+${suffix.added}`} + {' '} + {`-${suffix.removed}`} + + ))} - ), [diffStat, fileLinkKeyDown, openFile, running, state, suffix, summaryText]) + ), [fileLinkKeyDown, openFile, state, suffix, summaryText]) const expandedContent = useMemo(() => open ? (
{askQuestionBody !== null @@ -340,7 +343,6 @@ export const ToolRow = memo(function ToolRow({ rowClassName={css.row} leadingClassName={css.leading} titleClassName={css.title} - chevronClassName={css.chevron} icon={icon} title={title} running={running} diff --git a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css index 9ef5b4b829..007219766c 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css +++ b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.module.css @@ -89,6 +89,18 @@ align-items: center; height: calc(24px + var(--dsh-content-font-delta, 0px)); min-width: 0; + color: var(--dsw-alias-label-tertiary); + transition: color 100ms ease; +} + +.root:hover { + color: var(--dsw-alias-label-secondary); +} + +@media (prefers-reduced-motion: reduce) { + .root { + transition: none; + } } .leading { @@ -100,7 +112,7 @@ align-items: center; justify-content: center; margin-right: 6px; - color: var(--dsw-alias-label-tertiary); + color: inherit; } /* Same glyph-scaling rule as DisclosureRow's leading: the CSS edge overrides @@ -115,7 +127,7 @@ } .chevron { - color: var(--dsw-alias-label-secondary); + color: inherit; } /* Hover preview on the expandable row: the idle icon crossfades (100ms) into @@ -146,8 +158,6 @@ flex: none; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); - color: var(--dsw-alias-label-secondary); - transition: color 100ms ease; } .sep { @@ -167,13 +177,6 @@ white-space: nowrap; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); - color: var(--dsw-alias-label-tertiary); - transition: color 100ms ease; -} - -.root:hover .title, -.root:hover .summary:not(.errorSummary):not(.stoppedSummary) { - color: var(--dsw-alias-label-primary); } /* Error row's collapsed summary: the failure's first line in the error color. */ diff --git a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx index 47ae2d1b6d..68e18b8aee 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx @@ -3,7 +3,7 @@ import type { Context } from '@deepseek-ai/cordis' import clsx from 'clsx' import { IconApiOutlineRegular, IconChevronDownOutlineRegular, IconChevronUpOutlineRegular, IconInspectOutlineRegular, - TerminalBlock, TextShimmer, + TerminalBlock, Shimmer, ShimmerText, } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '../../contract/slots.ts' @@ -107,17 +107,19 @@ const StartedBashRow = memo(function StartedBashRow({ toolName, block, sessionId onClick={expandable ? toggleExpand : undefined} onKeyDown={expandable ? toggleFromKeyboard : undefined} > - {leading} {status !== null && {status}} - {t(model.titleKey)} - - - {settlementLine ?? normalSummary} - + {leading} + + {t(model.titleKey)} + + + {settlementLine ?? normalSummary} + +
{open && (
diff --git a/packages/client/ui-tool/tests/diff-card.client.spec.tsx b/packages/client/ui-tool/tests/diff-card.client.spec.tsx index 3f9282b29f..7ebc5265a3 100644 --- a/packages/client/ui-tool/tests/diff-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/diff-card.client.spec.tsx @@ -245,9 +245,10 @@ describe('FileMutationRow diff card', () => { meta: { diffs: [] }, }), 'write')} />) // The collapsed row already carries the card's +/- totals beside the path. - expect(view.getByText('+1 -0')).toBeTruthy() + expect(view.container.querySelector('[data-disclosure-row]')?.textContent).toContain('+1 -0') toggleRow(view) - expect(view.getAllByText('+1 -0')).toHaveLength(1) + expect(view.getAllByText('+1')).toHaveLength(1) + expect(view.getAllByText('-0')).toHaveLength(1) expect(view.getByText('hello fixture')).toBeTruthy() }) diff --git a/packages/client/ui-tool/tests/tool-row-styles.client.spec.ts b/packages/client/ui-tool/tests/tool-row-styles.client.spec.ts index db05c7f769..399c588713 100644 --- a/packages/client/ui-tool/tests/tool-row-styles.client.spec.ts +++ b/packages/client/ui-tool/tests/tool-row-styles.client.spec.ts @@ -25,9 +25,15 @@ describe('ToolRow.module.css summary line', () => { it.each([ ['ToolRow', '../src/client/tool/components/ToolRow.module.css'], ['BashRow', '../src/client/tool/toolviews/bash-sample.module.css'], - ] as const)('%s keeps stopped and error summaries outside hover darkening', (_name, path) => { + ] as const)('%s leaves row text color to the shared disclosure header', (_name, path) => { + // The header supplies tertiary at rest and secondary on hover; a tool sheet + // that restates a row text color, or brightens the row text on hover, would + // pin one state against the shared process-row colors. The Inspect button + // keeps its own solid hover fill and is not row text. const sheet = readFileSync(fileURLToPath(new URL(path, import.meta.url)), 'utf8') - expect(sheet).toContain('.summary:not(.errorSummary):not(.stoppedSummary)') + .replace(/\/\*[\s\S]*?\*\//g, ' ') + expect(sheet).not.toMatch(/\.(title|summary|summarySuffix)\s*\{[^{}]*\bcolor:/) + expect(sheet).not.toMatch(/:hover\s+\.(title|summary|summarySuffix)\b/) }) it('keeps the summary suffix on one line and unshrunk', () => { diff --git a/packages/client/ui-tool/tests/tool-row.client.spec.tsx b/packages/client/ui-tool/tests/tool-row.client.spec.tsx index f9338cac00..5f9c210e69 100644 --- a/packages/client/ui-tool/tests/tool-row.client.spec.tsx +++ b/packages/client/ui-tool/tests/tool-row.client.spec.tsx @@ -309,10 +309,11 @@ describe('ToolRow', () => { newText: 'start\nsecond\nthird\nnew\nfourth\nfifth\nend', }] }, }} />) - expect(view.getByText('+1 -1')).toBeTruthy() + expect(view.container.querySelector('[data-disclosure-row]')?.textContent).toContain('+1 -1') expect(view.container.querySelector('[data-diff]')).toBeNull() fireEvent.click(view.getByRole('button')) - expect(view.getAllByText('+1 -1')).toHaveLength(1) + expect(view.getAllByText('+1')).toHaveLength(1) + expect(view.getAllByText('-1')).toHaveLength(1) expect(view.getAllByText('start')).toHaveLength(1) expect(view.getAllByText('end')).toHaveLength(1) expect(view.getByText('old', { exact: true })).toBeTruthy() @@ -344,7 +345,7 @@ describe('ToolRow', () => { it('keeps the business icon across running and error states', () => { const runningView = render() - expect(runningView.queryByTestId('tool-icon')).not.toBeNull() + expect(runningView.container.querySelector('[data-testid="tool-icon"]:not([inert] *)')).not.toBeNull() expect(runningView.container.querySelector('[data-state="running"]')).not.toBeNull() const errorView = render() expect(errorView.container.querySelector('[data-testid="tool-icon"]')).not.toBeNull() diff --git a/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css b/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css index 190f136f70..270efc6c24 100644 --- a/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css +++ b/packages/client/ui-workflow-run/src/client/WorkflowRunPanel.module.css @@ -29,14 +29,14 @@ align-items: center; justify-content: center; margin-right: 0; - color: var(--dsw-alias-label-tertiary); + color: inherit; } .runTitle { overflow: hidden; flex: none; max-width: 42%; - color: var(--dsw-alias-label-secondary); + color: inherit; font-size: var(--dsh-content-font-size-secondary, 13px); font-weight: 510; line-height: calc(24px + var(--dsh-content-font-delta, 0px)); @@ -48,7 +48,7 @@ overflow: hidden; flex: 1; min-width: 0; - color: var(--dsw-alias-label-tertiary); + color: inherit; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(18px + var(--dsh-content-font-delta-secondary, 0px)); text-overflow: ellipsis; @@ -65,7 +65,7 @@ font-size: 11px; font-weight: 510; line-height: 16px; - color: var(--dsw-alias-label-secondary); + color: inherit; white-space: nowrap; } @@ -93,7 +93,7 @@ align-items: center; justify-content: center; margin-right: 0; - color: var(--dsw-alias-label-tertiary); + color: inherit; } .phaseTitle { @@ -101,7 +101,7 @@ flex: 0 1 auto; min-width: 0; max-width: 42%; - color: var(--dsw-alias-label-secondary); + color: inherit; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(24px + var(--dsh-content-font-delta, 0px)); text-overflow: ellipsis; @@ -112,7 +112,7 @@ overflow: hidden; flex: 1; min-width: 0; - color: var(--dsw-alias-label-tertiary); + color: inherit; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px)); text-overflow: ellipsis; @@ -125,7 +125,7 @@ /* Column width scales with the text (~132/13 ≈ 10px per font-size px) so larger sizes keep the same status text visible instead of truncating. */ width: calc(132px + var(--dsh-content-font-delta-secondary, 0px) * 10); - color: var(--dsw-alias-label-secondary); + color: inherit; font-size: var(--dsh-content-font-size-secondary, 13px); line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px)); text-align: right; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d834a613b..0e7d7b3a45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3193,6 +3193,9 @@ importers: '@types/react-dom': specifier: ~18.3.0 version: 18.3.7(@types/react@18.3.31) + lottie-web: + specifier: ^5.13.0 + version: 5.13.0 react: specifier: ^18.2.0 version: 18.3.1 @@ -9182,7 +9185,7 @@ importers: version: link:../../../vendor/schemastery '@earendil-works/pi-ai': specifier: ^0.85.1 - version: 0.85.1(patch_hash=b9bcce474fb2ac44633dff0fa722816a5bff5451b4575d5874035ea14ba70a4f)(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) + version: 0.85.1(patch_hash=b9bcce474fb2ac44633dff0fa722816a5bff5451b4575d5874035ea14ba70a4f)(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(supports-color@9.4.0)(ws@8.21.0)(zod@4.4.3) devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -19363,6 +19366,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lottie-web@5.13.0: + resolution: {integrity: sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==} + lowercase-keys@2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} @@ -22058,14 +22064,14 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@earendil-works/pi-ai@0.85.1(patch_hash=b9bcce474fb2ac44633dff0fa722816a5bff5451b4575d5874035ea14ba70a4f)(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-ai@0.85.1(patch_hash=b9bcce474fb2ac44633dff0fa722816a5bff5451b4575d5874035ea14ba70a4f)(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(supports-color@9.4.0)(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.123.0(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 '@earendil-works/pi-telemetry': 0.85.1 '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) '@smithy/node-http-handler': 4.7.3 - http-proxy-agent: 7.0.2 + http-proxy-agent: 7.0.2(supports-color@9.4.0) https-proxy-agent: 7.0.6 openai: 6.40.0(ws@8.21.0)(zod@4.4.3) partial-json: 0.1.7 @@ -24807,7 +24813,7 @@ snapshots: cross-spawn: 7.0.6 debug: 4.4.3(supports-color@9.4.0) fs-extra: 10.1.0 - http-proxy-agent: 7.0.2 + http-proxy-agent: 7.0.2(supports-color@9.4.0) https-proxy-agent: 7.0.6 js-yaml: 4.3.1 sanitize-filename: 1.6.4 @@ -26237,7 +26243,7 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - http-proxy-agent@7.0.2: + http-proxy-agent@7.0.2(supports-color@9.4.0): dependencies: agent-base: 7.1.4 debug: 4.4.3(supports-color@9.4.0) @@ -26747,6 +26753,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lottie-web@5.13.0: {} + lowercase-keys@2.0.0: {} lru-cache@10.4.3: {} diff --git a/scripts/rescope-vendor.ts b/scripts/rescope-vendor.ts index 2f724032d1..793df0ad81 100644 --- a/scripts/rescope-vendor.ts +++ b/scripts/rescope-vendor.ts @@ -135,6 +135,7 @@ const GENERIC_SKIPS: readonly GenericSkip[] = [ { file: 'packages/extensions/ui-cordis/src/client/CordisActionRow.tsx', upstream: ['cordis'] }, { file: 'packages/extensions/ui-cordis/src/client/CordisDefineRow.tsx', upstream: ['cordis'] }, { file: 'packages/extensions/ui-cordis/src/client/CordisPanel.tsx', upstream: ['cordis'] }, + { file: 'packages/extensions/ui-cordis/src/client/CordisPreparingRow.tsx', upstream: ['cordis'] }, { file: 'packages/extensions/ui-cordis/src/client/CordisRunRow.tsx', upstream: ['cordis'] }, { file: 'packages/extensions/ui-cordis/src/client/locales.ts', upstream: ['cordis'] }, ] diff --git a/scripts/test-dom-environment.ts b/scripts/test-dom-environment.ts index 12c8895735..6991a6854e 100644 --- a/scripts/test-dom-environment.ts +++ b/scripts/test-dom-environment.ts @@ -1,8 +1,23 @@ /** Initial resize delivery from jsdom's modeled offsets; geometry/timing suites supply their own observers. */ import { afterAll, beforeEach } from 'vitest' +// The SVG animation player initializes a transparent 1px Canvas placeholder at import time. +const canvasPrototype = typeof HTMLCanvasElement === 'undefined' ? undefined : HTMLCanvasElement.prototype +const originalCanvasContext = canvasPrototype === undefined ? undefined : Object.getOwnPropertyDescriptor(canvasPrototype, 'getContext') +if (canvasPrototype !== undefined) { + Object.defineProperty(canvasPrototype, 'getContext', { + configurable: true, + value: (contextId: string) => { + if (contextId !== '2d') throw new Error(`test DOM: unsupported Canvas context ${contextId}`) + return { fillStyle: '', fillRect: () => {} } + }, + }) +} + const original = Object.getOwnPropertyDescriptor(globalThis, 'ResizeObserver') let installed = false +const originalMatchMedia = typeof window === 'undefined' ? undefined : Object.getOwnPropertyDescriptor(window, 'matchMedia') +let installedMatchMedia = false class TestResizeObserver implements ResizeObserver { private readonly targets = new Set() @@ -27,6 +42,13 @@ class TestResizeObserver implements ResizeObserver { } beforeEach(() => { + if (typeof window !== 'undefined' && typeof window.matchMedia !== 'function') { + const matchMedia: Window['matchMedia'] = media => Object.assign(new EventTarget(), { + media, matches: false, onchange: null, addListener: () => {}, removeListener: () => {}, + }) + Object.defineProperty(window, 'matchMedia', { configurable: true, writable: true, value: matchMedia }) + installedMatchMedia = true + } if (typeof document !== 'undefined' && typeof ResizeObserver === 'undefined') { // Establish the per-environment default, so vi.unstubAllGlobals restores it. Object.defineProperty(globalThis, 'ResizeObserver', { configurable: true, writable: true, value: TestResizeObserver }) @@ -35,7 +57,16 @@ beforeEach(() => { }) afterAll(() => { - if (!installed) return - if (original === undefined) Reflect.deleteProperty(globalThis, 'ResizeObserver') - else Object.defineProperty(globalThis, 'ResizeObserver', original) + if (canvasPrototype !== undefined) { + if (originalCanvasContext === undefined) Reflect.deleteProperty(canvasPrototype, 'getContext') + else Object.defineProperty(canvasPrototype, 'getContext', originalCanvasContext) + } + if (installed) { + if (original === undefined) Reflect.deleteProperty(globalThis, 'ResizeObserver') + else Object.defineProperty(globalThis, 'ResizeObserver', original) + } + if (installedMatchMedia) { + if (originalMatchMedia === undefined) Reflect.deleteProperty(window, 'matchMedia') + else Object.defineProperty(window, 'matchMedia', originalMatchMedia) + } }) diff --git a/snapshots/web/auto-review-denial/ui.expected.md b/snapshots/web/auto-review-denial/ui.expected.md index 1ceeeb4f1e..d3172476e3 100644 --- a/snapshots/web/auto-review-denial/ui.expected.md +++ b/snapshots/web/auto-review-denial/ui.expected.md @@ -9,8 +9,8 @@ - tab "Trajectory" - text: Inspect the protected operation, but do not run it unless authorized. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Called tools, ran code, ran commands" [expanded] - text: Failed - button "Tool call Rejected by Auto review" @@ -37,8 +37,8 @@ - tab "Trajectory" - text: Inspect the protected operation, but do not run it unless authorized. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Called tools, ran code, ran commands" [expanded] - text: Failed - button "Tool call Rejected by Auto review" [expanded] diff --git a/snapshots/web/bash-abort-row/ui.expected.md b/snapshots/web/bash-abort-row/ui.expected.md index c74b1b8193..e7f584b73c 100644 --- a/snapshots/web/bash-abort-row/ui.expected.md +++ b/snapshots/web/bash-abort-row/ui.expected.md @@ -8,7 +8,7 @@ - text: "Run two shell commands: wait for cancellation, then write skipped.txt. {{clock}}" - button "Copy" - status: Stopped -- button "Stopped" [disabled] [expanded] +- button "Stopped" [expanded] - button "Ran commands" [expanded] - 'button "Failed Bash Error: tool call aborted" [expanded]' - text: "IN { \"command\": \"node -e \\\"const fs=require('node:fs'); fs.writeFileSync('started.tmp', 'started'); fs.renameSync('started.tmp', 'started.txt'); setInterval(() => {}, 1000)\\\"\", \"description\": \"Wait until cancellation\" } OUT Error: tool call aborted" diff --git a/snapshots/web/changed-files-turn/ui.expected.md b/snapshots/web/changed-files-turn/ui.expected.md index 7500599d2e..2f1d34dc98 100644 --- a/snapshots/web/changed-files-turn/ui.expected.md +++ b/snapshots/web/changed-files-turn/ui.expected.md @@ -1,7 +1,7 @@ - text: 不用先查看目录,直接做四件事:把 intro.md 里的标题「示例项目」改成「项目说明」,新建 src/util.ts 导出一个两数相加的 add 函数,新建 app.local 写一行 mode=demo,最后用 bash 在 notes.txt 末尾追加一行 done。 {{clock}} - button "复制" -- status: 已完成工作 -- button "用时 {{duration}}" [expanded] +- status: 已完成 +- button "已完成,用时 {{duration}}" [expanded] - button "执行了命令,已写入文件,已读取文件等" [expanded] - button "思考" - button "读取 intro.md": diff --git a/snapshots/web/deepseek-messages-chat/ui.expected.md b/snapshots/web/deepseek-messages-chat/ui.expected.md index 021809ac0c..56f27b62ba 100644 --- a/snapshots/web/deepseek-messages-chat/ui.expected.md +++ b/snapshots/web/deepseek-messages-chat/ui.expected.md @@ -8,8 +8,8 @@ - tab "轨迹" - text: 只回复 MESSAGES_WEB_READY,不调用工具。 {{clock}} - button "复制" -- status: 已完成工作 -- button "用时 {{duration}}" [disabled] +- status: 已完成 +- button "已完成,用时 {{duration}}" [disabled] - paragraph: MESSAGES_WEB_READY - button "复制" - button "好的回答" diff --git a/snapshots/web/default-workspace/ui.expected.md b/snapshots/web/default-workspace/ui.expected.md index 27cdaa5150..9f26daff62 100644 --- a/snapshots/web/default-workspace/ui.expected.md +++ b/snapshots/web/default-workspace/ui.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: "Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: DONE - button "Copy" - button "Good response" diff --git a/snapshots/web/feedback-command/ack-expanded.expected.md b/snapshots/web/feedback-command/ack-expanded.expected.md index f9d619e382..102788fbbe 100644 --- a/snapshots/web/feedback-command/ack-expanded.expected.md +++ b/snapshots/web/feedback-command/ack-expanded.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Analysis completed" [expanded] - button "Think" - paragraph: LIGHTHOUSE diff --git a/snapshots/web/feedback-command/ack.expected.md b/snapshots/web/feedback-command/ack.expected.md index 716f3bd304..4f55e3e95f 100644 --- a/snapshots/web/feedback-command/ack.expected.md +++ b/snapshots/web/feedback-command/ack.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: LIGHTHOUSE - button "Copy" - button "Good response" diff --git a/snapshots/web/feedback-release/ack-expanded.expected.md b/snapshots/web/feedback-release/ack-expanded.expected.md index f9d619e382..102788fbbe 100644 --- a/snapshots/web/feedback-release/ack-expanded.expected.md +++ b/snapshots/web/feedback-release/ack-expanded.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Analysis completed" [expanded] - button "Think" - paragraph: LIGHTHOUSE diff --git a/snapshots/web/feedback-release/ack.expected.md b/snapshots/web/feedback-release/ack.expected.md index 716f3bd304..4f55e3e95f 100644 --- a/snapshots/web/feedback-release/ack.expected.md +++ b/snapshots/web/feedback-release/ack.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: LIGHTHOUSE - button "Copy" - button "Good response" diff --git a/snapshots/web/file-upload-round/ui.expected.md b/snapshots/web/file-upload-round/ui.expected.md index 0c60a10a69..ff7effe13d 100644 --- a/snapshots/web/file-upload-round/ui.expected.md +++ b/snapshots/web/file-upload-round/ui.expected.md @@ -11,8 +11,8 @@ - img "reference-1.png" - text: Read the attached file with the read tool, reply with exactly the single word it contains, and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: UPLOAD_ROUND_OK - button "Copy" - button "Good response" diff --git a/snapshots/web/fresh-round-trip/ui-expanded.expected.md b/snapshots/web/fresh-round-trip/ui-expanded.expected.md index 0c4d334df3..de867a8745 100644 --- a/snapshots/web/fresh-round-trip/ui-expanded.expected.md +++ b/snapshots/web/fresh-round-trip/ui-expanded.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: "Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Ran commands" [expanded] - button "Think" - button "Bash Echo the test string" diff --git a/snapshots/web/fresh-round-trip/ui.expected.md b/snapshots/web/fresh-round-trip/ui.expected.md index 27cdaa5150..9f26daff62 100644 --- a/snapshots/web/fresh-round-trip/ui.expected.md +++ b/snapshots/web/fresh-round-trip/ui.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: "Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: DONE - button "Copy" - button "Good response" diff --git a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md index 067f2ece45..00385a4c9b 100644 --- a/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui-expanded.expected.md @@ -14,8 +14,8 @@ - button "Continuing goal {{clock}}": - text: Continuing goal - time: {{clock}} -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Ran commands and updated the plan" [expanded] - button "Think" - button "Bash List package directories deterministically" @@ -51,8 +51,8 @@ - button "Continuing goal {{clock}}": - text: Continuing goal - time: {{clock}} -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Ran commands" [expanded] - button "Think" - button "Bash Randomly select another package with Python" diff --git a/snapshots/web/goal-multi-turn-actions/ui.expected.md b/snapshots/web/goal-multi-turn-actions/ui.expected.md index d7d9ad4839..505a067cbb 100644 --- a/snapshots/web/goal-multi-turn-actions/ui.expected.md +++ b/snapshots/web/goal-multi-turn-actions/ui.expected.md @@ -14,8 +14,8 @@ - button "Continuing goal {{clock}}": - text: Continuing goal - time: {{clock}} -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: - strong: Turn 1 / 2 - text: — 随机选中的包: @@ -38,8 +38,8 @@ - button "Continuing goal {{clock}}": - text: Continuing goal - time: {{clock}} -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: 两个 turn 都完成了 ✅ - paragraph: - strong: 总结: @@ -94,7 +94,6 @@ - button "Good response" - button "Bad response" - button "Branch into a new conversation" -- tooltip "Branch into a new conversation" - text: {{clock}} - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" diff --git a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md index 761eb41613..8bf5e6fb79 100644 --- a/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded-expanded.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Analysis completed" [expanded] - button "Think" - paragraph: LIGHTHOUSE diff --git a/snapshots/web/lifecycle-chrome/reloaded.expected.md b/snapshots/web/lifecycle-chrome/reloaded.expected.md index f6a61355d8..6f51412377 100644 --- a/snapshots/web/lifecycle-chrome/reloaded.expected.md +++ b/snapshots/web/lifecycle-chrome/reloaded.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Reply with the single word LIGHTHOUSE and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: LIGHTHOUSE - button "Copy" - button "Good response" diff --git a/snapshots/web/live-interactions/loading.expected.md b/snapshots/web/live-interactions/loading.expected.md index 7976fcca1c..5e83a173bc 100644 --- a/snapshots/web/live-interactions/loading.expected.md +++ b/snapshots/web/live-interactions/loading.expected.md @@ -8,9 +8,9 @@ - tab "Trajectory" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - paragraph: partial +- status: Deep diving +- text: Deep diving for {{duration}} - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/live-interactions/retry-expanded.expected.md b/snapshots/web/live-interactions/retry-expanded.expected.md index ad8e9f8f2b..1fc7f0416b 100644 --- a/snapshots/web/live-interactions/retry-expanded.expected.md +++ b/snapshots/web/live-interactions/retry-expanded.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - group: - status: Retried model request (1/5) · {{duration}} - button "Analysis completed" [expanded] diff --git a/snapshots/web/live-interactions/retry.expected.md b/snapshots/web/live-interactions/retry.expected.md index 33c058c865..abc388a29c 100644 --- a/snapshots/web/live-interactions/retry.expected.md +++ b/snapshots/web/live-interactions/retry.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. - button "Copy" - button "Good response" diff --git a/snapshots/web/live-interactions/running-draft.expected.md b/snapshots/web/live-interactions/running-draft.expected.md index 1bdf81ba91..9e5231c18f 100644 --- a/snapshots/web/live-interactions/running-draft.expected.md +++ b/snapshots/web/live-interactions/running-draft.expected.md @@ -8,9 +8,9 @@ - tab "Trajectory" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - paragraph: partial +- status: Deep diving +- text: Deep diving for {{duration}} - textbox "Message or run a task, / commands, @ files or sessions": - paragraph: Queue this follow-up while the current turn is running. - button "Add files or run commands" diff --git a/snapshots/web/message-actions/ui.expected.md b/snapshots/web/message-actions/ui.expected.md index ed11835a6c..0eee19f56e 100644 --- a/snapshots/web/message-actions/ui.expected.md +++ b/snapshots/web/message-actions/ui.expected.md @@ -13,18 +13,15 @@ - button "Copy" - tooltip "Copy" - status: Stopped -- button "Stopped" [disabled] [expanded] -- button "Analysis completed" -- paragraph: I will read both files before answering. -- button "Read files" +- button "Stopped" - button "Copy" - button "Good response" - button "Bad response" - button "Branch into a new conversation" [disabled] - text: Available only on the last message of a completed turn {{clock}} Now give the final answer. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: DONE - button "Copy" - button "Good response" @@ -32,8 +29,8 @@ - button "Branch into a new conversation" - text: {{clock}} Keep this later input in the original conversation. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - paragraph: ORIGINAL ONLY - button "Copy" - button "Good response" diff --git a/snapshots/web/minimal-preset/ui.expected.md b/snapshots/web/minimal-preset/ui.expected.md index e489e916e6..0469ba0e75 100644 --- a/snapshots/web/minimal-preset/ui.expected.md +++ b/snapshots/web/minimal-preset/ui.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: "Use the bash tool to run exactly: printf 'MINIMAL_BASH_CARD_OK\\n'. Then reply exactly MINIMAL_PRESET_REQUEST_OK and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Ran commands" [expanded] - button "Bash printf 'MINIMAL_BASH_CARD_OK\\n'" [expanded] - text: "IN { \"command\": \"printf 'MINIMAL_BASH_CARD_OK\\\\n'\" } OUT MINIMAL_BASH_CARD_OK [Command finished with exit code 0]" diff --git a/snapshots/web/plan-review/approved-expanded.expected.md b/snapshots/web/plan-review/approved-expanded.expected.md index 6f978228c7..1bfe52e4e6 100644 --- a/snapshots/web/plan-review/approved-expanded.expected.md +++ b/snapshots/web/plan-review/approved-expanded.expected.md @@ -7,8 +7,8 @@ - tab "Trajectory" - text: "plan Plan mode on. Use /plan off to leave. Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Analysis completed" [expanded] - button "Think" - paragraph: diff --git a/snapshots/web/plan-review/approved.expected.md b/snapshots/web/plan-review/approved.expected.md index 279843a249..bab8040ae6 100644 --- a/snapshots/web/plan-review/approved.expected.md +++ b/snapshots/web/plan-review/approved.expected.md @@ -7,8 +7,8 @@ - tab "Trajectory" - text: "plan Plan mode on. Use /plan off to leave. Plan a small change: add a --greeting flag to a CLI. Do not read or write any files. Call exit_plan_mode with a short plan of at most five bullet points. Once the plan is approved, reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: DONE - 'button "Open plan: Add `--greeting` flag to CLI"': "Add `--greeting` flag to CLI Plan · Markdown Open" - button "Copy" diff --git a/snapshots/web/present-svg/ui.expected.md b/snapshots/web/present-svg/ui.expected.md index 08f503ab05..7aeef09f49 100644 --- a/snapshots/web/present-svg/ui.expected.md +++ b/snapshots/web/present-svg/ui.expected.md @@ -1,7 +1,7 @@ - text: 简单画一个 SVG 表示冯诺依曼架构, 保存为 von-neumann.svg {{clock}} - button "复制" -- status: 已完成工作 -- button "用时 {{duration}}" [expanded] +- status: 已完成 +- button "已完成,用时 {{duration}}" [expanded] - button "已完成分析" [expanded] - button "思考" - paragraph: 我来创建一个冯诺依曼架构的 SVG 示意图。 diff --git a/snapshots/web/present/ui.expected.md b/snapshots/web/present/ui.expected.md index 34a2a77047..41d4680b7a 100644 --- a/snapshots/web/present/ui.expected.md +++ b/snapshots/web/present/ui.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: "Use one run_code program to do the following in order. Call present for missing.txt and catch its error without creating that file. Use bash to run exactly `printf \"DELIVERED_REPORT\\n\" > report.txt; printf \"DELIVERED_NOTE\\n\" > 说明.txt`. Call present for report.txt and 说明.txt. After present succeeds, deliberately throw the string \"AFTER_PRESENT\" (not an Error object) from that same run_code program. Do not retry the program or create any other files. Finish by mentioning `report.txt` and `说明.txt` in inline code, and put PRESENT_DONE in a separate paragraph. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Called tools, ran code, ran commands" [expanded] - button "Think" - text: Failed diff --git a/snapshots/web/ptc-round/ui.expected.md b/snapshots/web/ptc-round/ui.expected.md index 986dcf439c..3dbcb5d3ec 100644 --- a/snapshots/web/ptc-round/ui.expected.md +++ b/snapshots/web/ptc-round/ui.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: "Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt catching its error in the program. Return an object with both outcomes. Then reply DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Ran code, ran commands, read files" [expanded] - button "Think" - button "Code Run bash echo and catch missing file read" diff --git a/snapshots/web/question-composer/answered-expanded.expected.md b/snapshots/web/question-composer/answered-expanded.expected.md index 346b77ff8d..b8f6cce9a0 100644 --- a/snapshots/web/question-composer/answered-expanded.expected.md +++ b/snapshots/web/question-composer/answered-expanded.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: "Use the ask_user_question tool to ask me exactly one multi-select question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" Set multi_select to true. After I answer, reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Asked questions" [expanded] - button "Think" - button "Ask question 1/1 answered" [expanded] diff --git a/snapshots/web/question-composer/answered.expected.md b/snapshots/web/question-composer/answered.expected.md index be7b65aa37..6b62b305cb 100644 --- a/snapshots/web/question-composer/answered.expected.md +++ b/snapshots/web/question-composer/answered.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: "Use the ask_user_question tool to ask me exactly one multi-select question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" Set multi_select to true. After I answer, reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: DONE - button "Copy" - button "Good response" diff --git a/snapshots/web/question-composer/cancelled.expected.md b/snapshots/web/question-composer/cancelled.expected.md index 7d5d89a078..647e99e6f5 100644 --- a/snapshots/web/question-composer/cancelled.expected.md +++ b/snapshots/web/question-composer/cancelled.expected.md @@ -7,8 +7,8 @@ - tab "Trajectory" - text: "Use the ask_user_question tool to ask me exactly one multi-select question with id \"color\", question \"Which color do you prefer?\", header \"Pick one\", and two options: label \"Blue\" with description \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\", and label \"Green\" with description \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\" Set multi_select to true. After I answer, reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Asked questions" [expanded] - button "Think" - button "Ask question cancelled" [expanded] diff --git a/snapshots/web/queue-actions/collapsed.expected.md b/snapshots/web/queue-actions/collapsed.expected.md index 64738a0101..9c17d3c73c 100644 --- a/snapshots/web/queue-actions/collapsed.expected.md +++ b/snapshots/web/queue-actions/collapsed.expected.md @@ -8,9 +8,9 @@ - tab "Trajectory" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - paragraph: partial +- status: Deep diving +- text: Deep diving for {{duration}} - button "2 queued messages" - textbox "Cmd/Ctrl+Enter steers all queued messages" - button "Add files or run commands" diff --git a/snapshots/web/queue-actions/editing.expected.md b/snapshots/web/queue-actions/editing.expected.md index d375969b1e..dbd34d30b8 100644 --- a/snapshots/web/queue-actions/editing.expected.md +++ b/snapshots/web/queue-actions/editing.expected.md @@ -8,9 +8,9 @@ - tab "Trajectory" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - paragraph: partial +- status: Deep diving +- text: Deep diving for {{duration}} - button "2 queued messages" [disabled] [expanded] - list: - listitem: diff --git a/snapshots/web/queue-actions/layout.expected.md b/snapshots/web/queue-actions/layout.expected.md index 2455bfb385..6d1d9a2f93 100644 --- a/snapshots/web/queue-actions/layout.expected.md +++ b/snapshots/web/queue-actions/layout.expected.md @@ -11,9 +11,9 @@ - button "Continuing goal {{clock}}": - text: Continuing goal - time: {{clock}} -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - paragraph: partial +- status: Deep diving +- text: Deep diving for {{duration}} - region "To-dos": - button "To-dos 1 completed · 1 in progress" - text: Ongoing Goal Keep the composer context panels aligned diff --git a/snapshots/web/queue-actions/sending.expected.md b/snapshots/web/queue-actions/sending.expected.md index 594d0053e8..1ec3f95c7b 100644 --- a/snapshots/web/queue-actions/sending.expected.md +++ b/snapshots/web/queue-actions/sending.expected.md @@ -8,9 +8,9 @@ - tab "Trajectory" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - paragraph: partial +- status: Deep diving +- text: Deep diving for {{duration}} - list: - listitem: - text: Queue item to remove diff --git a/snapshots/web/queue-actions/ui.expected.md b/snapshots/web/queue-actions/ui.expected.md index 6dc03fa797..2de2ae656f 100644 --- a/snapshots/web/queue-actions/ui.expected.md +++ b/snapshots/web/queue-actions/ui.expected.md @@ -8,9 +8,9 @@ - tab "Trajectory" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - paragraph: partial +- status: Deep diving +- text: Deep diving for {{duration}} - list: - listitem: - text: Edited queue item diff --git a/snapshots/web/queued-image/delivered.expected.md b/snapshots/web/queued-image/delivered.expected.md index d014db4b09..7d06bd0b91 100644 --- a/snapshots/web/queued-image/delivered.expected.md +++ b/snapshots/web/queued-image/delivered.expected.md @@ -25,8 +25,8 @@ - img "queued.png" - text: Compare with this screenshot {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. - button "Copy" - button "Good response" @@ -34,8 +34,8 @@ - button "Branch into a new conversation" - text: {{clock}} Continue with the queued comparison {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. - button "Copy" - button "Good response" diff --git a/snapshots/web/queued-image/queued.expected.md b/snapshots/web/queued-image/queued.expected.md index 6496bb83f5..7ef1375487 100644 --- a/snapshots/web/queued-image/queued.expected.md +++ b/snapshots/web/queued-image/queued.expected.md @@ -8,9 +8,9 @@ - tab "Trajectory" - text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - paragraph: partial +- status: Deep diving +- text: Deep diving for {{duration}} - list: - listitem: - img "Queued message image" diff --git a/snapshots/web/seeded-history/command-row.expected.md b/snapshots/web/seeded-history/command-row.expected.md index 86f3b1a9cc..1bf37d80f4 100644 --- a/snapshots/web/seeded-history/command-row.expected.md +++ b/snapshots/web/seeded-history/command-row.expected.md @@ -10,8 +10,8 @@ - button "Jump to turn 2" - text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Read files" [expanded] - button "Think" - button "Read a.txt": @@ -28,8 +28,8 @@ - button "Branch into a new conversation" - text: {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" - 'button "Access mode, current: Read Only"': Read Only diff --git a/snapshots/web/seeded-history/feedback-row.expected.md b/snapshots/web/seeded-history/feedback-row.expected.md index 769c31023f..a0d3f44eed 100644 --- a/snapshots/web/seeded-history/feedback-row.expected.md +++ b/snapshots/web/seeded-history/feedback-row.expected.md @@ -10,8 +10,8 @@ - button "Jump to turn 2" - text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Read files" [expanded] - button "Think" - button "Read a.txt": @@ -28,8 +28,8 @@ - button "Branch into a new conversation" - text: {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - 'button "feedback Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}." [expanded]' - text: "Feedback recorded for session {{seededId}} Anonymous user: {{uuid}}." - textbox "Message or run a task, / commands, @ files or sessions" diff --git a/snapshots/web/seeded-history/ui-expanded.expected.md b/snapshots/web/seeded-history/ui-expanded.expected.md index 6cbe989134..9c3baf29a6 100644 --- a/snapshots/web/seeded-history/ui-expanded.expected.md +++ b/snapshots/web/seeded-history/ui-expanded.expected.md @@ -10,8 +10,8 @@ - button "Jump to turn 2" - text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Read files" [expanded] - button "Think" - button "Read a.txt": @@ -28,8 +28,8 @@ - button "Branch into a new conversation" - text: {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/seeded-history/ui.expected.md b/snapshots/web/seeded-history/ui.expected.md index b1842edc41..d2c53e99b2 100644 --- a/snapshots/web/seeded-history/ui.expected.md +++ b/snapshots/web/seeded-history/ui.expected.md @@ -10,8 +10,8 @@ - button "Jump to turn 2" - text: "Use the read tool twice in one assistant message: read a.txt and b.txt. Then reply with the single word DONE and stop. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: DONE - button "Copy" - button "Good response" @@ -19,8 +19,8 @@ - button "Branch into a new conversation" - text: {{clock}} - button "compact Compacted 5 history items (~{{tokens}} tokens)" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/skill-tool-row/ui.expected.md b/snapshots/web/skill-tool-row/ui.expected.md index 6e08ccbd47..7dde4db16f 100644 --- a/snapshots/web/skill-tool-row/ui.expected.md +++ b/snapshots/web/skill-tool-row/ui.expected.md @@ -7,8 +7,8 @@ - tab "Trajectory" - text: Load the editing-cordis-compositions skill with the skill tool, then reply DONE. {{date}} {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Called tools" [expanded] - button "Think" - button "Skill editing-cordis-compositions" [expanded] diff --git a/snapshots/web/steering/mid-steer.expected.md b/snapshots/web/steering/mid-steer.expected.md index 0a731c2f3e..29ee449f59 100644 --- a/snapshots/web/steering/mid-steer.expected.md +++ b/snapshots/web/steering/mid-steer.expected.md @@ -8,11 +8,11 @@ - tab "Trajectory" - text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}} - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - button "Waiting for your action" - text: "Interjection: include the word BANANA in your final reply." - button "Copy" +- status: Deep diving +- text: Deep diving for {{duration}} - region "Ready to continue?": - text: Checkpoint - heading "Ready to continue?" [level=2] diff --git a/snapshots/web/steering/settled-expanded.expected.md b/snapshots/web/steering/settled-expanded.expected.md index b04beec4bb..6ccbadfd62 100644 --- a/snapshots/web/steering/settled-expanded.expected.md +++ b/snapshots/web/steering/settled-expanded.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - button "Asked questions" [expanded] - button "Ask question 1/1 answered" - text: "Interjection: include the word BANANA in your final reply. {{clock}}" diff --git a/snapshots/web/steering/settled.expected.md b/snapshots/web/steering/settled.expected.md index 030841b7e4..b13cb9a2f9 100644 --- a/snapshots/web/steering/settled.expected.md +++ b/snapshots/web/steering/settled.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - button "Asked questions" - text: "Interjection: include the word BANANA in your final reply. {{clock}}" - button "Copy" diff --git a/snapshots/web/subagent-conversation/nested.expected.md b/snapshots/web/subagent-conversation/nested.expected.md index c04320b0b6..01e95cd04d 100644 --- a/snapshots/web/subagent-conversation/nested.expected.md +++ b/snapshots/web/subagent-conversation/nested.expected.md @@ -12,8 +12,8 @@ - tab "Trajectory" - text: Give one concrete event sourcing example. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [disabled] +- status: Completed +- button "Completed in {{duration}}" [disabled] - status: - strong: This subagent is read-only for now - text: The parent session is offline; reopen it to continue sending messages. diff --git a/snapshots/web/subagent-conversation/sidebar-chat.expected.md b/snapshots/web/subagent-conversation/sidebar-chat.expected.md index 607e142b62..748cd0db25 100644 --- a/snapshots/web/subagent-conversation/sidebar-chat.expected.md +++ b/snapshots/web/subagent-conversation/sidebar-chat.expected.md @@ -1,7 +1,7 @@ - text: "Explain event sourcing in one sentence.Your parent agent id is \"session-{{uuid}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"session-{{uuid}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. - button "Copy" - button "Good response" diff --git a/snapshots/web/subagent-conversation/ui-expanded.expected.md b/snapshots/web/subagent-conversation/ui-expanded.expected.md index 625180666b..56cb24ea87 100644 --- a/snapshots/web/subagent-conversation/ui-expanded.expected.md +++ b/snapshots/web/subagent-conversation/ui-expanded.expected.md @@ -15,8 +15,8 @@ - button "Jump to turn 2" - text: "Explain event sourcing in one sentence.Your parent agent id is \"session-{{uuid}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"session-{{uuid}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Analysis completed" [expanded] - button "Think" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. @@ -26,8 +26,8 @@ - button "Branch into a new conversation" - text: {{clock}} Now give the same explanation to a human reader. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Analysis completed" [expanded] - button "Think" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. diff --git a/snapshots/web/subagent-conversation/ui.expected.md b/snapshots/web/subagent-conversation/ui.expected.md index 6e67c79557..fd6280355e 100644 --- a/snapshots/web/subagent-conversation/ui.expected.md +++ b/snapshots/web/subagent-conversation/ui.expected.md @@ -15,8 +15,8 @@ - button "Jump to turn 2" - text: "Explain event sourcing in one sentence.Your parent agent id is \"session-{{uuid}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"session-{{uuid}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. - button "Copy" - button "Good response" @@ -24,8 +24,8 @@ - button "Branch into a new conversation" - text: {{clock}} Now give the same explanation to a human reader. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. - button "Copy" - button "Good response" diff --git a/snapshots/web/subagent-interrupt/offline-composer.expected.md b/snapshots/web/subagent-interrupt/offline-composer.expected.md index c6e866c5e2..9b0d6b0309 100644 --- a/snapshots/web/subagent-interrupt/offline-composer.expected.md +++ b/snapshots/web/subagent-interrupt/offline-composer.expected.md @@ -11,9 +11,9 @@ - tab "Trajectory" - text: "Explain event sourcing in one sentence.Your parent agent id is \"session-{{uuid}}\". Before you finish, send your result to that agent with send_message({ agent_id: \"session-{{uuid}}\", message: \"\" }). The parent shares your workspace but does not automatically receive your transcript, tool output, or reasoning. Send earlier messages as well when a finding changes what the parent should do next; sending a message does not end your turn. {{clock}}" - button "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - paragraph: partial +- status: Deep diving +- text: Deep diving for {{duration}} - list: - listitem: - text: Keep working until I stop you again. diff --git a/snapshots/web/turn-tail-actions/completed.expected.md b/snapshots/web/turn-tail-actions/completed.expected.md index 75ee221050..5d309edf46 100644 --- a/snapshots/web/turn-tail-actions/completed.expected.md +++ b/snapshots/web/turn-tail-actions/completed.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: DONE - button "Copy" - button "Good response" diff --git a/snapshots/web/turn-tail-actions/focused.expected.md b/snapshots/web/turn-tail-actions/focused.expected.md index a31f0d0c79..353f648456 100644 --- a/snapshots/web/turn-tail-actions/focused.expected.md +++ b/snapshots/web/turn-tail-actions/focused.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Analysis completed" - paragraph: Reading the workspace now. - button "Ran commands" [expanded] diff --git a/snapshots/web/turn-tail-actions/running.expected.md b/snapshots/web/turn-tail-actions/running.expected.md index 8a9f36aee9..e8cd6fbf1d 100644 --- a/snapshots/web/turn-tail-actions/running.expected.md +++ b/snapshots/web/turn-tail-actions/running.expected.md @@ -9,12 +9,12 @@ - text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}} - button "Copy" - tooltip "Copy" -- status: Deep diving... -- button "Deep diving for {{duration}}" [disabled] [expanded] - button "Analysis completed" - paragraph: Reading the workspace now. - button "Ran commands" - paragraph: partial +- status: Deep diving +- text: Deep diving for {{duration}} - textbox "Message or run a task, / commands, @ files or sessions" - button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write diff --git a/snapshots/web/turn-tail-actions/settled.expected.md b/snapshots/web/turn-tail-actions/settled.expected.md index cbe5273748..a626148bfa 100644 --- a/snapshots/web/turn-tail-actions/settled.expected.md +++ b/snapshots/web/turn-tail-actions/settled.expected.md @@ -9,10 +9,7 @@ - text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}} - button "Copy" - status: Stopped -- button "Stopped" [disabled] [expanded] -- button "Analysis completed" -- paragraph: Reading the workspace now. -- button "Ran commands" +- button "Stopped" - paragraph: partial - text: Stopped - button "Copy" diff --git a/snapshots/web/turn-tail-actions/usage-expanded.expected.md b/snapshots/web/turn-tail-actions/usage-expanded.expected.md index 8ced72edd1..791695b15c 100644 --- a/snapshots/web/turn-tail-actions/usage-expanded.expected.md +++ b/snapshots/web/turn-tail-actions/usage-expanded.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Begin your reply with the plain sentence "Reading the workspace now." as text, and in that same message call the bash tool with the command "echo alpha". After the tool result, reply with the single word DONE and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" +- status: Completed +- button "Completed in {{duration}}" - paragraph: DONE - button "Copy" - button "Good response" diff --git a/snapshots/web/web-search-round/ui.expected.md b/snapshots/web/web-search-round/ui.expected.md index f229309b8c..92cc1ce25e 100644 --- a/snapshots/web/web-search-round/ui.expected.md +++ b/snapshots/web/web-search-round/ui.expected.md @@ -8,8 +8,8 @@ - tab "Trajectory" - text: Use web_search once with queries ["DeepSeek Harness snapshot search","DeepSeek Harness multi-query search"]. Then reply exactly SEARCH_DONE and stop. {{clock}} - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Searched the web" [expanded] - button "Search DeepSeek Harness snapshot search, DeepSeek Harness multi-query search" - paragraph: SEARCH_DONE diff --git a/snapshots/web/workflow-run/ui.expected.md b/snapshots/web/workflow-run/ui.expected.md index 12ffccb420..c760b4962d 100644 --- a/snapshots/web/workflow-run/ui.expected.md +++ b/snapshots/web/workflow-run/ui.expected.md @@ -1,7 +1,7 @@ - text: "Use the workflow tool exactly once, with args omitted, meta set to { \"name\": \"snapshot-flow\", \"description\": \"one child for the snapshot\" }, and this EXACT script body (copy it verbatim): phase('Run') const reply = await agent('Reply with exactly the word WF_CHILD_OK and nothing else.') return { reply } After the workflow returns, reply with the single word WORKFLOW_DONE and stop. Do not use any other tool. {{clock}}" - button "Copy" -- status: Worked -- button "Took {{duration}}" [expanded] +- status: Completed +- button "Completed in {{duration}}" [expanded] - button "Called tools" [expanded] - button "Think" - button "Run workflow snapshot-flow" diff --git a/tsconfig.host.json b/tsconfig.host.json index 2d31d0df31..08de88abdc 100644 --- a/tsconfig.host.json +++ b/tsconfig.host.json @@ -29,6 +29,7 @@ "apps/web/tests/streaming-fence-highlight.e2e.ts", "apps/web/tests/declared-reasoning.e2e.ts", "apps/web/tests/support.ts", + "apps/web/tests/shimmer.ts", "apps/web/tests/model-input-layout.ts", "apps/web/tests/scaffold-hermetic.e2e.ts", "apps/web/tests/startup-rpc-budget.e2e.ts",