Merge remote-tracking branch 'origin/master' into fix/master-ci-windows-acl

This commit is contained in:
turtle1999 committed 2026-09-28 22:59:47 +08:00
commit 4bcd820889
175 files changed
+787 -623

No files matched your search

+4 -2
View File
@@ -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<HTMLElement>('[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
@@ -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
@@ -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 I will inspect the current Session's dynamic Cordis Plugins before defining the snapshot Package."
- 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"
@@ -20,8 +20,6 @@
- button "减小字号"
- text: px 工作步骤展示 选择希望看到多少工具调用细节
- button "标准"
- text: 性能与用量 选择性能与用量信息展示的详细程度
- button "详细"
- text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换
- switch "代码工作工具" [checked]
- text: 快捷键
@@ -31,4 +29,6 @@
- button "排队发送"
- text: 在使用官方模型 API 时上传 Session Log 帮助改进 DeepSeek 模型与产品
- switch "在使用官方模型 API 时上传 Session Log"
- text: 性能与用量 选择性能与用量信息展示的详细程度
- button "详细"
- text: 当前版本:{{version}}
@@ -20,8 +20,6 @@
- button "Decrease font size"
- text: px Work details Choose how much detail to show for tool calls
- button "Standard"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
- switch "Coding Tools" [checked]
- text: Keyboard shortcuts
@@ -31,4 +29,6 @@
- button "Queue"
- text: Upload Session Log when using the official model API Help improve DeepSeek models and products.
- switch "Upload Session Log when using the official model API"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: "Current version: {{version}}"
@@ -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}}"
- status: Completed
- button "Completed in {{duration}}"
- 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"
@@ -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"
@@ -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"
@@ -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: 注意:
@@ -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"':
@@ -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:"
@@ -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
@@ -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
@@ -20,8 +20,6 @@
- button "Decrease font size"
- text: px Work details Choose how much detail to show for tool calls
- button "Detailed"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
- switch "Coding Tools"
- text: Keyboard shortcuts
@@ -31,4 +29,6 @@
- button "Queue"
- text: Upload Session Log when using the official model API Help improve DeepSeek models and products.
- switch "Upload Session Log when using the official model API"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: "Current version: {{version}}"
@@ -20,8 +20,6 @@
- button "Decrease font size"
- text: px Work details Choose how much detail to show for tool calls
- button "Detailed"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: Coding Tools Shows trajectory, code diffs, and Agent preset switching in new chats
- switch "Coding Tools"
- text: Keyboard shortcuts
@@ -31,4 +29,6 @@
- button "Queue"
- text: Upload Session Log when using the official model API Help improve DeepSeek models and products.
- switch "Upload Session Log when using the official model API"
- text: Performance & usage Choose how much performance and usage information to show
- button "Detailed"
- text: "Current version: {{version}}"
@@ -20,8 +20,6 @@
- button "减小字号"
- text: px 工作步骤展示 选择希望看到多少工具调用细节
- button "详细"
- text: 性能与用量 选择性能与用量信息展示的详细程度
- button "详细"
- text: 代码工作工具 开启后显示轨迹、本轮代码差异,新对话中的 Agent 预设切换
- switch "代码工作工具"
- text: 快捷键
@@ -31,4 +29,6 @@
- button "排队发送"
- text: 在使用官方模型 API 时上传 Session Log 帮助改进 DeepSeek 模型与产品
- switch "在使用官方模型 API 时上传 Session Log"
- text: 性能与用量 选择性能与用量信息展示的详细程度
- button "详细"
- text: 当前版本:{{version}}
@@ -23,7 +23,7 @@
- text: Add workspace ⌥ ⌘ O
- listitem:
- button "Edit shortcut for Rename session"
- text: Rename session ⇧ ⌘ R
- text: Rename session ⌥ ⌘ G
- listitem:
- button "Edit shortcut for Fork session"
- text: Fork session ⇧ ⌘ F
@@ -32,7 +32,7 @@
- text: Archive session ⌥ ⌘ A
- listitem:
- button "Edit shortcut for Open settings"
- text: Open settings ⌘ ,
- text: Open settings ⌥ ⌘ ,
- listitem:
- button "Edit shortcut for Toggle right sidebar"
- text: Toggle right sidebar ⇧ ⌘ B
@@ -23,7 +23,7 @@
- text: Add workspace Ctrl + Alt + O
- listitem:
- button "Edit shortcut for Rename session"
- text: Rename session Ctrl + Shift + R
- text: Rename session Ctrl + Alt + G
- listitem:
- button "Edit shortcut for Fork session"
- text: Fork session Ctrl + Shift + F
@@ -32,7 +32,7 @@
- text: Archive session Ctrl + Alt + A
- listitem:
- button "Edit shortcut for Open settings"
- text: Open settings Ctrl + ,
- text: Open settings Ctrl + Alt + ,
- listitem:
- button "Edit shortcut for Toggle right sidebar"
- text: Toggle right sidebar Ctrl + Shift + B
@@ -23,7 +23,7 @@
- text: 添加工作区 ⌥ ⌘ O
- listitem:
- button "修改重命名会话快捷键"
- text: 重命名会话 ⇧ ⌘ R
- text: 重命名会话 ⌥ ⌘ G
- listitem:
- button "修改分叉会话快捷键"
- text: 分叉会话 ⇧ ⌘ F
@@ -32,7 +32,7 @@
- text: 归档会话 ⌥ ⌘ A
- listitem:
- button "修改打开设置快捷键"
- text: 打开设置 ⌘ ,
- text: 打开设置 ⌥ ⌘ ,
- listitem:
- button "修改展开/收起右侧栏快捷键"
- text: 展开/收起右侧栏 ⇧ ⌘ B
@@ -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"
@@ -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"
@@ -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"
@@ -13,8 +13,8 @@
- button "Copy"
- text: "Interjection: include the word ORANGE in your final reply."
- button "Copy"
- status: Deep diving...
- text: Deep diving for {{duration}}...
- 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
@@ -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 The user wants me to ask them a checkpoint question first, then continue with whatever they interject. Let me do exactly that."
- button "Ask question 1/1 answered"
@@ -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"
@@ -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 [10, 14, 22]) {
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"]',
+38 -4
View File
@@ -161,7 +161,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
() => page.getByRole('status').filter({ hasText: 'Deep diving' }).isVisible(),
{ timeout: 10_000 },
).toBe(true)
await page.locator('[data-streaming="true"]')
await page.locator('[data-streaming="true"]:not([inert] *)')
.getByText('partial', { exact: true })
.waitFor({ timeout: 30_000 })
const loadingSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
@@ -195,7 +195,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
// (awaited above), but the abort frame reaches the browser over SSE — the
// frozen-partial swap is eventually consistent, so poll rather than count.
await expect.poll(() => page.locator('[data-composer-input]').first().isEnabled(), { timeout: 10_000 }).toBe(true)
await expect.poll(() => page.locator('[data-streaming="true"]').count(), { timeout: 10_000 }).toBe(0)
await expect.poll(() => page.locator('[data-streaming="true"]:not([inert] *)').count(), { timeout: 10_000 }).toBe(0)
await expect.poll(() => page.getByRole('tooltip').count()).toBe(0)
// Golden of the aborted end-state: the prompt bubble plus the frozen
// partial ('partial' is the hang entry's replayed prefix) and no more.
@@ -222,7 +222,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
// AUTH is outside llm-retry's retryable set: no retry record.
expect(sessionEvents.filter(e => e.type === 'llm/retry').length).toBe(0)
await expect.poll(() => page.locator('[data-composer-input]').first().isEnabled(), { timeout: 10_000 }).toBe(true)
expect(await page.locator('[data-streaming="true"]').count()).toBe(0)
expect(await page.locator('[data-streaming="true"]:not([inert] *)').count()).toBe(0)
const errorStatus = page.getByRole('status').filter({ hasText: 'This turn failed' })
await errorStatus.waitFor({ timeout: 10_000 })
expect(await errorStatus.textContent()).toContain('API key is invalid')
@@ -292,6 +292,40 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
expect(tripwire.warnings).toEqual([])
}, 120_000)
it.skipIf(MODE === 'record')('wraps an active retry label inside a narrow conversation column', async () => {
await launch(() => ({ patches: [{ at: 0, entry: { kind: 'throw', chunks: [], message: 'upstream 503', code: 'SERVER' } }] }),
{ mode: 'normal', maxRetries: 1, retryableCodes: ['SERVER'],
backoff: { initialDelayMs: 60_000, maxDelayMs: 60_000, jitterRatio: 0 } })
await scaffold!.ctx.settings.update('ui-theme', { fontSize: 17 })
const { settled } = await sendPrompt(90_000)
const retry = page.locator('details[data-active="true"]')
await retry.waitFor()
const originalStyle = await retry.getAttribute('style')
try {
const geometry = await retry.evaluate((element) => {
element.style.width = '200px'
const summary = element.querySelector('summary')!
const status = summary.querySelector('[role="status"]')!
return { width: summary.getBoundingClientRect().width,
scrollWidth: summary.scrollWidth, clientWidth: summary.clientWidth,
height: status.getBoundingClientRect().height,
lineHeight: Number.parseFloat(getComputedStyle(status).lineHeight) }
})
expect(geometry.width).toBeLessThanOrEqual(200)
expect(geometry.scrollWidth).toBe(geometry.clientWidth)
expect(geometry.height).toBeGreaterThan(geometry.lineHeight)
expect(await retry.getByRole('status').textContent()).toContain('Retrying')
} finally {
await retry.evaluate((element, style) => {
if (style === null) element.removeAttribute('style')
else element.setAttribute('style', style)
}, originalStyle)
await page.getByRole('button', { name: 'Stop generating', exact: true }).click()
await settled
}
expect(tripwire.pageErrors).toEqual([])
})
it.skipIf(MODE === 'record')('recovers a transient SERVER failure through llm-retry and completes', async () => {
const derived = deriveReplayScript(parseSessionLog(await readFile(FIXTURE, 'utf8')))
expect(derived).toHaveLength(1)
@@ -366,7 +400,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
expect(turnEndReasons(sessionEvents).at(-1)).toBe('error')
expect(sessionEvents.filter(e => e.type === 'llm/retry').length).toBe(2)
await expect.poll(() => page.locator('[data-composer-input]').first().isEnabled(), { timeout: 10_000 }).toBe(true)
expect(await page.locator('[data-streaming="true"]').count()).toBe(0)
expect(await page.locator('[data-streaming="true"]:not([inert] *)').count()).toBe(0)
// The terminal error row must render even though the turn owns a retry
// chain: exhausted recovery shares the failing turn, so suppressing the
// row by retry history would leave the failure invisible.
-1
View File
@@ -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
+7
View File
@@ -76,6 +76,11 @@ it.each([
if (await group.getAttribute('aria-expanded') !== 'true') await group.click()
await page.getByRole('treeitem').nth(1).click()
await page.getByText('DONE', { exact: true }).waitFor()
const renameDialog = page.getByRole('dialog', { name: 'Rename session', exact: true })
await deliverPrimary('KeyG', true)
await renameDialog.getByLabel('Session name').waitFor()
await page.keyboard.press('Escape')
await renameDialog.waitFor({ state: 'hidden' })
const composer = page.locator('[data-composer-input]').first()
await composer.focus()
await page.keyboard.insertText('Desktop focus draft')
@@ -272,6 +277,8 @@ it.each([
} satisfies DesktopShortcutInput)
await page.getByText('DONE', { exact: true }).waitFor({ state: 'hidden' })
await expect.poll(() => page.getByRole('treeitem', { selected: true }).count()).toBe(0)
await deliverPrimary('KeyG', true)
expect(await renameDialog.count()).toBe(0)
await page.getByRole('treeitem').nth(1).click()
await page.getByText('DONE', { exact: true }).waitFor()
await openReference()
+25
View File
@@ -249,6 +249,29 @@ it.skipIf(mode === 'record').each([
if (await group.getAttribute('aria-expanded') !== 'true') await group.click()
await page.getByRole('treeitem').nth(1).click()
await page.getByText('DONE', { exact: true }).waitFor()
// Synthetic input observes browser ownership without navigating the fixture away.
expect(await page.evaluate((primary) => {
return [{ key: 'R', code: 'KeyR', shiftKey: true }, { key: ',', code: 'Comma', shiftKey: false }].map((key) => {
const event = new KeyboardEvent('keydown', { ...key,
metaKey: primary === 'Meta', ctrlKey: primary === 'Control', bubbles: true, cancelable: true })
document.activeElement!.dispatchEvent(event)
return event.defaultPrevented
})
}, primary)).toEqual([false, false])
expect(await page.getByRole('dialog').count()).toBe(0)
const rename = page.getByRole('dialog', { name: 'Rename session', exact: true })
expect(await rename.count()).toBe(0)
await page.keyboard.press(`${primary}+Alt+G`)
await rename.getByLabel('Session name').waitFor()
await page.keyboard.press('Escape')
await rename.waitFor({ state: 'hidden' })
await page.keyboard.press(`${primary}+Alt+Comma`)
const settings = page.getByRole('dialog', { name: 'Settings', exact: true })
await settings.waitFor()
await page.keyboard.press(`${primary}+Alt+G`)
expect(await rename.count()).toBe(0)
await page.keyboard.press('Escape')
await settings.waitFor({ state: 'hidden' })
await page.keyboard.press(`${primary}+Slash`)
const reference = page.getByRole('dialog', { name: 'Keyboard shortcuts', exact: true })
await reference.waitFor()
@@ -312,6 +335,8 @@ it.skipIf(mode === 'record').each([
await page.getByText('DONE', { exact: true }).waitFor({ state: 'hidden' })
await expect.poll(() => page.getByRole('treeitem', { selected: true }).count()).toBe(0)
await page.locator('[data-composer-input]').first().waitFor()
await page.keyboard.press(`${primary}+Alt+G`)
expect(await rename.count()).toBe(0)
expect(console.pageErrors).toEqual([])
} finally {
await browser?.close()
+6 -6
View File
@@ -32,8 +32,8 @@ describe('web e2e: shortcut reference', () => {
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
const trigger = page.getByRole('button', { name: settings, exact: true })
expect(await trigger.getAttribute('aria-keyshortcuts')).toBe(`${key}+,`)
const settingsHint = `${settings} ${key === 'Meta' ? '⌘ ,' : 'Ctrl + ,'}`
expect(await trigger.getAttribute('aria-keyshortcuts')).toBe(key === 'Meta' ? 'Alt+Meta+,' : 'Control+Alt+,')
const settingsHint = `${settings} ${key === 'Meta' ? '⌥ ⌘ ,' : 'Ctrl + Alt + ,'}`
await trigger.hover()
await page.getByRole('tooltip', { name: settingsHint, exact: true }).waitFor()
await trigger.click()
@@ -193,7 +193,7 @@ describe('web e2e: shortcut reference', () => {
expect(await dialog.getByRole('listitem').count()).toBe(1)
expect(await dialog.getByRole('listitem').textContent()).toContain(locale === 'zh-CN' ? '展开/收起左侧栏' : 'Toggle left sidebar')
}
await page.keyboard.press(`${key}+Comma`)
await page.keyboard.press(`${key}+Alt+Comma`)
expect(await referenceNode!.evaluate(element => element.isConnected)).toBe(true)
expect(await page.getByRole('dialog').count()).toBe(2)
expect(await page.getByRole('searchbox').inputValue()).toBe('toggle left sidebar')
@@ -205,7 +205,7 @@ describe('web e2e: shortcut reference', () => {
expect(await page.getByRole('button', { name: view, exact: true }).evaluate(element => element === document.activeElement)).toBe(true)
expect(await page.getByRole('button', { name: view, exact: true }).getAttribute('data-dsh-automatic-focus')).toBe('')
expect(await page.getByRole('button', { name: view, exact: true }).evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none')
await page.keyboard.press(`${key}+Comma`)
await page.keyboard.press(`${key}+Alt+Comma`)
await page.getByRole('dialog', { name: settings, exact: true }).waitFor({ state: 'hidden' })
expect(await page.getByRole('dialog').count()).toBe(0)
expect(await trigger.evaluate(element => element === document.activeElement)).toBe(true)
@@ -213,7 +213,7 @@ describe('web e2e: shortcut reference', () => {
expect(await trigger.evaluate(element => getComputedStyle(element).outlineStyle)).toBe('none')
await page.keyboard.press(referenceKey)
await dialog.waitFor()
await page.keyboard.press(`${key}+Comma`)
await page.keyboard.press(`${key}+Alt+Comma`)
expect(await page.getByRole('dialog', { name: settings, exact: true }).isVisible()).toBe(false)
expect(await page.getByRole('dialog').count()).toBe(1)
expect(await searchInput.evaluate(element => element === document.activeElement)).toBe(true)
@@ -308,7 +308,7 @@ describe('web e2e: shortcut reference', () => {
await dialog.getByRole('button', { name: locale === 'zh-CN' ? '恢复默认' : 'Restore default', exact: true }).click()
await dialog.getByRole('group').waitFor({ state: 'hidden' })
await page.keyboard.press('Escape')
expect(await trigger.getAttribute('aria-keyshortcuts')).toBe(`${key}+,`)
expect(await trigger.getAttribute('aria-keyshortcuts')).toBe(key === 'Meta' ? 'Alt+Meta+,' : 'Control+Alt+,')
await trigger.hover()
await page.getByRole('tooltip', { name: settingsHint, exact: true }).waitFor()
const sidebar = page.getByRole('button', { name: locale === 'zh-CN' ? '收起侧边栏' : 'Collapse sidebar', exact: true })
@@ -12,8 +12,8 @@
- button "Wrap lines" [pressed]
- button "Copy"
- code: "const first: number = 1 const second = \"two\" let tail"
- status: Deep diving...
- text: Deep diving for {{duration}}...
- 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
+5 -2
View File
@@ -136,7 +136,7 @@ export async function expandTurnProcesses(page: Page): Promise<void> {
}
/**
* 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.
*/
@@ -145,7 +145,10 @@ export async function expandOwningTurnProcess(page: Page, target: Locator): Prom
const turn = await target.evaluate(element => element.closest<HTMLElement>('[data-chat-turn]')?.dataset.chatTurn)
if (turn !== undefined) {
const control = page.locator(`[data-turn-process="${turn}"]`)
if (await control.count() > 0 && 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()
+2 -2
View File
@@ -15,8 +15,8 @@
en: cd8120cba5566e19
zh: 65359436088c8bc5
/dsh-unified-corner-radius-standard/select-the-radius:
en: 51cd57ec0fe8bb03
zh: 53784eee8aa8be6e
en: 06ddd6bf9ef1e48c
zh: 4c42d0c9266a00e8
/dsh-unified-corner-radius-standard/reuse-tokens-and-preserve-deliberate-shapes:
en: fb6d50f3139ad186
zh: 668cb0c02bd22167
+1 -1
View File
@@ -40,7 +40,7 @@ Prefer an existing shared size variant when a proposed control falls between the
Concrete defaults: `Button` uses H28/R8 for `sm` and H36/R12 for `md`, including outline variants. Standard H32 inputs and H36 selectors use R12. The Workspace files and New terminal guide entries both use R20, including title-only and described variants. Reserve R28 for their enclosing panel or dialog, not those entries.
Code blocks, diffs, and file previews share R16: changed-file entry cards, delivered-file cards, generic file attachment cards, hover preview panels, and the file-icon tiles inside entry cards. Their hover fills keep the same contour.
Code blocks, diffs, and file previews share R16: changed-file entry cards, delivered-file cards, generic file attachment cards, and hover preview panels. The 40px file-icon tiles inside changed-file and delivery cards use R10 on all four corners as a component-specific exception. Their hover fills keep the same contour.
<a id="shapes"></a>
## Reuse tokens and preserve deliberate shapes
+1 -1
View File
@@ -40,7 +40,7 @@ H 表示以 CSS 像素计、包含边框的设计外部高度。R 表示浏览
具体默认值:`Button` 的 `sm` 使用 H28/R8,`md` 使用 H36/R12,包含描边变体。标准 H32 输入框和 H36 选择器使用 R12。“工作区文件”和“新建终端”引导入口统一使用 R20,包含仅标题与带描述的变体。R28 留给包围它们的面板或对话框,不用于这些入口。
代码块、diff 与文件预览共用 R16:包括改动文件入口卡片、交付文件卡片、通用附件文件卡片、悬停预览浮层,以及入口卡片内部的文件图标底框。各自的 hover 保持同一轮廓。
代码块、diff 与文件预览共用 R16:包括改动文件入口卡片、交付文件卡片、通用附件文件卡片和悬停预览浮层。改动文件与交付文件卡片内部的 40px 文件图标底框作为组件特例,四角统一使用 R10。各自的 hover 保持同一轮廓。
<a id="shapes"></a>
## 复用 token,保留有意设计的形状
+2 -2
View File
@@ -30,8 +30,8 @@
en: ca75c51c89c2c9b0
zh: a0e9f8d52b4b62ee
/deepseek-ai-dsh-client-shortcuts/known-limitations-and-deferred-work:
en: 1fe209aad4915c64
zh: 078d33d3ca9e05ea
en: 323e99bf933b4c6c
zh: 0a7ecdb2dcdb1288
/deepseek-ai-dsh-client-shortcuts/known-limitations-and-deferred-work/dev-note:
en: 09894c06f9d76ac0
zh: d166e1338ae660e4
+1 -1
View File
@@ -79,7 +79,7 @@ None; this package neither assembles nor sends a provider request.
<a id="known-limitations-and-deferred-work"></a>
- Feature owners declare the Windows and macOS Web defaults explicitly: Mod+Slash, Mod+Comma, Mod+Backslash, and Control+Backquote stay two-key combinations; other defaults without Alt use Mod+Alt, and defaults with Alt use Mod+Shift except fullscreen uses Mod+Alt+Enter. macOS Web leaves Refresh current page unbound by default to preserve browser-specific Command+Option+R actions; its button and explicit user bindings remain available. Mod is Command on macOS and Control on Windows. Windows and macOS accept any three or four distinct modifiers in Web and Desktop. Web also accepts the listed combinations and Mod+Shift combinations; browser or system delivery requires platform testing. Linux Web accepts Mod+Slash, Mod+Shift+Comma, and Mod+Shift+Period. Operating-system shortcuts that do not reach the window cannot be intercepted. A chord does not delay or consume its first key while waiting for the second; a native action or focus change can therefore prevent completion. Command registration or disposal invalidates open edit drafts even when saved preferences are unchanged. Desktop rereads its file on every catalog update; there is no file watcher.
- Feature owners declare the Windows and macOS Web defaults explicitly: Mod+Slash, Mod+Backslash, and Control+Backquote stay two-key combinations; other defaults without Alt use Mod+Alt, and defaults with Alt use Mod+Shift except fullscreen uses Mod+Alt+Enter and session rename uses Mod+Alt+G. Web Settings uses Mod+Alt+Comma, leaving Mod+Comma to the browser. macOS Web leaves Refresh current page unbound by default to preserve browser-specific Command+Option+R actions; its button and explicit user bindings remain available. Mod is Command on macOS and Control on Windows. Windows and macOS accept any three or four distinct modifiers in Web and Desktop. Web also accepts the listed combinations and Mod+Shift combinations; browser or system delivery requires platform testing. Linux Web accepts Mod+Slash, Mod+Shift+Comma, and Mod+Shift+Period. Operating-system shortcuts that do not reach the window cannot be intercepted. A chord does not delay or consume its first key while waiting for the second; a native action or focus change can therefore prevent completion. Command registration or disposal invalidates open edit drafts even when saved preferences are unchanged. Desktop rereads its file on every catalog update; there is no file watcher.
<a id="dev-note"></a>
### Dev Note
+1 -1
View File
@@ -79,7 +79,7 @@ Desktop 原生菜单和内嵌 frame 使用与 DOM 输入相同的命令注册表
<a id="known-limitations-and-deferred-work"></a>
- 功能 owner 显式声明 Windows 和 macOS Web 的默认键位:Mod+Slash、Mod+Comma、Mod+Backslash 和 Control+Backquote 保持双键;其他不含 Alt 的默认键位使用 Mod+Alt,含 Alt 的使用 Mod+Shift,全屏除外,使用 Mod+Alt+Enter。macOS Web 的“刷新当前页”默认不绑定快捷键,以保留浏览器自身的 Command+Option+R 操作;刷新按钮和用户显式绑定仍然可用。Mod 在 macOS 上为 Command,在 Windows 上为 Control。Windows 和 macOS 的 Web、Desktop 均接受任意三个或四个不同修饰键的组合。Web 还接受上述组合及 Mod+Shift 组合;浏览器或系统能否送达按键需在对应平台测试。Linux Web 接受 Mod+Slash、Mod+Shift+Comma 和 Mod+Shift+Period。未送达窗口的操作系统快捷键无法被拦截。双键组合等待第二个键时,不延迟或消费第一个键;原生操作或焦点变化可能使组合无法完成。命令注册或注销会使打开的编辑草稿过期,即使保存的偏好未改变。Desktop 在每次命令目录更新时重新读取文件,不设置文件 watcher。
- 功能 owner 显式声明 Windows 和 macOS Web 的默认键位:Mod+Slash、Mod+Backslash 和 Control+Backquote 保持双键;其他不含 Alt 的默认键位使用 Mod+Alt,含 Alt 的使用 Mod+Shift,其中全屏使用 Mod+Alt+Enter,会话重命名使用 Mod+Alt+G。Web 设置使用 Mod+Alt+Comma,将 Mod+Comma 留给浏览器。macOS Web 的“刷新当前页”默认不绑定快捷键,以保留浏览器自身的 Command+Option+R 操作;刷新按钮和用户显式绑定仍然可用。Mod 在 macOS 上为 Command,在 Windows 上为 Control。Windows 和 macOS 的 Web、Desktop 均接受任意三个或四个不同修饰键的组合。Web 还接受上述组合及 Mod+Shift 组合;浏览器或系统能否送达按键需在对应平台测试。Linux Web 接受 Mod+Slash、Mod+Shift+Comma 和 Mod+Shift+Period。未送达窗口的操作系统快捷键无法被拦截。双键组合等待第二个键时,不延迟或消费第一个键;原生操作或焦点变化可能使组合无法完成。命令注册或注销会使打开的编辑草稿过期,即使保存的偏好未改变。Desktop 在每次命令目录更新时重新读取文件,不设置文件 watcher。
<a id="dev-note"></a>
### 开发备注
+7 -7
View File
@@ -15,17 +15,17 @@
en: 41cd21266011d571
zh: 3172d1a2acbb27b7
/deepseek-ai-dsh-client-ui-chat/reference-previews:
en: 3fda3d75483ce7c1
zh: 4d96ba654f70c021
en: 0a1948349b1fbfd9
zh: 56256e4d695e5192
/deepseek-ai-dsh-client-ui-chat/hidden-chat-rows:
en: 5706ae03ceacf251
en: cbf1f0a2205046a5
zh: 2e59d0815351349f
/deepseek-ai-dsh-client-ui-chat/command-and-failure-rows:
en: 2157fcd6b0d100b5
zh: 9e865ac3953ebe30
/deepseek-ai-dsh-client-ui-chat/turn-token-usage:
en: 29346bd0f9a5ff51
zh: 54dbed55f67aeecd
en: e1dd055d68ab25ea
zh: c5b62a22c9e457cb
/deepseek-ai-dsh-client-ui-chat/completed-turn-footer:
en: b4cd36e7ab91281e
zh: 8ccf412b9dde61d5
@@ -33,8 +33,8 @@
en: 0ca3e8c4eab1ca78
zh: 1a1c71bab151eb7d
/deepseek-ai-dsh-client-ui-chat/grouped-rendering:
en: f7b5260bea372361
zh: 2daa3ff650ec9246
en: 1ccc8de0d1ca0481
zh: 156cdfb5b6ba6490
/deepseek-ai-dsh-client-ui-chat/scroll-ownership:
en: 69b021c5a5d8b5a7
zh: 091ae7986c7a8a20
+5 -3
View File
@@ -37,12 +37,12 @@ Chat supplies file and HTTP(S) navigation through one `MarkdownDelegateProvider`
Standalone Markdown images show contained previews and open the shared image lightbox; local paths resolve against the viewed workspace after settlement. Image file links keep their sidebar activation and show a thumbnail after hover dwell or keyboard focus. Escape dismisses the thumbnail. Failed images retain a localized status and their description; no duplicate-image filtering is applied.
Settings → General → Open chat links in selects the destination for ordinary clicks on Chat HTTP(S) links: In-App Sidebar (default) opens a new right-Sidebar Browser tab, while Default Browser opens an external tab. The setting is shown only while the Sidebar Browser is available. If the Sidebar Browser is not registered, both choices use the external browser; modified clicks retain native behavior. The `ui-chat.linkOpening` preference persists on loopback browsers and stays process-local when settings cannot persist writes. Sent file references and skills confirmed by the message’s logged invocation also open in the right Sidebar. File paths use the viewed Session; skill names resolve through its current input-trigger source. Both use the prose file-link dotted underline on hover or focus. Sessions, directories, and command labels remain non-navigating references.
Settings → General → Open chat links in selects the destination for ordinary clicks on Chat HTTP(S) links: In-App Sidebar (default) opens a new right-Sidebar Browser tab, while Default Browser opens an external tab. The setting follows Keyboard shortcuts and is shown only while the Sidebar Browser is available. If the Sidebar Browser is not registered, both choices use the external browser; modified clicks retain native behavior. The `ui-chat.linkOpening` preference persists on loopback browsers and stays process-local when settings cannot persist writes. Sent file references and skills confirmed by the message’s logged invocation also open in the right Sidebar. File paths use the viewed Session; skill names resolve through its current input-trigger source. Both use the prose file-link dotted underline on hover or focus. Sessions, directories, and command labels remain non-navigating references.
<a id="system-prompt-row"></a>
## Hidden Chat rows
Chat omits system-prompt, ordinary Context injection, and `permission` command rows in every work-details mode. Context containing tool additions or removals remains visible. 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. Context containing tool additions or removals remains visible. 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.
@@ -58,7 +58,7 @@ Generic command rows retain the ordinary command glyph in every lifecycle state;
A completed Turn shows an expandable usage row only when the loaded window includes `turn/start` and every started model attempt reports safe, exact usage. The row omits unavailable optional buckets. Incomplete or contradictory accounting hides the complete disclosure instead of presenting a partial total.
Settings → General → Performance & usage stores `ui-chat.performanceUsage` as `detailed` (default) or `compact`. Compact shows only available output speed and cache-hit percentage beneath the composer, without interactive statistic dialogs or per-Turn usage. Detailed exposes session statistics and per-Turn token usage. Neither mode shows elapsed time in the completed-turn footer. The preference changes presentation only; accounting and Session events remain intact.
Settings → General → Performance & usage appears immediately before Current version and stores `ui-chat.performanceUsage` as `detailed` (default) or `compact`. Compact shows only available output speed and cache-hit percentage beneath the composer, without interactive statistic dialogs or per-Turn usage. Detailed exposes session statistics and per-Turn token usage. Neither mode shows elapsed time in the completed-turn footer. The preference changes presentation only; accounting and Session events remain intact.
On non-loopback browsers, the preference remains process-local because the settings scope cannot persist writes. Explicit selections update every consumer immediately; accepted Host settings reconcile the live value on loopback browsers.
@@ -103,6 +103,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. Leading icons, disclosure chevrons, and their boxes scale with the content font-size delta.
-----
<a id="scroll-ownership"></a>
+4 -2
View File
@@ -37,7 +37,7 @@ Chat 在节点列表外通过一个 `MarkdownDelegateProvider` 提供文件及 H
独立的 Markdown 图片以内嵌预览显示,点击打开共享图片浮层;本地路径在消息落定后基于当前查看的工作区解析。图片文件链接保持点击打开侧栏,鼠标停留或键盘聚焦时显示缩略图,Esc 关闭缩略图。图片加载失败时保留本地化状态与图片说明;不执行重复图片过滤。
设置 → 通用设置 → 网页链接默认打开方式控制普通点击 Chat HTTP(S) 链接时的目标:「应用内侧边栏」(默认)打开新的右侧 Sidebar Browser tab,「默认浏览器」打开外部标签页。该设置项仅在 Sidebar Browser 可用时显示。若 Sidebar Browser 未注册,两种选择均使用外部浏览器;带修饰键的点击保留原生行为。`ui-chat.linkOpening` 偏好在回环地址浏览器中持久化,设置无法持久化写入时仅在当前进程内生效。已发送的文件引用及消息日志确认调用的 skill 也可在右侧栏打开预览。文件路径使用当前查看的 Session;skill 名称由该 Session 当前的输入触发源解析。两者悬停或聚焦时均使用正文文件链接的虚线下划线。会话、目录和命令标签仍只作为引用展示。
设置 → 通用设置 → 网页链接默认打开方式控制普通点击 Chat HTTP(S) 链接时的目标:「应用内侧边栏」(默认)打开新的右侧 Sidebar Browser tab,「默认浏览器」打开外部标签页。该设置项位于快捷键之后,仅在 Sidebar Browser 可用时显示。若 Sidebar Browser 未注册,两种选择均使用外部浏览器;带修饰键的点击保留原生行为。`ui-chat.linkOpening` 偏好在回环地址浏览器中持久化,设置无法持久化写入时仅在当前进程内生效。已发送的文件引用及消息日志确认调用的 skill 也可在右侧栏打开预览。文件路径使用当前查看的 Session;skill 名称由该 Session 当前的输入触发源解析。两者悬停或聚焦时均使用正文文件链接的虚线下划线。会话、目录和命令标签仍只作为引用展示。
<a id="system-prompt-row"></a>
## Chat 隐藏的行
@@ -58,7 +58,7 @@ Assistant 尝试结束且没有可见消息时,Chat 隐藏已发布的 Node,
只有当已加载窗口包含 `turn/start`,且每次已启动的模型尝试都报告安全、精确的用量时,已完成轮次才显示可展开的用量行。该行会省略不可用的可选用量桶。记账不完整或相互矛盾时,整个详情都不显示,避免把部分总量冒充完整结果。
设置 → 通用设置 → 性能与用量将 `ui-chat.performanceUsage` 保存为 `detailed`(默认)或 `compact`。简洁模式仅在输入框下方显示可用的输出速度和缓存命中率,不显示统计交互卡片或每轮用量。详细模式提供会话统计和每轮 token 用量。两种模式的已完成轮次页脚均不显示耗时。该偏好仅影响展示,记账和 Session 事件保持完整。
设置 → 通用设置 → 性能与用量位于当前版本之前,将 `ui-chat.performanceUsage` 保存为 `detailed`(默认)或 `compact`。简洁模式仅在输入框下方显示可用的输出速度和缓存命中率,不显示统计交互卡片或每轮用量。详细模式提供会话统计和每轮 token 用量。两种模式的已完成轮次页脚均不显示耗时。该偏好仅影响展示,记账和 Session 事件保持完整。
在非回环地址浏览器中,设置作用域无法持久化写入,因此该偏好仅在当前进程内生效。明确选择会立即更新所有使用方;回环地址浏览器收到 Host 已接受的设置后会同步当前值。
@@ -103,6 +103,8 @@ Chat 通过 `uiConversation.groups` 注册过程 Group Definition。React 通过
Chat 节点 slot 为推理与工具注入绑定重置来源的 `useDisclosure` 钩子。中间 renderer 只透传、不订阅,每次调用拥有独立展开状态。来源回调保留接收对象及稳定引用。外层轮次实际隐藏过程成员时,所在节点重置这些开合状态,不替换组件 key,也不改变钩子引用。展示模式切换保留展开状态。
过程行的文字与图标在所有工作过程展示模式下共用一种颜色:默认 tertiary、悬停 secondary,错误与警告文字保留语义色。前置图标、折叠箭头及其容器随正文字号增量缩放。
-----
<a id="scroll-ownership"></a>
+2 -2
View File
@@ -133,7 +133,7 @@ export function apply(ctx: Context): void {
scope.slots.inject('settings.general.item', () => scope.slots.register({
name: 'settings.general.item',
id: 'link-opening',
order: 14,
order: 17,
locale: NS,
inject: (): LinkOpeningRowInjected => ({
hooks: { linkOpening, browserAvailable },
@@ -156,7 +156,7 @@ export function apply(ctx: Context): void {
ctx.slots.inject('settings.general.item', () => ctx.slots.register({
name: 'settings.general.item',
id: 'performance-usage',
order: 13,
order: 95,
locale: NS,
inject: (): PerformanceUsageRowInjected => ({
hooks: { performanceUsage },
@@ -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,
@@ -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],
@@ -56,10 +56,8 @@ export const GenericCommandCard = memo(function GenericCommandCard({ node, t, ru
{state === 'running' && <span className={a11yCss.visuallyHidden}>{t('row.running')}</span>}
{state === 'error' && <span className={a11yCss.visuallyHidden}>{t('row.failed')}</span>}
<DisclosureRow
rowClassName={css.row}
leadingClassName={css.leading}
titleClassName={css.title}
chevronClassName={css.chevron}
icon={COMMAND_ICON}
title={title}
running={running}
@@ -48,9 +48,8 @@
padding: 2px 0;
}
/* Compaction marker: one dim 24px row with a context icon at rest and a
hover/focus disclosure for the summary body. Dimmed title (not
label-primary) — the row is a boundary notice, not conversation content. */
/* Compaction marker: one 24px row with a context icon at rest and a
hover/focus disclosure for the summary body. */
.compactionRow {
/* The toggle's line, and the band a pinned toggle occupies. The button's
height and the offset that holds a summary's code banner below that band
@@ -69,7 +68,8 @@
border: none;
border-radius: var(--dsw-radius-sm);
background: none;
color: inherit;
color: var(--dsw-alias-label-tertiary);
transition: color 100ms ease;
font: inherit;
text-align: left;
}
@@ -113,6 +113,10 @@
cursor: pointer;
}
.compactionButton:hover {
color: var(--dsw-alias-label-secondary);
}
.compactionButton:not(:disabled):hover {
background: var(--dsw-alias-interactive-bg-hover);
}
@@ -124,7 +128,7 @@
width: calc(16px + var(--dsh-content-font-delta, 0px));
height: calc(16px + var(--dsh-content-font-delta, 0px));
margin-right: 6px;
color: var(--dsw-alias-label-secondary);
color: inherit;
}
/* Same glyph-scaling rule as DisclosureRow's leading: the CSS edge overrides
@@ -160,7 +164,7 @@
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-primary-dimmed);
color: inherit;
}
.compactionSep {
@@ -176,7 +180,7 @@
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
color: var(--dsw-alias-label-tertiary);
color: inherit;
font-size: var(--dsh-content-font-size-secondary, 13px);
line-height: calc(24px + var(--dsh-content-font-delta, 0px));
text-overflow: ellipsis;
@@ -223,7 +227,6 @@
border-right: 1.5px solid currentcolor;
border-bottom: 1.5px solid currentcolor;
content: '';
opacity: 0.8;
transform: rotate(-45deg);
transition: transform 120ms ease;
}
@@ -232,6 +235,12 @@
color: var(--dsw-alias-label-secondary);
}
@media (prefers-reduced-motion: reduce) {
.compactionButton {
transition: none;
}
}
.retrySummary:focus-visible {
outline: 1.5px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
outline-offset: 2px;
@@ -9,7 +9,6 @@
}
.row {
position: relative;
overflow: hidden;
}
@@ -26,10 +25,6 @@
flex-shrink: 0;
}
.chevron {
color: var(--dsw-alias-label-secondary);
}
.title {
font-weight: 400;
}
@@ -50,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;
@@ -86,7 +86,6 @@ 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}
@@ -2,7 +2,7 @@
import { memo, useEffect, useState } from 'react'
import { TextShimmer } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ChatViewSlotProps } from '../contract/slots.ts'
import { formatLiveRunDuration, LIVE_RUN_CLOCK_INTERVAL_MS } from './message-chrome.ts'
import { formatRunDuration, LIVE_RUN_CLOCK_INTERVAL_MS } from './message-chrome.ts'
import { RunningWhaleTail } from './RunningWhaleTail.tsx'
import a11yCss from './accessibility.module.css'
import css from './ChatView.module.css'
@@ -26,7 +26,7 @@ export const RunningStatus = memo(function RunningStatus({ startTime, t }: Runni
return () => { clearInterval(timer) }
}, [startTime])
const label = startTime === undefined ? t('chat.deepDiving') : t('chat.deepDivingFor', {
duration: formatLiveRunDuration(Math.max(1000, now - startTime), t),
duration: formatRunDuration(Math.max(1000, now - startTime), t).map(part => part.text).join(''),
})
return (
<div className={css.running} data-chat-running>
@@ -10,6 +10,8 @@
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
background: none;
color: var(--dsw-alias-label-tertiary);
font: inherit;
font-size: var(--dsh-content-font-size, 14px);
cursor: pointer;
text-align: left;
transition: color 100ms ease;
@@ -19,8 +21,8 @@
cursor: default;
}
.root:not(:disabled):hover {
color: var(--dsw-alias-label-primary);
.root:hover:not(:disabled) {
color: var(--dsw-alias-label-secondary);
}
.chevron {
@@ -28,7 +30,7 @@
width: 14px;
height: 14px;
margin-left: 4px;
color: var(--dsw-alias-label-caption);
color: inherit;
transition: transform 100ms ease;
}
@@ -39,12 +41,17 @@
.label {
min-width: 0;
overflow: hidden;
font-size: var(--dsh-content-font-size-secondary, 13px);
font-size: inherit;
line-height: calc(24px + var(--dsh-content-font-delta, 0px));
text-overflow: ellipsis;
white-space: nowrap;
}
.durationNumber {
font-family: var(--ds-font-family-code);
font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
.root,
.chevron {
@@ -6,7 +6,7 @@ import { formatRunDuration } from './message-chrome.ts'
import a11yCss from './accessibility.module.css'
import css from './TurnProcessNodeView.module.css'
/** Turn-level process disclosure controller. */
/** Settled Turn duration and process disclosure above its content. */
export const TurnProcessNodeView = memo(function TurnProcessNodeView({
node, turnProcess, t,
}: ChatNodeViewProps<'turn-process'>) {
@@ -20,13 +20,13 @@ export const TurnProcessNodeView = memo(function TurnProcessNodeView({
const reason = turn.end?.data.reason.kind
const elapsedMs = turn.start === undefined || turn.end === undefined ? undefined
: Math.max(1000, turn.end.time - turn.start.time)
const duration = elapsedMs === undefined ? undefined
const duration = elapsedMs === undefined || reason === 'aborted' || reason === 'error' ? undefined
: formatRunDuration(elapsedMs, t)
// Other end reasons retain elapsed time; only cancellation and failure replace it.
const label = reason === 'aborted' ? t('message.stopped')
: reason === 'error' ? t('message.turnProcess.failed')
: duration === undefined ? t('message.turnProcess.worked')
: t('message.turnProcess.took', { duration })
: t('message.turnProcess.took')
const announcement = reason === 'aborted' ? t('message.stopped')
: reason === 'error' ? t('message.turnProcess.failed')
: t('message.turnProcess.worked')
@@ -48,7 +48,12 @@ export const TurnProcessNodeView = memo(function TurnProcessNodeView({
turnProcess.setOpen(!open)
}}
>
<span className={css.label}>{label}</span>
<span className={css.label}>
{label}
{duration?.map((part, index) => (
<span key={index} className={part.numeric ? css.durationNumber : undefined}>{part.text}</span>
))}
</span>
{canCollapse && <IconChevronDownOutlineRegular className={css.chevron} />}
</button>
</>
@@ -2,10 +2,10 @@
min-width: 0;
border: 0.5px solid var(--dsw-alias-border-l1);
border-radius: var(--dsw-radius-xl);
background: var(--dsw-alias-markdown-code-block);
background: var(--dsw-alias-turn-trigger-bg, var(--dsw-alias-markdown-code-block));
transition: background-color 100ms ease;
}
.root:hover { background: var(--dsw-alias-interactive-bg-hover); }
.root:hover { background: var(--dsw-alias-turn-trigger-bg-hover, var(--dsw-alias-interactive-bg-hover)); }
.header {
display: flex;
align-items: center;
@@ -14,20 +14,22 @@
padding: 12px 16px;
border: 0;
background: none;
color: var(--dsw-alias-label-primary);
color: var(--dsw-alias-label-tertiary);
transition: color 100ms ease;
font: inherit;
text-align: left;
cursor: pointer;
}
.icon { display: inline-flex; flex: none; color: var(--dsw-alias-label-tertiary); }
.title { flex: none; font: var(--dsw-font-xs-13); }
.time { flex: none; margin-left: auto; color: var(--dsw-alias-label-caption); font: var(--dsw-font-xxs-12); }
.chevron, .openChevron { flex: none; color: var(--dsw-alias-label-tertiary); }
.header:hover { color: var(--dsw-alias-label-secondary); }
.icon { display: inline-flex; flex: none; color: inherit; }
.title { flex: none; font-size: var(--dsh-content-font-size, 14px); }
.time { flex: none; margin-left: auto; color: inherit; font: var(--dsw-font-xxs-12); }
.chevron, .openChevron { flex: none; color: inherit; }
.openChevron { transform: rotate(180deg); }
.body { padding: 0 16px 12px 40px; }
.explanation { margin: 8px 0; color: var(--dsw-alias-label-secondary); font: var(--dsw-font-xxs-12); }
.content { max-height: 240px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font: var(--dsw-font-xxs-12); }
@media (prefers-reduced-motion: reduce) {
.root { transition: none; }
.root, .header { transition: none; }
}
@@ -7,7 +7,7 @@ export type ClockTranslate = Translate<'clock.md' | 'clock.ymd'>
/** The elapsed-duration share of the conversation dictionary. */
export type RunDurationTranslate =
Translate<'duration.seconds' | 'duration.minutes' | 'duration.hours'>
Translate<'duration.secondUnit' | 'duration.minuteUnit' | 'duration.hourUnit'>
/** Refresh interval for whole-second live run clocks. */
export const LIVE_RUN_CLOCK_INTERVAL_MS = 1000
@@ -38,43 +38,29 @@ export function msUntilNextLocalMidnight(ms: number): number {
return Math.max(next.getTime() - ms, 1)
}
/** One numeric value or localized unit in an elapsed-time label. */
export interface RunDurationPart {
readonly text: string
readonly numeric: boolean
}
/**
* Localized elapsed-time label for the running conversation clock.
* @param ms - Elapsed duration in milliseconds (negatives clamp to zero).
* @param t - Translate seat supplying the duration templates.
* @returns Display string in whole seconds; minutes and seconds once the
* duration reaches a minute; hours, minutes, and seconds once it reaches an
* hour, with the smaller units zero-padded.
* Build elapsed-time fragments for both live and completed Turn labels.
* @param ms - elapsed milliseconds; negatives clamp to zero and fractions floor.
* @param t - translate seat supplying units and their trailing spacing.
* @returns numbers and localized units in display order, without leading zeros;
* minutes start at 60 seconds and hours at 60 minutes.
*/
export function formatRunDuration(ms: number, t: RunDurationTranslate): string {
export function formatRunDuration(ms: number, t: RunDurationTranslate): RunDurationPart[] {
const total = Math.max(0, Math.floor(ms / 1000))
const hours = Math.floor(total / 3600)
const minutes = Math.floor(total / 60) % 60
const seconds = total % 60
if (hours > 0) {
return t('duration.hours', { hours, minutes: pad2(minutes), seconds: pad2(seconds) })
}
return minutes > 0
? t('duration.minutes', { minutes, seconds: pad2(seconds) })
: t('duration.seconds', { seconds })
}
/**
* Localized live elapsed time without padded seconds or early rollover.
* @param ms - Elapsed duration in milliseconds (negatives clamp to zero).
* @param t - Translate seat supplying the duration templates.
* @returns Whole seconds without a leading zero; minutes start at 60 seconds
* and hours start at exactly 60 minutes.
*/
export function formatLiveRunDuration(ms: number, t: RunDurationTranslate): string {
const totalSeconds = Math.max(0, Math.floor(ms / 1000))
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor(totalSeconds / 60) % 60
const seconds = String(totalSeconds % 60)
if (hours > 0) return t('duration.hours', { hours, minutes: pad2(minutes), seconds })
return minutes > 0
? t('duration.minutes', { minutes, seconds })
: t('duration.seconds', { seconds })
const parts: RunDurationPart[] = []
if (hours > 0) parts.push({ text: String(hours), numeric: true }, { text: t('duration.hourUnit'), numeric: false })
if (total >= 60) parts.push({ text: String(minutes), numeric: true }, { text: t('duration.minuteUnit'), numeric: false })
parts.push({ text: String(seconds), numeric: true }, { text: t('duration.secondUnit'), numeric: false })
return parts
}
/**
@@ -24,8 +24,8 @@
en: 91b35cbebe8c2a1d
zh: a4fee6d7102bc874
/chat-conversation-node-rules/whole-turn-folding:
en: 69989cc6a42c137a
zh: ceca703426315b88
en: f8674438930c268c
zh: cb8d007db714de6f
/chat-conversation-node-rules/display-modes:
en: ada5a60d2c46f6a8
zh: f3eaeb5cb4d9b3de
@@ -33,8 +33,8 @@
en: a47e16132ba3c736
zh: fbed2b6f43a93287
/chat-conversation-node-rules/display-modes/group-title-rules:
en: 49fc1aa60cbd239e
zh: 44b0f9a96ec5b112
en: e4b8384f32e4ff98
zh: e8a42f078b38f41b
/chat-conversation-node-rules/process-grouping:
en: 276a97b95610f5e8
zh: 7627b2afb0a7c0f4
@@ -101,6 +101,8 @@ Paging adds older content above the retained anchor without jumping to the new t
While the Session runs, Chat appends a Session-level indicator at the bottom of the current transcript, after pending submission or steering rows: a whale tail beside shimmering elapsed time. Before the current Turn start time arrives, the indicator shows status text without a duration. The clock updates in whole seconds, starts at one second, and uses hours from 60 minutes. When the Session stops running, the indicator disappears; the closed Turn process control shows the fixed duration, cancellation, or failure status. Lifecycle changes have a polite announcement; clock ticks do not. The whale is hidden from assistive technology and remains static under reduced motion.
The top duration/status control and trigger title use the group-title font size and follow the font-size setting. Completed duration digits use the code font with tabular numerals; running and completed minutes and seconds have no leading zero. The timed running label ends with a space and “···”; completion shows “Completed in”.
Automatic collapse keeps the process open if hiding it would hide keyboard focus. Manual closing focuses the process control before hiding its members. Closing a whole Turn resets its groups and inner reasoning/tool disclosures; it does not reset unrelated renderer state. Browser find can reveal searchable hidden content.
-----
@@ -165,7 +167,7 @@ The labels below describe recorded activity, not successful outcomes. For exampl
English lowercases the initial letter of joined labels after the first. Closing a group immediately selects the completed summary; the 150ms minimum applies to running-title changes, not to delaying completion. Detailed hides group headers in running Turns, including groups ended by a reply or steering before their Turn ends. Verbose also hides historical group headers.
Group headers show a category icon, replace it with a down arrow on hover or keyboard focus, and show an up arrow while open. Manually expanded group bodies use 8px row spacing, a `min(400px, 50vh)` height cap, and 24px directional fades. Wheel scrolling can continue into the outer transcript at an edge. Detailed removes the group-level cap and uses 16px row spacing in running Turns; Verbose applies this layout to historical Turns as well.
Group headers show a category icon, replace it with a down arrow on hover or keyboard focus, and show an up arrow while open. Manually expanded group bodies use 6px row spacing, a `min(400px, 50vh)` height cap, and 24px directional fades. Wheel scrolling can continue into the outer transcript at an edge. Detailed removes the group-level cap in running Turns; Verbose applies this layout to historical Turns as well. Process rows retain 6px spacing in both modes; Assistant responses have 12px clearance. Expanded group titles sit 8px above their content; Turn process controls retain 16px clearance.
An open capped group follows content growth only while its own scroll position is at the bottom. Scrolling away pauses that group's following; returning to the bottom resumes it, independently of outer transcript following. Manually opening an unclosed group starts at the bottom and follows growth; manually opening a closed group starts at the top with following disabled, even when its initial content fits without scrolling. Closing the group in the data or restoring its height cap through a mode change does not reset an already-open reader's position. Browser find retains its own reveal position.
@@ -101,6 +101,8 @@ description: "Chat 过程分组与活动摘要的明细业务规则。"
会话运行期间,Chat 在当前 transcript 最底部、待提交或 steering 行之后附加 Session 级运行指示:鲸鱼摆尾旁显示闪烁的已用时间。当前 Turn 的开始时间到达前,运行指示只显示状态文字,不显示时长。时钟按整秒更新,最少显示一秒,满 60 分钟使用小时。会话停止运行后,该指示消失;已结束 Turn 的过程控件显示固定时长、取消或失败状态。生命周期变化有礼貌播报,时钟跳动不播报。鲸鱼对辅助技术隐藏,启用减少动态效果时保持静态。
顶部用时/状态控件与唤醒标题沿用二级汇总标题字号,并跟随字号设置。已完成用时的数字使用代码等宽字体;运行和完成状态的分钟和秒数均不补前导零。含用时的运行文案末尾附加一个空格及“···”,完成后显示“已完成,用时”。
自动收起若会隐藏键盘焦点,则保持过程展开。手动收起先将焦点移到过程控件,再隐藏成员。收起整轮会重置内部组及推理、工具的开合,不重置渲染器的其他状态。浏览器查找可以显示可搜索的隐藏内容。
-----
@@ -165,7 +167,7 @@ description: "Chat 过程分组与活动摘要的明细业务规则。"
英文组合标题将第一项之后各项的首字母转为小写。组结束时立即切换完成摘要;150ms 最短保留时间针对运行中标题变化,不延迟完成状态。详细模式隐藏运行中轮次的组头,包括被回复或 steering 截断、但所属轮次尚未结束的小组;完全展开模式也隐藏历史组头。
组头显示类别图标,悬停或键盘聚焦时替换为向下箭头,展开时显示向上箭头。手动展开的组正文使用 8px 行间距、`min(400px, 50vh)` 限高及 24px 方向渐隐;到达边缘时滚轮可以继续滚动外层文本记录。详细模式对运行中轮次取消组级限高,使用 16px 行间距;完全展开模式对历史轮次也采用该布局。
组头显示类别图标,悬停或键盘聚焦时替换为向下箭头,展开时显示向上箭头。手动展开的组正文使用 6px 行间距、`min(400px, 50vh)` 限高及 24px 方向渐隐;到达边缘时滚轮可以继续滚动外层文本记录。详细模式对运行中轮次取消组级限高;完全展开模式对历史轮次也采用该布局。两种模式下过程行均保留 6px 间距,Assistant 回复周围保留 12px 间距。展开组头与内容相隔 8px,整轮过程控件周围保留 16px 间距。
已展开的限高组仅在自己的滚动位置处于底部时跟随内容增长。滚离底部暂停该组跟随,滚回底部后恢复,与外层文本记录的跟随状态独立。手动展开未关闭的组时定位到底部并跟随增长;手动展开已关闭的组时从顶部开始且不启用跟随,即使初始内容无需滚动即可容纳。数据中的组变为已关闭,或通过模式切换恢复限高时,不重置已展开组的阅读位置。浏览器查找沿用自身的展开定位。
+14 -14
View File
@@ -61,8 +61,8 @@ export const zh = {
'message.trigger.job': '后台任务状态更新',
'message.trigger.plugin': '插件状态更新',
'message.trigger.explanation': '这条通知触发了本轮回复。',
'message.turnProcess.worked': '已完成工作',
'message.turnProcess.took': '用时 {duration}',
'message.turnProcess.worked': '已完成',
'message.turnProcess.took': '已完成,用时 ',
'message.turnProcess.failed': '处理失败',
'view.chat': '对话',
'image.open': '查看大图',
@@ -86,8 +86,8 @@ export const zh = {
'chat.loadError': '历史加载失败:{message}({code})',
'chat.loadOlder': '加载更早',
'chat.toBottom': '回到底部',
'chat.deepDiving': '深度求索中...',
'chat.deepDivingFor': '深度求索中,用时 {duration}...',
'chat.deepDiving': '深度求索中',
'chat.deepDivingFor': '深度求索中,用时 {duration} ···',
'chat.turnNavigation.label': '轮次导航',
'chat.turnNavigation.jump': '跳转到第 {turn} 轮',
'chat.turnNavigation.jumpLoad': '加载并跳转到第 {turn} 轮',
@@ -179,9 +179,9 @@ export const zh = {
'message.turnUsage.output': '输出',
'message.turnUsage.reasoning': '(其中推理 {tokens})',
'message.turnUsage.count': '{count} tok',
'duration.seconds': '{seconds}秒',
'duration.minutes': '{minutes}分{seconds}秒',
'duration.hours': '{hours}小时{minutes}分{seconds}秒',
'duration.secondUnit': '秒',
'duration.minuteUnit': '分',
'duration.hourUnit': '小时',
'command.running': '执行中…',
'command.failed': '指令失败',
'command.done': '已完成',
@@ -254,8 +254,8 @@ export const en = {
'message.trigger.job': 'Background task updated',
'message.trigger.plugin': 'Plugin status updated',
'message.trigger.explanation': 'This notification triggered this response.',
'message.turnProcess.worked': 'Worked',
'message.turnProcess.took': 'Took {duration}',
'message.turnProcess.worked': 'Completed',
'message.turnProcess.took': 'Completed in ',
'message.turnProcess.failed': 'Failed',
'view.chat': 'Chat',
'image.open': 'View full image',
@@ -279,8 +279,8 @@ export const en = {
'chat.loadError': 'Failed to load history: {message} ({code})',
'chat.loadOlder': 'Load earlier',
'chat.toBottom': 'Back to bottom',
'chat.deepDiving': 'Deep diving...',
'chat.deepDivingFor': 'Deep diving for {duration}...',
'chat.deepDiving': 'Deep diving',
'chat.deepDivingFor': 'Deep diving for {duration} ···',
'chat.turnNavigation.label': 'Turn navigation',
'chat.turnNavigation.jump': 'Jump to turn {turn}',
'chat.turnNavigation.jumpLoad': 'Load and jump to turn {turn}',
@@ -372,9 +372,9 @@ export const en = {
'message.turnUsage.output': 'Output',
'message.turnUsage.reasoning': ' ({tokens} reasoning)',
'message.turnUsage.count': '{count} tok',
'duration.seconds': '{seconds}s',
'duration.minutes': '{minutes}m {seconds}s',
'duration.hours': '{hours}h {minutes}m {seconds}s',
'duration.secondUnit': 's',
'duration.minuteUnit': 'm ',
'duration.hourUnit': 'h ',
'command.running': 'Running…',
'command.failed': 'Command failed',
'command.done': 'Completed',
@@ -129,7 +129,7 @@ describe('Chat apply wiring', () => {
expect(b.runtime.slots.entries('conversation.composer.dock').map(row => row.options.id))
.toEqual(['stats'])
expect(b.runtime.slots.entries('settings.general.item').map(row => row.options.id))
.toEqual(['transcript-view', 'performance-usage', 'link-opening', 'composer-enter'])
.toEqual(['transcript-view', 'link-opening', 'composer-enter', 'performance-usage'])
await b.runtime.dispose()
})
@@ -45,7 +45,7 @@ import {
import { TurnTailNodeView } from '../src/client/chat/TurnTailNodeView.tsx'
import { TurnProcessNodeView } from '../src/client/chat/TurnProcessNodeView.tsx'
import { SystemPromptNodeView } from '../src/client/chat/SystemPromptRow.tsx'
import { formatRunDuration } from '../src/client/chat/message-chrome.ts'
import { formatRunDuration as durationParts } from '../src/client/chat/message-chrome.ts'
import { ChatSnapshotBuilder } from '../src/client/conversation-nodes/chat-snapshot-builder.ts'
import { ProcessState } from '../src/client/conversation-nodes/process-groups.ts'
import type { TurnProcessSpec } from '../src/client/contract/turn-process.ts'
@@ -571,6 +571,10 @@ function installScrollMetrics(element: HTMLElement, initialHeight: number, clien
}
}
function formatRunDuration(ms: number, t: Parameters<typeof durationParts>[1]): string {
return durationParts(ms, t).map(part => part.text).join('')
}
describe('Chat node rendering', () => {
it('opens Markdown references to unmodified files with line navigation', () => {
@@ -623,17 +627,17 @@ describe('Chat node rendering', () => {
expect(formatRunDuration(0, t)).toBe('0秒')
expect(formatRunDuration(-500, t)).toBe('0秒')
expect(formatRunDuration(15_999, t)).toBe('15秒')
expect(formatRunDuration(125_000, t)).toBe('2分05秒')
expect(formatRunDuration(125_000, t)).toBe('2分5秒')
// The hour rolls at exactly 3600s, never at 60 displayed minutes.
expect(formatRunDuration(3_599_999, t)).toBe('59分59秒')
expect(formatRunDuration(3_600_000, t)).toBe('1小时00分00秒')
expect(formatRunDuration(3_903_000, t)).toBe('1小时05分03秒')
expect(formatRunDuration(7_261_000, t)).toBe('2小时01分01秒')
expect(formatRunDuration(3_600_000, t)).toBe('1小时0分0秒')
expect(formatRunDuration(3_903_000, t)).toBe('1小时5分3秒')
expect(formatRunDuration(7_261_000, t)).toBe('2小时1分1秒')
})
it('formatRunDuration uses the English hour template', () => {
const t = makeTranslate(en, commonEn)
expect(formatRunDuration(3_903_000, t)).toBe('1h 05m 03s')
expect(formatRunDuration(3_903_000, t)).toBe('1h 5m 3s')
})
})
@@ -763,7 +767,7 @@ describe('ChatView', () => {
expect(headers.every(header => header.closest('[hidden]') !== null)).toBe(true)
expect(bodies.every(body => !body.hasAttribute('hidden'))).toBe(true)
expect(control.disabled).toBe(true)
expect(control.textContent).toContain(h.props.t('message.turnProcess.took', { duration: formatRunDuration(3_000, h.props.t) }))
expect(control.textContent).toContain(h.props.t('message.turnProcess.took') + formatRunDuration(3_000, h.props.t))
fireEvent.click(control)
expect(bodies.every(body => !body.hasAttribute('hidden'))).toBe(true)
expect([...view.container.querySelectorAll<HTMLElement>('[data-chat-group-key]')]).toEqual(roots)
@@ -1624,7 +1628,7 @@ describe('ChatView', () => {
expect(writeText).toHaveBeenCalledWith('interrupt now')
expect(within(pendingBubble as HTMLElement).queryByRole('button', { name: '在新对话中分支' })).toBeNull()
expect(turnProcessControl(view.container)).toBeNull()
expect(runningContent(view.container)?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/)
expect(runningContent(view.container)?.textContent).toMatch(/^深度求索中,用时 \d+秒 ···$/)
expect(view.getByRole('status').compareDocumentPosition(view.getByText('interrupt now'))
& Node.DOCUMENT_POSITION_PRECEDING).not.toBe(0)
@@ -2204,7 +2208,7 @@ describe('ChatView', () => {
turnEnds: new Map([[1, 6]]),
})
const view = render(<h.ChatView {...h.props} />)
const toggle = view.getByRole('button', { name: '用时 4秒' })
const toggle = view.getByRole('button', { name: '已完成,用时 4秒' })
expect(toggle.getAttribute('aria-expanded')).toBe('false')
expect(toggle.getAttribute('data-turn-process-tool-calls')).toBe('1')
expect(toggle.getAttribute('data-turn-process-messages')).toBe('1')
@@ -2230,12 +2234,12 @@ describe('ChatView', () => {
expect(members.map(member => member.getAttribute('hidden'))).toEqual([null, null, null])
act(() => { h.set({ nodes: [user(1, 'question'), first] }) })
expect(view.getByRole('button', { name: '用时 4秒' }).getAttribute('aria-expanded')).toBe('false')
expect(view.getByRole('button', { name: '已完成,用时 4秒' }).getAttribute('aria-expanded')).toBe('false')
expect(members[0]?.getAttribute('hidden')).toBeNull()
act(() => { h.set({
nodes: [user(1, 'question'), first, toolResult(3, 'a'), toolResult(4, 'b', 'subagent'), second],
}) })
const renewedToggle = view.getByRole('button', { name: '用时 4秒' })
const renewedToggle = view.getByRole('button', { name: '已完成,用时 4秒' })
expect(renewedToggle.getAttribute('aria-expanded')).toBe('true')
expect(members[0]?.getAttribute('hidden')).toBeNull()
})
@@ -2277,7 +2281,7 @@ describe('ChatView', () => {
const view = render(<h.ChatView {...h.props} />)
expect(renderedFlowKinds(view.container)).toEqual(['user', 'turn-process'])
expect(turnProcessControl(view.container)).toBeNull()
expect(runningContent(view.container)?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/)
expect(runningContent(view.container)?.textContent).toMatch(/^深度求索中,用时 \d+秒 ···$/)
expect(view.container.querySelector('[data-chat-flow-kind="system-prompt"]')).toBeNull()
act(() => {
@@ -2337,7 +2341,7 @@ describe('ChatView', () => {
turnEnds: new Map([[1, 4]]),
})
const view = render(<h.ChatView {...h.props} />)
const toggle = view.getByRole('button', { name: '已完成工作' }) as HTMLButtonElement
const toggle = view.getByRole('button', { name: '已完成' }) as HTMLButtonElement
const contextRow = view.container.querySelector<HTMLElement>('[data-chat-flow-kind="context"]')
expect(toggle.disabled).toBe(true)
@@ -2460,7 +2464,7 @@ describe('ChatView', () => {
})
const view = render(<h.ChatView {...h.props} />)
expect(turnProcessControl(view.container)).toBeNull()
expect(runningContent(view.container)?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/)
expect(runningContent(view.container)?.textContent).toMatch(/^深度求索中,用时 \d+秒 ···$/)
const processRow = view.getByText('inspect').closest('[data-chat-flow-kind="assistant-step"]') as HTMLElement
expect(processRow.getAttribute('hidden')).toBeNull()
@@ -2474,14 +2478,14 @@ describe('ChatView', () => {
})
const toggle = turnProcessControl(view.container)!
expect(runningContent(view.container)).toBeNull()
expect(toggle.textContent).toBe('用时 5秒')
expect(toggle.textContent).toBe('已完成,用时 5秒')
expect(toggle.disabled).toBe(false)
expect(toggle.getAttribute('aria-expanded')).toBe('false')
expect(processRow.getAttribute('hidden')).toBe('until-found')
fireEvent.click(toggle)
act(() => { vi.advanceTimersByTime(1_000) })
expect(toggle.getAttribute('aria-expanded')).toBe('true')
expect(toggle.textContent).toBe('用时 5秒')
expect(toggle.textContent).toBe('已完成,用时 5秒')
expect(processRow.getAttribute('hidden')).toBeNull()
}))
@@ -2527,7 +2531,7 @@ describe('ChatView', () => {
turnEnds: new Map([[1, 4]]),
})
const view = render(<h.ChatView {...h.props} />)
const toggle = view.getByRole('button', { name: '已完成工作' })
const toggle = view.getByRole('button', { name: '已完成' })
const reasoning = view.container.querySelector<HTMLElement>('[data-turn-process-inline]')
expect(toggle.getAttribute('aria-expanded')).toBe('false')
expect(reasoning?.getAttribute('hidden')).toBe('until-found')
@@ -2591,7 +2595,7 @@ describe('ChatView', () => {
}) })
const toggle = turnProcessControl(view.container)!
expect(toggle.getAttribute('aria-expanded')).toBe('false')
expect(toggle.textContent).toBe('已完成工作')
expect(toggle.textContent).toBe('已完成')
expect(view.container.querySelector('[data-chat-node-key="fixture:assistant:2"]')).toBe(processRow)
expect(processRow.getAttribute('hidden')).toBe('until-found')
expect(view.getByLabelText('回到底部')).toBeTruthy()
@@ -2659,7 +2663,7 @@ describe('ChatView', () => {
}) })
expect(turnProcessControl(view.container)).toBe(toggle)
expect(toggle.getAttribute('aria-expanded')).toBe('false')
expect(toggle.textContent).toBe(h.props.t('message.turnProcess.took', { duration: formatRunDuration(5_000, h.props.t) }))
expect(toggle.textContent).toBe(h.props.t('message.turnProcess.took') + formatRunDuration(5_000, h.props.t))
expect(row.getAttribute('hidden')).toBe('until-found')
})
@@ -2770,7 +2774,7 @@ describe('ChatView', () => {
expect(view.container.querySelector('[data-chat-group-key]')).toBe(group)
expect(group?.hasAttribute('hidden')).toBe(!manualOpen)
}
expect(toggle.textContent).toBe(h.props.t('message.turnProcess.took', { duration: formatRunDuration(4_000, h.props.t) }))
expect(toggle.textContent).toBe(h.props.t('message.turnProcess.took') + formatRunDuration(4_000, h.props.t))
act(() => { h.set({ hasMore: false }) })
expect(toggle.getAttribute('aria-expanded')).toBe(String(manualOpen))
})
@@ -2840,7 +2844,7 @@ describe('ChatView', () => {
const view = render(<h.ChatView {...h.props} />)
const toggle = turnProcessControl(view.container)!
const processRow = view.getByText('working').closest('[data-chat-flow-kind="assistant-step"]')!
expect(toggle.textContent).toBe('用时 5秒')
expect(toggle.textContent).toBe('已完成,用时 5秒')
expect(toggle.disabled).toBe(true)
expect(toggle.getAttribute('aria-expanded')).toBeNull()
expect(processRow.hasAttribute('hidden')).toBe(false)
@@ -2936,7 +2940,7 @@ describe('ChatView', () => {
})
const view = render(<h.ChatView {...h.props} />)
// The exact turn/end includes trailing tool activity after the final text.
expect(view.container.querySelector('[data-turn-tail="1"]')?.textContent).not.toContain('用时 19秒')
expect(view.container.querySelector('[data-turn-tail="1"]')?.textContent).not.toContain('已完成,用时 19秒')
})
it('the assistant footer omits hour-scale run time', () => {
@@ -2952,7 +2956,7 @@ describe('ChatView', () => {
})
const view = render(<h.ChatView {...h.props} />)
expect(view.container.querySelector('[data-turn-tail="1"]')?.textContent)
.not.toContain('用时 1小时05分03秒')
.not.toContain('已完成,用时 1小时5分3秒')
})
it('the settled footer shows usage only in Detailed mode', () => {
@@ -2992,7 +2996,7 @@ describe('ChatView', () => {
expect(view.queryByRole('dialog')).toBeNull()
const footer = view.container.querySelector<HTMLElement>('[data-turn-tail="1"]')!
expect(within(footer).queryByRole('button', { name: /用时/ })).toBeNull()
expect(turnProcessControl(view.container)?.textContent).toBe('用时 19秒')
expect(turnProcessControl(view.container)?.textContent).toBe('已完成,用时 19秒')
act(() => { h.setPerformanceUsage('compact') })
expect(view.queryByRole('button', { name: /用量/ })).toBeNull()
act(() => { h.setPerformanceUsage('detailed') })
@@ -3013,7 +3017,7 @@ describe('ChatView', () => {
const view = render(<h.ChatView {...h.props} />)
const footer = view.container.querySelector<HTMLElement>('[data-turn-tail="1"]')!
expect(within(footer).queryByRole('button', { name: /用时/ })).toBeNull()
expect(turnProcessControl(view.container)?.textContent).toBe('用时 19秒')
expect(turnProcessControl(view.container)?.textContent).toBe('已完成,用时 19秒')
expect(view.queryByRole('button', { name: /用量/ })).toBeNull()
})
@@ -3207,9 +3211,9 @@ describe('ChatView', () => {
const view = render(<h.ChatView {...h.props} />)
expect(view.getByTestId('tool-seat-r1')).toBeTruthy()
expect(h.toolOwners[0]?.block).toMatchObject({ callId: 'r1', argsRaw: '{"command":"cmd-r1"}' })
expect(view.getByRole('status').textContent).toBe('深度求索中...')
expect(view.getByRole('status').textContent).toBe('深度求索中')
expect(turnProcessControl(view.container)).toBeNull()
expect(runningContent(view.container)?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/)
expect(runningContent(view.container)?.textContent).toMatch(/^深度求索中,用时 \d+秒 ···$/)
}))
it('keeps the Tool renderer mounted when a running call settles into log order', () => {
@@ -3269,9 +3273,9 @@ describe('ChatView', () => {
const view = render(<h.ChatView {...h.props} />)
const status = view.getByRole('status')
const content = runningContent(view.container)!
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒\.\.\.$/)
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒 ···$/)
expect(turnProcessControl(view.container)).toBeNull()
expect(status.textContent).toBe('深度求索中...')
expect(status.textContent).toBe('深度求索中')
expect(status.getAttribute('aria-live')).toBe('polite')
expect(status.getAttribute('aria-atomic')).toBe('true')
expect(content.closest('[aria-live]')).toBeNull()
@@ -3284,23 +3288,23 @@ describe('ChatView', () => {
role: 'user', source: { kind: 'user' },
}] } })
})
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒\.\.\.$/)
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒 ···$/)
const beforeTick = content.textContent
act(() => { vi.advanceTimersByTime(2_000) })
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒\.\.\.$/)
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒 ···$/)
expect(content.textContent).not.toBe(beforeTick)
expect(view.getByRole('status')).toBe(status)
expect(status.textContent).toBe('深度求索中...')
expect(status.textContent).toBe('深度求索中')
act(() => {
h.setSession({ testInbox: { 'next-turn': [], 'next-step': [] } })
h.setChat({ nodes: [trigger, { ...steering(2, 'also', 1), time: 128_000 }] })
})
expect(runningContent(view.container)).toBe(content)
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒\.\.\.$/)
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒 ···$/)
expect(view.getByText('also').closest('[data-pending-steering]')).toBeNull()
act(() => { vi.advanceTimersByTime(1_000) })
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒\.\.\.$/)
expect(status.textContent).toBe('深度求索中...')
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒 ···$/)
expect(status.textContent).toBe('深度求索中')
view.unmount()
expect(vi.getTimerCount()).toBe(0)
}))
@@ -3314,14 +3318,14 @@ describe('ChatView', () => {
)
const view = render(<h.ChatView {...h.props} />)
const content = runningContent(view.container)!
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒\.\.\.$/)
expect(content.textContent).toMatch(/^深度求索中,用时 \d+分\d+秒 ···$/)
act(() => { vi.advanceTimersByTime(1_000) })
expect(content.textContent).toMatch(/^深度求索中,用时 \d+小时\d+分\d+秒\.\.\.$/)
expect(content.textContent).toMatch(/^深度求索中,用时 \d+小时\d+分\d+秒 ···$/)
const hourlyText = content.textContent
act(() => { vi.advanceTimersByTime(303_000) })
expect(content.textContent).toMatch(/^深度求索中,用时 \d+小时\d+分\d+秒\.\.\.$/)
expect(content.textContent).toMatch(/^深度求索中,用时 \d+小时\d+分\d+秒 ···$/)
expect(content.textContent).not.toBe(hourlyText)
expect(view.getByRole('status').textContent).toBe('深度求索中...')
expect(view.getByRole('status').textContent).toBe('深度求索中')
view.unmount()
expect(vi.getTimerCount()).toBe(0)
}))
@@ -4104,8 +4108,8 @@ describe('ChatView', () => {
const column = view.container.querySelector('[data-chat-flow]')!
const status = column.querySelector('[data-chat-running]')
expect(column.lastElementChild).toBe(status)
expect(within(status as HTMLElement).getByRole('status').textContent).toBe('深度求索中...')
expect(status?.lastElementChild?.textContent).toBe('深度求索中...')
expect(within(status as HTMLElement).getByRole('status').textContent).toBe('深度求索中')
expect(status?.lastElementChild?.textContent).toBe('深度求索中')
expect(status?.querySelector('svg')?.parentElement?.getAttribute('aria-hidden')).toBe('true')
expect(status?.querySelector('animate')).not.toBeNull()
act(() => { h.setSession({ running: false }) })
@@ -24,14 +24,14 @@ function statusHarness(startTime?: number) {
describe('RunningStatus', () => {
it('waits for an open Turn start before allocating its clock', () => {
const view = statusHarness()
expect(view.content()?.textContent).toBe('深度求索中...')
expect(view.content()?.textContent).toBe('深度求索中')
expect(vi.getTimerCount()).toBe(0)
view.set(1_000)
expect(view.content()?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/)
expect(view.content()?.textContent).toMatch(/^深度求索中,用时 \d+秒 ···$/)
expect(view.content()?.querySelectorAll('[data-shimmer="true"]')).toHaveLength(1)
expect(vi.getTimerCount()).toBe(1)
view.set()
expect(view.content()?.textContent).toBe('深度求索中...')
expect(view.content()?.textContent).toBe('深度求索中')
expect(vi.getTimerCount()).toBe(0)
})
@@ -41,28 +41,28 @@ describe('RunningStatus', () => {
const status = view.getByRole('status')
const initialText = content?.textContent
act(() => { vi.advanceTimersByTime(2_000) })
expect(content?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/)
expect(content?.textContent).toMatch(/^深度求索中,用时 \d+秒 ···$/)
expect(content?.textContent).not.toBe(initialText)
view.set(7_000)
expect(view.content()).toBe(content)
expect(content?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/)
expect(content?.textContent).toMatch(/^深度求索中,用时 \d+秒 ···$/)
expect(vi.getTimerCount()).toBe(1)
const nextTurnText = content?.textContent
act(() => { vi.advanceTimersByTime(2_000) })
expect(content?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/)
expect(content?.textContent).toMatch(/^深度求索中,用时 \d+秒 ···$/)
expect(content?.textContent).not.toBe(nextTurnText)
expect(view.getByRole('status')).toBe(status)
expect(status.textContent).toBe('深度求索中...')
expect(status.textContent).toBe('深度求索中')
view.unmount()
expect(vi.getTimerCount()).toBe(0)
})
it('keeps the duration nonnegative when the start is ahead of the local clock', () => {
const view = statusHarness(6_000)
expect(view.content()?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/)
expect(view.content()?.textContent).toMatch(/^深度求索中,用时 \d+秒 ···$/)
expect(view.content()?.textContent).not.toContain('-')
act(() => { vi.advanceTimersByTime(3_000) })
expect(view.content()?.textContent).toMatch(/^深度求索中,用时 \d+秒\.\.\.$/)
expect(view.content()?.textContent).toMatch(/^深度求索中,用时 \d+秒 ···$/)
expect(view.content()?.textContent).not.toContain('-')
})
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
/deepseek-ai-dsh-client-ui-deliverables/use-this-package:
en: cd0926783147a496
zh: b9d713c5c19c3aa3
en: 4a2fcdcf13276fef
zh: 8bde03bbbf15840c
/deepseek-ai-dsh-client-ui-deliverables/use-this-package/explicit-deliveries:
en: 0ee4bbe45bb5e473
zh: 9c99d829b25f59ba
+1 -1
View File
@@ -27,7 +27,7 @@ This package renders the changed-files card a finished turn ends with — the fi
Mount this plugin alongside `ui-conversation` and the Host [workspace-changes](../../deliverables/workspace-changes/README.md) plugin; a finished turn then ends with the changed-files card between the closing message's body and its action footer. Without a served summary — a turn that changed no file, the plugin composed out, or a Host restarted since the turn ran — the card is absent and only deliveries and prose links remain; outside a git repository the summary lists file-tool edits only.
Changed-file and delivery entry cards, their file-icon tiles, and hover previews follow the [R16 preview-card rule](../../../docs/ui-radius.md#radius-scale), retaining the same contour on hover.
Changed-file and delivery entry cards and hover previews follow the [R16 preview-card rule](../../../docs/ui-radius.md#radius-scale); their file-icon tiles use the same rule’s R10 exception. Each retains the same contour on hover.
<a id="explicit-deliveries"></a>
### Explicit deliveries
+1 -1
View File
@@ -27,7 +27,7 @@ kind: "package-reference"
与 `ui-conversation` 和 Host 侧的 [workspace-changes](../../deliverables/workspace-changes/README.zh.md) 插件一起挂载本插件;已完成轮次随即以改动文件卡片收尾,位于收尾消息正文与其动作页脚之间。没有可提供的摘要时——本轮没有改动任何文件、该插件被组合出去,或该轮之后 Host 重启过——卡片不出现,只保留交付卡片与正文链接;工作区不在 git 仓库内时摘要只列文件工具的编辑。
改动文件与交付文件的入口卡片、内部文件图标底框和悬停预览浮层遵循 [R16 预览卡片规则](../../../docs/ui-radius.zh.md#radius-scale),hover 保持相同轮廓。
改动文件与交付文件的入口卡片和悬停预览浮层遵循 [R16 预览卡片规则](../../../docs/ui-radius.zh.md#radius-scale);内部文件图标底框采用该规则的 R10 特例。各自的 hover 保持相同轮廓。
<a id="explicit-deliveries"></a>
### 显式交付
@@ -7,7 +7,7 @@ button.header { cursor: pointer; transition: background-color 120ms ease; }
button.header:hover:not(:disabled), button.header:focus-visible { background: var(--changes-hover); }
button.header:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
button.header:disabled { cursor: progress; }
.tile { display: grid; flex: none; place-items: center; box-sizing: border-box; width: 40px; height: 40px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: var(--dsw-radius-lg); background: color-mix(in srgb, var(--dsw-static-neutral-00) 50%, transparent); }
.tile { display: grid; flex: none; place-items: center; box-sizing: border-box; width: 40px; height: 40px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 10px; background: color-mix(in srgb, var(--dsw-static-neutral-00) 50%, transparent); }
:global(body[data-ds-dark-theme]) .tile { background: color-mix(in srgb, var(--dsw-static-neutral-00) 5%, transparent); }
.titles { display: flex; flex: 1; flex-direction: column; min-width: 0; }
.title { overflow: hidden; font-size: 13px; font-weight: 500; line-height: 20px; text-overflow: ellipsis; white-space: nowrap; }
@@ -11,7 +11,7 @@
.file:hover { background: var(--deliverable-hover); }
.cardPreview { position: absolute; z-index: 1; inset: 0; width: 100%; padding: 0; border: 0; border-radius: inherit; background: transparent; cursor: pointer; }
.cardPreview:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
.fileIcon { position: relative; z-index: 2; box-sizing: border-box; flex: 0 0 auto; display: grid; place-items: center; width: 40px; height: 40px; overflow: hidden; pointer-events: none; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: var(--dsw-radius-lg); background: color-mix(in srgb, var(--dsw-static-neutral-00) 50%, transparent); color: var(--dsw-alias-link); }
.fileIcon { position: relative; z-index: 2; box-sizing: border-box; flex: 0 0 auto; display: grid; place-items: center; width: 40px; height: 40px; overflow: hidden; pointer-events: none; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 10px; background: color-mix(in srgb, var(--dsw-static-neutral-00) 50%, transparent); color: var(--dsw-alias-link); }
:global(body[data-ds-dark-theme]) .fileIcon { background: color-mix(in srgb, var(--dsw-static-neutral-00) 5%, transparent); }
.fileBody { position: relative; z-index: 2; display: flex; flex: 1; align-items: center; justify-content: space-between; gap: 12px; min-width: 0; pointer-events: none; }
.details { display: flex; flex: 1; flex-direction: column; justify-content: center; gap: 2px; min-width: 0; }
@@ -1,5 +1,5 @@
/** Present status remains readable beside long workspace paths. */
.summary { margin-left: 8px; display: flex; align-items: center; gap: 8px; min-width: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; }
.summary { margin-left: 8px; display: flex; align-items: center; gap: 8px; min-width: 0; color: inherit; font-size: 12px; }
.summary > :first-child { flex-shrink: 0; }
.paths { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.output { margin: 8px 0; padding: 12px; border-radius: var(--dsw-radius-lg); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; }
@@ -18,8 +18,8 @@
en: 7bae72c97d0449f5
zh: 4589ecc2b216edc5
/deepseek-ai-dsh-client-ui-primitives/use-this-package/component-catalog:
en: 91d4b79159aab5df
zh: 3a2d8461e836bb66
en: b521f773eff5c728
zh: 7b56c5b4777a0b00
/deepseek-ai-dsh-client-ui-primitives/use-this-package/controls-and-icons:
en: b4cd33720a6cf6b8
zh: 8db75bf633789c6d
+2 -2
View File
@@ -52,7 +52,7 @@ Check this table before writing a control in a feature package. A plugin cannot
| `PathLabel` | Single-line file path with subdued directories, a primary filename, and the full path on hover. Fitting paths align left; clipped paths preserve their suffix with a left-edge fade that updates on path and size changes. |
| `StateDot` | Solid green `done`, amber `warning`, red `error`, and neutral-grey `idle` marks in a 10px slot, plus a tertiary-grey 14px rotating `ongoing` loader whose animations pin to document time zero so every visible loader rotates in phase. `aria-hidden`, so the render site owns the name. `appearance="step"` shows a filled check for completion and a hollow pending circle. |
| `ConnectionIndicator` | Inline connection-recovery control across outage, retry, and recovered states. |
| `DisclosureRow` | 24px compact disclosure that lays title and content side by side. Memoized with shallow prop comparison; keep callbacks and React-node props stable when their content is unchanged. |
| `DisclosureRow` | 24px compact disclosure that lays title and content side by side. The header supplies tertiary at rest and secondary on hover; text and icons inherit it regardless of expandability or open state unless the caller supplies a feature-specific override. Semantic status colors remain explicit. Collapsed rows preview a down chevron on hover, and expanded rows keep an up chevron visible. Memoized with shallow prop comparison; keep callbacks and React-node props stable when their content is unchanged. |
| `TextShimmer` | One left-to-right highlight across a row’s title, separators, summary, and suffix, with a 300ms initial delay, a one-second sweep and a 500ms rest. The mask spans the content width, capped by the visible row and tilts 15° from vertical, with a flat peak and soft edges. The moving decoration stays clipped to the row without enlarging its scrollable area. Base text inherits the caller’s color, including hover changes; the theme supplies the translucent highlight. Place icons and chevrons outside `TextShimmer` so only text and separators receive the highlight. A string child uses the same retained text interface; nested `TextShimmer` instances share the outer animation. Composite children render twice while active and must have no effects or element ids. Mark separators with `data-shimmer-decoration` so their fill receives the highlight. `DisclosureRow` forwards `contentClassName` for the text area and `contentLayoutClassName` for its inner layout. The inert decorative copy is excluded from interaction, selection, and accessibility; text updates retain the animation, and reduced-motion mode keeps the static base. |
| `Modal` | Centered dialog over a page mask. A nested dialog can intercept keys with `onKeyDownCapture` before document Escape handlers. The tint and dialog fade in while backdrop blur stays fully applied, honoring reduced motion. Set `backdropBlur={false}` when the caller already blurs the source page. |
| `RiskConfirmation` | Sensitive action gated behind an explicit checkbox. |
@@ -65,7 +65,7 @@ Check this table before writing a control in a feature package. A plugin cannot
| `JsonTree`, `JsonBlock` | Read-only JSON inspection. |
| `MarkdownText`, `MarkdownDelegateProvider`, `CodeBlock` | Untrusted GFM with TeX math, owner-delegated HTTP(S) navigation, and highlighted code. `CodeBlock` accepts opt-in `lineNumbers`; copied source excludes the gutter, and `contentRef` exposes its stable source wrapper to an owner that uses it as a scrollport. Set `showHeader={false}` when the owner supplies its own language and copy toolbar. |
| `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, `WebBlock` | The agent-output card matching each tool-result intent. |
| `icons/*`, `FishLogo`, `BrandWordmark`, `ReferenceIconRegular`/`ReferenceIconMedium`, `LinkIconRegular`/`LinkIconMedium` | Glyphs and brand marks. Use `LinkIconMedium` for 14px clickable-link categories and known-site marks. |
| `icons/*`, `FishLogo`, `BrandWordmark`, `ReferenceIconRegular`/`ReferenceIconMedium`, `LinkIconRegular`/`LinkIconMedium` | Glyphs and brand marks. The thinking glyph keeps an inset orbit within its 16px view box. Use `LinkIconMedium` for 14px clickable-link categories and known-site marks. |
| `PermissionIconReadOnlyRegular`/`Medium`, `PermissionIconWorkspaceWriteRegular`/`Medium`, `PermissionIconFullAccessRegular`/`Medium` | Permission-mode glyphs for read-only, workspace-write, and full-access choices. |
| `PluginArtworkTerminal`/`Loop`/`Subagent`/`Search`/`Default` | Fixed-palette 36×36 plugin artwork; `Terminal` supplies the light-blue prompt in plugin cards and sidebar guide entries. `Default` marks plugins without artwork of their own. Def ids are per-instance, so the same artwork repeats safely on one page. |
| `GuideArtworkBrowser`/`Files` | Fixed-palette 36×36 browser and folder artwork for sidebar guide entries. |
+2 -2
View File
@@ -52,7 +52,7 @@ kind: "package-library"
| `PathLabel` | 单行文件路径:目录使用弱化颜色,文件名使用主色,悬停可查看完整路径。空间足够时靠左显示;溢出时保留尾部并在左侧渐隐,路径或尺寸变化时更新。 |
| `StateDot` | 10px 槽内的绿色 `done`、琥珀色 `warning`、红色 `error`、中性灰色 `idle` 圆点,以及 tertiary 灰色 14px 旋转 `ongoing` loading,其动画固定到文档时间零点,所以所有可见 loading 同相旋转。它是 `aria-hidden` 的,名称由渲染点提供。 `appearance="step"` 以实心勾表示完成、空心圆表示等待。 |
| `ConnectionIndicator` | 行内连接恢复控件,覆盖断线、重试与已恢复三种状态。 |
| `DisclosureRow` | 24px 紧凑折叠行,标题与内容左右排列。使用浅层 prop 比较进行 memo;内容未变时,保持回调与 React 节点 prop 的引用稳定。 |
| `DisclosureRow` | 24px 紧凑折叠行,标题与内容左右排列。标题行默认使用 tertiary,悬停时使用 secondary;文字与图标继承该颜色,不受能否展开及开合状态影响,调用方可提供功能专属覆盖。语义状态色保持显式指定。收起时悬停预览向下箭头,展开后持续显示向上箭头。使用浅层 prop 比较进行 memo;内容未变时,保持回调与 React 节点 prop 的引用稳定。 |
| `TextShimmer` | 同一道高光从左到右扫过一行的标题、分隔符、摘要与后缀:首次等待 300ms,扫动一秒,再静止 500ms。遮罩覆盖内容宽度,并限制在可见行宽内,相对垂直方向倾斜 15°,峰值区域平坦,两侧柔和渐隐。移动的装饰层始终裁剪在行内,不会扩大可滚动区域。底色继承调用方的颜色,包括悬停变化;主题提供半透明高光。图标和箭头放在 `TextShimmer` 外,只有文字和分隔符参与扫光。字符串子节点沿用稳定的文字接口;嵌套的 `TextShimmer` 共用外层动画。活动期间组合子节点渲染两次,因此不得带副作用或元素 id。分隔符标记 `data-shimmer-decoration`,让填色参与扫光。`DisclosureRow` 将 `contentClassName` 传给文字区域,将 `contentLayoutClassName` 传给内部布局。惰性装饰副本不参与交互、选区和无障碍访问;文字更新保留动画,减少动态效果模式保留静态底色。 |
| `Modal` | 页面遮罩之上的居中对话框。嵌套对话框可通过 `onKeyDownCapture` 在文档级 Escape 处理器之前拦截按键。 色层与弹窗淡入,背景模糊始终完整生效,并遵循减少动态效果偏好。调用方已模糊源页面时设置 `backdropBlur={false}`。 |
| `RiskConfirmation` | 以显式复选框把关的敏感操作确认。 |
@@ -65,7 +65,7 @@ kind: "package-library"
| `JsonTree`、`JsonBlock` | 只读 JSON 查看。 |
| `MarkdownText`、`MarkdownDelegateProvider`、`CodeBlock` | 不可信 GFM 与 TeX 数学、owner 委托的 HTTP(S) 导航,以及高亮代码。`CodeBlock` 可通过 `lineNumbers` 开启行号;复制的源码不含行号栏,`contentRef` 则向需要把稳定源码包装节点用作滚动区的 owner 提供该节点。调用方提供自己的语言与复制工具栏时,设置 `showHeader={false}`。 |
| `TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock`、`WebBlock` | 与各类工具结果意图对应的 agent 输出卡片。 |
| `icons/*`、`FishLogo`、`BrandWordmark`、`ReferenceIconRegular`/`ReferenceIconMedium`、`LinkIconRegular`/`LinkIconMedium` | 字形与品牌标识。`LinkIconMedium` 用于 14px 的可点击链接分类及已知站点标记。 |
| `icons/*`、`FishLogo`、`BrandWordmark`、`ReferenceIconRegular`/`ReferenceIconMedium`、`LinkIconRegular`/`LinkIconMedium` | 字形与品牌标识。思考图标的轨道在 16px 视口内保留内边距。`LinkIconMedium` 用于 14px 的可点击链接分类及已知站点标记。 |
| `PermissionIconReadOnlyRegular`/`Medium`、`PermissionIconWorkspaceWriteRegular`/`Medium`、`PermissionIconFullAccessRegular`/`Medium` | 只读、工作区写入与完全访问选项使用的权限模式图形。 |
| `PluginArtworkTerminal`/`Loop`/`Subagent`/`Search`/`Default` | 固定配色的 36×36 插件插画;`Terminal` 为插件卡片和侧边栏开始页入口提供浅蓝色提示符。`Default` 用于没有自有插画的插件。def id 按实例生成,同一插画可在一页中安全重复。 |
| `GuideArtworkBrowser`/`Files` | 固定配色的 36×36 浏览器与文件夹插画,用于侧栏引导入口。 |
@@ -20,12 +20,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[data-expandable] {
cursor: pointer;
}
.row:hover {
color: var(--dsw-alias-label-secondary);
}
.leading {
position: relative;
flex: none;
@@ -38,7 +44,7 @@
padding: 0;
border: none;
background: none;
color: var(--dsw-alias-label-tertiary);
color: inherit;
}
/* Flow-row glyphs render at 14px inside the 16px box; the CSS edge overrides
@@ -80,5 +86,11 @@ button.leading {
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);
color: inherit;
}
@media (prefers-reduced-motion: reduce) {
.row {
transition: none;
}
}
@@ -4,7 +4,7 @@ import { IconChevronDownOutlineRegular, IconChevronUpOutlineRegular } from './ic
import { TextShimmer } from './TextShimmer.tsx'
import css from './DisclosureRow.module.css'
/** Shared 24px disclosure chrome for compact flow rows. */
/** Shared 24px process row: tertiary text and icons, secondary on hover. */
export interface DisclosureRowProps {
icon: ReactNode
title: string
@@ -450,9 +450,9 @@ export const IconEditOutlineMedium = (props: IconProps) => (
const IconThinkOutlineArtwork = ({ size = 16, className, strokeWidth }: WeightedIconProps) => (
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" strokeWidth={strokeWidth}>
<path d="M10.7554 5.24466C13.9891 8.4783 15.3769 12.3333 13.8552 13.8551C12.3335 15.3768 8.4785 13.989 5.24478 10.7553C2.01111 7.52165 0.623307 3.66664 2.14504 2.14491C3.66676 0.623189 7.52178 2.01099 10.7554 5.24466Z" stroke="currentColor" />
<path d="M10.7554 10.7553C7.52178 13.989 3.66676 15.3768 2.14504 13.8551C0.623307 12.3333 2.01111 8.4783 5.24478 5.24466C8.4785 2.01099 12.3335 0.623189 13.8552 2.14491C15.3769 3.66664 13.9891 7.52165 10.7554 10.7553Z" stroke="currentColor" />
<path d="M8.9587 8.00025C8.9587 8.52835 8.5306 8.95655 8.0024 8.95655C7.47429 8.95655 7.04614 8.52835 7.04614 8.00025C7.04614 7.47209 7.47429 7.04395 8.0024 7.04395C8.5306 7.04395 8.9587 7.47209 8.9587 8.00025Z" fill="currentColor" />
<path d="M10.2854 5.71481C12.9673 8.39663 14.1182 11.5938 12.8562 12.8559C11.5942 14.1179 8.39706 12.9669 5.71518 10.2851C3.03333 7.60323 1.88236 4.40608 3.14441 3.14403C4.40644 1.882 7.6036 3.03297 10.2854 5.71481Z" stroke="currentColor" />
<path d="M10.2854 10.2851C7.6036 12.9669 4.40644 14.1179 3.14441 12.8559C1.88236 11.5938 3.03333 8.39663 5.71518 5.71481C8.39706 3.03297 11.5942 1.882 12.8562 3.14403C14.1182 4.40608 12.9673 7.60323 10.2854 10.2851Z" stroke="currentColor" />
<path d="M8.86291 8.0002C8.86291 8.47549 8.47762 8.86087 8.00224 8.86087C7.52694 8.86087 7.1416 8.47549 7.1416 8.0002C7.1416 7.52485 7.52694 7.13953 8.00224 7.13953C8.47762 7.13953 8.86291 7.52485 8.86291 8.0002Z" fill="currentColor" />
</svg>
)
@@ -15,8 +15,8 @@
en: 2843d46e95311bce
zh: ec11bdeb5d3ce818
/deepseek-ai-dsh-client-ui-settings-general/use-this-package:
en: 3be9bf992c10bc6b
zh: b270083651e5dc43
en: 8b75b6fc5a0951e3
zh: 90c54f660988588b
/deepseek-ai-dsh-client-ui-settings-general/use-this-package/the-general-section:
en: 8b8a7c6287bb303b
zh: 19535112531aedd5
@@ -35,7 +35,7 @@ When the section navigation exceeds the panel's available height, the list scrol
In Desktop, the account-row update control shows availability, progress, verification, readiness, and persistent retry feedback. It shares the connection indicator’s 28px height, 8px corners, 14px icon slot, 4px icon gap, and 12px medium text with an 18px line height; update-state border, fill, and text colors remain independently defined. Retry text and its dot use the same brand blue as other update labels; numeric download progress has no ellipsis. The preload carries semantic phase, version, progress, and classified failures; the component resolves every visible and accessible string from the active `settings` locale, including after an in-application language change. Selecting an available update starts downloading; installation requires a separate shell-owned confirmation. A collapsed sidebar shows the same status as a brand-blue dot on its top expand button, including failures. Connection feedback takes priority except during shell-reported installation, when the expected backend disconnect must not hide update status. Failure restores connection feedback. Both controls share one carrier subscription; browser code cannot choose packages or authorize installation. [Desktop updates](../../../apps/desktop/README.md) owns the release workflow. Checking, verifying, and preparing-to-restart tooltips show only the supplied version; without a version, they retain the state label. Desktop update tooltips preserve the complete version and prerelease suffix without adding v or V; their error summaries match the shell update dialogs.
Settings visibility and section selection live in the shell owner store. The shell supplies the effective Settings binding to the contributed launcher for menu keycaps and `aria-keyshortcuts`; the fallback button uses the same binding for hover and keyboard-focus hints and `aria-keyshortcuts`. The Settings command (`Mod+,` by default) toggles the dialog when Settings is in front or no modal is open; the sidebar control opens the same dialog. The command cannot open or close Settings while the shortcut reference or another modal is in front. Held-key repeats do nothing. Initial focus lands on the selected section in the navigation, or on the title when no sections are available, without drawing a focus outline. Tab and directional navigation retain their visible focus indicators. `Mod+/` can open the shortcut reference above Settings; `Escape` closes the top dialog and restores focus to its invoking control. Return focus omits outlines after shortcut, Escape, or pointer dismissal.
Settings visibility and section selection live in the shell owner store. The shell supplies the effective Settings binding to the contributed launcher for menu keycaps and `aria-keyshortcuts`; the fallback button uses the same binding for hover and keyboard-focus hints and `aria-keyshortcuts`. The Settings command (Desktop: `Mod+,`; Windows/macOS Web: `Mod+Alt+,`) toggles the dialog when Settings is in front or no modal is open; the sidebar control opens the same dialog. The command cannot open or close Settings while the shortcut reference or another modal is in front. Held-key repeats do nothing. Initial focus lands on the selected section in the navigation, or on the title when no sections are available, without drawing a focus outline. Tab and directional navigation retain their visible focus indicators. `Mod+/` can open the shortcut reference above Settings; `Escape` closes the top dialog and restores focus to its invoking control. Return focus omits outlines after shortcut, Escape, or pointer dismissal.
### The General section
@@ -35,7 +35,7 @@ kind: "package-reference"
在 Desktop 中,账户行更新控件显示可用版本、进度、验证、就绪和持久重试反馈。它与连接指示器共用 28px 高度、8px 圆角、14px 图标占位、4px 图文间距,以及 12px 中等字重文字和 18px 行高;更新状态的边框、填充和文字颜色独立定义。重试文字及其圆点与其他更新文案使用相同的品牌蓝色;数字下载进度不带省略号。预加载仅传递语义化阶段、版本、进度和失败类别;组件从当前 `settings` locale 解析全部可见与无障碍文案,并在应用内语言变化后同步更新。选择可用更新即开始下载;安装需要独立的壳拥有的确认。侧栏收起时,其顶部展开按钮以品牌蓝色圆点显示相同状态,包括失败状态。连接反馈通常优先展示,但壳报告正在安装时,预期的后端断开不能遮盖更新状态;失败后恢复连接反馈。两个控件共用一次载体订阅;浏览器代码不能选择安装包或授权安装。[Desktop 更新](../../../apps/desktop/README.zh.md)负责发布流程。检查中、校验中和准备重启状态的悬浮提示只显示已提供的版本号;没有版本号时保留状态文案。桌面更新提示完整保留版本号及预发布后缀,不额外添加 v 或 V;错误提示与桌面升级弹窗保持一致。
设置的可见状态和分区选择由 shell owner store 保存。外壳将设置命令的有效绑定传给入口贡献,用于菜单键帽和 `aria-keyshortcuts`;回退按钮使用相同绑定提供悬停、键盘聚焦提示和 `aria-keyshortcuts`。设置位于最前方或没有模态弹窗时,设置命令(默认为 `Mod+,`)切换设置弹窗的开关;侧栏控件打开同一弹窗。快捷键速查或其他模态弹窗位于前方时,该命令不能打开或关闭设置。长按产生的重复按键不执行操作。初始焦点落在导航中选中的分区;没有可用分区时落在标题上,均不绘制焦点外轮廓线。Tab 和方向键导航保留可见焦点提示。`Mod+/` 可在设置上方打开快捷键速查;`Escape` 关闭最上层弹窗,焦点返回触发该弹窗的控件。通过快捷键、Esc 或鼠标关闭后的回焦均不绘制外轮廓线。
设置的可见状态和分区选择由 shell owner store 保存。外壳将设置命令的有效绑定传给入口贡献,用于菜单键帽和 `aria-keyshortcuts`;回退按钮使用相同绑定提供悬停、键盘聚焦提示和 `aria-keyshortcuts`。设置位于最前方或没有模态弹窗时,设置命令(Desktop 为 `Mod+,`,Windows/macOS Web 为 `Mod+Alt+,`)切换设置弹窗的开关;侧栏控件打开同一弹窗。快捷键速查或其他模态弹窗位于前方时,该命令不能打开或关闭设置。长按产生的重复按键不执行操作。初始焦点落在导航中选中的分区;没有可用分区时落在标题上,均不绘制焦点外轮廓线。Tab 和方向键导航保留可见焦点提示。`Mod+/` 可在设置上方打开快捷键速查;`Escape` 关闭最上层弹窗,焦点返回触发该弹窗的控件。通过快捷键、Esc 或鼠标关闭后的回焦均不绘制外轮廓线。
### 「通用」分区
@@ -182,8 +182,8 @@ export function apply(ctx: ClientContext): void {
'desktop:macos': { code: 'Comma', modifiers: ['primary'] },
'desktop:windows': { code: 'Comma', modifiers: ['primary'] },
'desktop:linux': { code: 'Comma', modifiers: ['primary'] },
'web:macos': { code: 'Comma', modifiers: ['primary'] },
'web:windows': { code: 'Comma', modifiers: ['primary'] },
'web:macos': { code: 'Comma', modifiers: ['primary', 'alt'] },
'web:windows': { code: 'Comma', modifiers: ['primary', 'alt'] },
},
regions: ['page', 'editable', 'terminal'], modals: ['settings'],
resolve: ({ modal }) => {
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 0cd57b7b8d8c9b59
/deepseek-ai-dsh-client-ui-shortcuts/use-this-package:
en: 2d35beb03b54d4ea
zh: 12f3c6ec1a79c673
en: 045d38d5efdf1ff0
zh: 86a700339f5abc49
/deepseek-ai-dsh-client-ui-shortcuts/understand-the-implementation:
en: f56f2610e7258825
zh: 249d7cdd55c57166
+1 -1
View File
@@ -25,7 +25,7 @@ Browse the commands available in the current window and find them by action, Eng
<a id="use-this-package"></a>
## Use this package
The Web app bundle mounts this package together with `shortcuts`. The reference shows only registered commands and existing fixed input actions. Desktop shows the effective device keys; commands without a Web default show No shortcut. The Settings entry and Mod+/ open the same single dialog, titled “Keyboard shortcuts” in English and “快捷键” in Chinese. Its 480×600 CSS-pixel card shrinks to fit smaller windows; the header and search remain above an internally scrolling list. The search field receives focus on open without changing its border color. Automatic entry and return focus in the reference, recorder, and reset confirmation omit outlines, including returns after Escape and application close shortcuts. Containers remain outline-free, and Tab and directional navigation retain their visible focus indicators. The Settings entry is labeled “Edit shortcuts”, with an “Open from anywhere” shortcut tooltip, and uses a filled button with the same standard radius as adjacent settings controls and no dropdown icon and reads the same effective binding for its tooltip and `aria-keyshortcuts`; both are absent without a binding.
The Web app bundle mounts this package together with `shortcuts`. The reference shows only registered commands and existing fixed input actions. Desktop shows the effective device keys; commands without a Web default show No shortcut. The Settings entry and Mod+/ open the same single dialog, titled “Keyboard shortcuts” in English and “快捷键” in Chinese. Its 480×600 CSS-pixel card shrinks to fit smaller windows; the header and search remain above an internally scrolling list. The search field receives focus on open without changing its border color. Automatic entry and return focus in the reference, recorder, and reset confirmation omit outlines, including returns after Escape and application close shortcuts. Containers remain outline-free, and Tab and directional navigation retain their visible focus indicators. The Settings entry follows Coding Tools and is labeled “Edit shortcuts”, with an “Open from anywhere” shortcut tooltip, and uses a filled button with the same standard radius as adjacent settings controls and no dropdown icon and reads the same effective binding for its tooltip and `aria-keyshortcuts`; both are absent without a binding.
`Mod+/` opens the reference when no modal is open or Settings is in front, and closes it when the reference is in front. While the reference is in front, the Settings command cannot open or close Settings. Held-key repeats are ignored. Recording, pending writes, and a nested confirmation prevent the reference shortcut from closing the dialog.
+1 -1
View File
@@ -25,7 +25,7 @@ kind: "package-reference"
<a id="use-this-package"></a>
## 使用此包
Web 应用 bundle 将此包与 `shortcuts` 一同挂载。速查仅展示已注册命令和现有固定输入操作。桌面端展示设备对应的有效键位;没有 Web 默认键位的命令显示“暂无快捷键”。设置入口与 Mod+/打开同一个单实例弹窗,中文标题为“快捷键”,英文标题为“Keyboard shortcuts”。卡片尺寸为 480×600 CSS 像素,小窗口中缩小以保持可见;标题和搜索框位于内部滚动列表上方。打开时聚焦搜索框,边框颜色保持不变。速查窗、录键框及重置确认框内的自动进入和回焦均不绘制外轮廓线,包括通过 Esc 和应用关闭快捷键触发的回焦。容器仍不绘制焦点外框,Tab 和方向键导航保留可见焦点提示。设置入口标为“编辑快捷键”,快捷键提示文案为“全局唤起查看”,使用与相邻设置控件一致的标准圆角填充按钮,不带下拉图标,并从同一有效绑定读取提示及 `aria-keyshortcuts`;没有绑定时不显示提示,也不设置该属性。
Web 应用 bundle 将此包与 `shortcuts` 一同挂载。速查仅展示已注册命令和现有固定输入操作。桌面端展示设备对应的有效键位;没有 Web 默认键位的命令显示“暂无快捷键”。设置入口与 Mod+/打开同一个单实例弹窗,中文标题为“快捷键”,英文标题为“Keyboard shortcuts”。卡片尺寸为 480×600 CSS 像素,小窗口中缩小以保持可见;标题和搜索框位于内部滚动列表上方。打开时聚焦搜索框,边框颜色保持不变。速查窗、录键框及重置确认框内的自动进入和回焦均不绘制外轮廓线,包括通过 Esc 和应用关闭快捷键触发的回焦。容器仍不绘制焦点外框,Tab 和方向键导航保留可见焦点提示。设置入口位于代码工作工具之后,标为“编辑快捷键”,快捷键提示文案为“全局唤起查看”,使用与相邻设置控件一致的标准圆角填充按钮,不带下拉图标,并从同一有效绑定读取提示及 `aria-keyshortcuts`;没有绑定时不显示提示,也不设置该属性。
没有模态弹窗或设置位于最前方时,`Mod+/` 打开速查;速查位于最前方时,该快捷键关闭速查。速查位于前方时,设置命令不能打开或关闭设置。长按产生的重复按键不执行操作。录键期间、写入尚未完成或嵌套确认框打开时,速查快捷键不会关闭弹窗。
@@ -40,7 +40,7 @@ export function apply(ctx: Context): void {
const injected = () => ({ platform: ctx.shortcuts.platform, runtime: ctx.shortcuts.runtime, edit, recording, describeBinding,
hooks: { catalog: ctx.shortcuts.catalog, config: ctx.shortcuts.config, fixedCatalog: ctx.shortcuts.fixedCatalog } })
ctx.slots.inject('settings.general.item', () => ctx.slots.register({
name: 'settings.general.item', id: 'shortcuts', order: 20, locale: 'shortcuts', store,
name: 'settings.general.item', id: 'shortcuts', order: 16, locale: 'shortcuts', store,
inject: injected,
}, ShortcutsRow))
ctx.slots.inject('shell.overlay', () => {
+2 -2
View File
@@ -18,8 +18,8 @@
en: a970f0547496634f
zh: 36ef9a85b26e19f1
/deepseek-ai-dsh-client-ui-sidebar/use-this-package/brand-and-new-session:
en: d084cd6732966c41
zh: 466e028ca49b4230
en: 098f85b9287d3147
zh: b921c128ac37d2cd
/deepseek-ai-dsh-client-ui-sidebar/use-this-package/global-panel-entries:
en: 5d3300d3506254c5
zh: 541ae1ee1e10b921
+1 -1
View File
@@ -31,7 +31,7 @@ The sidebar is the navigation shell: users see the brand, start new sessions, co
### Brand and New Session
The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. The expanded New Session button displays the effective binding as trailing grey text on hover or keyboard focus. While the shortcut is visible, the centered icon and label fade before it when space is narrow; CSS reserves the shortcut’s width without measuring the button. Icon-only controls retain tooltips with platform-formatted keycaps, including the resident macOS header control when the sidebar is hidden. All controls expose `aria-keyshortcuts`.
The expanded brand row renders `sidebar.brand.mark` and `sidebar.brand.name` as independent single slots; the collapsed rail renders the same mark slot. On Web and Windows desktop, clicking the expanded brand starts a new session; hovering or focusing it shows no tooltip. Without occupants, the shell uses the fish mark and a localized local-build label. A complete build stacks a code badge below the label as `version[-commit][-dirty]`, using `DSH_CLIENT_VERSION`, the optional 7-character `DSH_CLIENT_COMMIT_HASH`, and `DSH_CLIENT_GIT_DIRTY=true`; missing version metadata omits the badge. New Session targets the explicit Workspace used by a scoped action, otherwise the current Session's Workspace, otherwise the most recently active Workspace; when none exists it clears into the blank New Session page. The expanded New Session button displays the effective binding as trailing grey text on hover or keyboard focus. While the shortcut is visible, the centered icon and label fade before it when space is narrow; CSS reserves the shortcut’s width without measuring the button. Icon-only controls retain tooltips with platform-formatted keycaps, including the resident macOS header control when the sidebar is hidden. All controls expose `aria-keyshortcuts`.
### Global panel entries
+1 -1
View File
@@ -31,7 +31,7 @@ dsh Web 客户端的侧边栏让用户识别当前构建、启动新会话、将
### 品牌与 New Session
展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 展开态的新建会话按钮在悬停或键盘聚焦时于右侧以灰色文字显示当前有效绑定。快捷键可见且空间较窄时,居中的图标与文字在快捷键之前渐隐;CSS 为快捷键保留宽度,无需测量按钮。纯图标控件保留使用平台键位样式的 tooltip,侧栏隐藏时常驻的 macOS 头部控件也保持一致。所有控件均提供 `aria-keyshortcuts`。
展开的品牌行把 `sidebar.brand.mark` 与 `sidebar.brand.name` 渲染为两个独立的 single slot;收起轨道则渲染同一个 mark slot。在 Web 和 Windows 桌面端,点击展开态的品牌会新建会话,悬停或聚焦时不显示 tooltip。没有占位者时,外壳使用鱼形标记和本地化的本地构建标签。完整构建会在标签下方显示代码徽标;该徽标使用 `DSH_CLIENT_VERSION`、可选的 7 位 `DSH_CLIENT_COMMIT_HASH` 与 `DSH_CLIENT_GIT_DIRTY=true` 组装成 `version[-commit][-dirty]`;缺少版本元数据时不显示徽标。New Session 优先使用作用域操作明确指定的 Workspace,否则使用当前 Session 所属 Workspace,再否则使用最近活跃 Workspace;一个 Workspace 都没有时则清空选择,进入空白 New Session 页面。 展开态的新建会话按钮在悬停或键盘聚焦时于右侧以灰色文字显示当前有效绑定。快捷键可见且空间较窄时,居中的图标与文字在快捷键之前渐隐;CSS 为快捷键保留宽度,无需测量按钮。纯图标控件保留使用平台键位样式的 tooltip,侧栏隐藏时常驻的 macOS 头部控件也保持一致。所有控件均提供 `aria-keyshortcuts`。
### 全局面板入口
@@ -241,17 +241,15 @@ export function SidebarRoot({
return darwinDesktop
? <span className={clsx(css.brand, css.wide)}>{identity}</span>
: (
<Tooltip label={t('session.new.label')} shortcutKeys={newShortcut?.keys} delayMs={500}>
<button
type="button"
className={clsx(css.brand, css.wide)}
aria-label={t('session.new.label')}
aria-keyshortcuts={newShortcut?.aria}
onClick={() => { startSession() }}
>
{identity}
</button>
</Tooltip>
<button
type="button"
className={clsx(css.brand, css.wide)}
aria-label={t('session.new.label')}
aria-keyshortcuts={newShortcut?.aria}
onClick={() => { startSession() }}
>
{identity}
</button>
)
})()}
{!darwinDesktop && toggle}
+2 -2
View File
@@ -21,8 +21,8 @@
en: 2ba1a6158c85b46e
zh: 9ab58ceef8743849
/deepseek-ai-dsh-client-ui-skill/use-this-package/the-skill-tool-row:
en: 452686249f73702d
zh: 47b0a191dfacba30
en: 975c6cd80af3684a
zh: 395c10e51c6a15f6
/deepseek-ai-dsh-client-ui-skill/understand-the-implementation:
en: df2a0ee9d2638f41
zh: 3b4af7e505e990ca
+1 -1
View File
@@ -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 one left-to-right highlight 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.
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 one left-to-right highlight 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. Text and icons are tertiary at rest and secondary on hover; error and interruption text retains its semantic color.
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.
+1 -1
View File
@@ -35,7 +35,7 @@ kind: "package-reference"
`preparing` 阶段只显示技能图标与标题,不显示请求加载的技能名称,也不能展开。`start` 和 `result` 阶段使用下述记录参数与输出;准备阶段不查询已安装目录。
收起的行显示 skill 图标、`Skill` 标题与请求加载的 skill 名称,并与普通工具行使用相同的 secondary 字号轴、行高、行高度和图标缩放。所有生命周期状态都保留普通 skill 图标;运行中的调用以同一道高光从左到右扫过标题、分隔符与 skill 名称,失败时以错误首行替换名称,中断则保留明确状态文本。已结算的行展开为一个尺寸受限的 `Instructions` 卡片,其中原样呈现持久化的工具输出;可用时还会提供标准轨迹的 `Inspect` 入口,展开卡片保留自身排版。该行的名称、生命周期与正文只派生自 ui-tool 提供的冻结调用/结果切片,绝不读取当前目录,因此即使已安装的 skill 或其描述发生变化,回放仍保持稳定。
收起的行显示 skill 图标、`Skill` 标题与请求加载的 skill 名称,并与普通工具行使用相同的 secondary 字号轴、行高、行高度和图标缩放。所有生命周期状态都保留普通 skill 图标;运行中的调用以同一道高光从左到右扫过标题、分隔符与 skill 名称,失败时以错误首行替换名称,中断则保留明确状态文本。已结算的行展开为一个尺寸受限的 `Instructions` 卡片,其中原样呈现持久化的工具输出;可用时还会提供标准轨迹的 `Inspect` 入口,展开卡片保留自身排版。该行的名称、生命周期与正文只派生自 ui-tool 提供的冻结调用/结果切片,绝不读取当前目录,因此即使已安装的 skill 或其描述发生变化,回放仍保持稳定。 文字和图标默认使用 tertiary 色,悬停使用 secondary 色;错误和中断文字保留各自的语义色。
鼠标移入 `/name` 时,背景覆盖整个引用。点击已知 skill 会在右侧栏打开提供方给出的 `SKILL.md` 路径,同时保留文本的可编辑性。缓存未就绪时,点击复用该 Session 的目录请求,在完成后按点击时的 Session 地址打开预览。切换预设、重置连接和插件释放会取消待处理的预览;后续点击重新获取当前目录。没有文件路径的 skill 仍可调用,但没有文件预览。
@@ -12,6 +12,12 @@
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] {
@@ -27,7 +33,7 @@
align-items: center;
justify-content: center;
margin-right: 6px;
color: var(--dsw-alias-label-tertiary);
color: inherit;
}
.leading svg {
@@ -36,7 +42,7 @@
}
.chevron {
color: var(--dsw-alias-label-secondary);
color: inherit;
}
.iconIdle {
@@ -65,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 {
@@ -85,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 {
@@ -189,6 +193,7 @@
}
@media (prefers-reduced-motion: reduce) {
.row,
.iconIdle,
.chevronHover,
.inspectButton {
+6 -6
View File
@@ -9,8 +9,8 @@
en: 06c7d7624947a72a
zh: ac4ca82984564825
/deepseek-ai-dsh-client-ui-theme/summary:
en: c2a579fd56c0c4e2
zh: f1842fbb209f876c
en: c98899d108fd8318
zh: ae2103e46d723784
/deepseek-ai-dsh-client-ui-theme/table-of-contents:
en: d152484eb41ac6b4
zh: 09388d293f9be9cb
@@ -18,8 +18,8 @@
en: 5d64b3d918d5edfa
zh: fc929354a0faf33a
/deepseek-ai-dsh-client-ui-theme/use-this-package/appearance-and-font-size:
en: 0b0177e1afefe4a7
zh: 52d88f12a8369ee8
en: ec98904b19d1b12b
zh: 59d5823ddb168195
/deepseek-ai-dsh-client-ui-theme/use-this-package/registering-a-theme:
en: 2cd32ccaba33b3e9
zh: 53ea78d19929ea02
@@ -30,8 +30,8 @@
en: 8fbfdb050e880f9b
zh: dd201f049036c5c4
/deepseek-ai-dsh-client-ui-theme/understand-the-implementation/stylesheets:
en: 76374ee500418cfa
zh: 741256fbc54c8207
en: 4bbf1beae9e25e9c
zh: 6494ac4d34b1fabb
/deepseek-ai-dsh-client-ui-theme/understand-the-implementation/scrollbar-rebinding:
en: bf76e7c34a331595
zh: 5accd0260ef7c61e
+4 -2
View File
@@ -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 10 to 22 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 10 to 22 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
@@ -69,6 +69,8 @@ System toasts use `--dsw-alias-toast-bg` and `--dsw-alias-toast-label` for a sha
`--dsw-alias-label-shimmer` supplies an overlay for the shared text shimmer: black at 30% 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. Dark notices use brighter interactive layers so the resting card remains distinct from the transcript background.
`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.
`corner-shape.css` smooths every rounded corner: inside `@supports (corner-shape: superellipse(1.5))` it defines `--dsw-corner-shape` and applies it to all elements and their `::before`/`::after` through the universal selector, so engines without `corner-shape` keep circular corners. Full-round shapes — `border-radius: 50%` circles and pill radii — pair `corner-shape: round` with their radius in the owning component sheet because a superellipse deforms them; the corner-shape stylesheet spec enforces that pairing across every package stylesheet.
+4 -2
View File
@@ -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`,并把会话正文字号设为 10 至 22 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 页面把两个选择都保留在进程内。
插件在「通用」分区注册外观偏好方块与字号步进器。步进器接受 10 至 22 px 的整数,默认值为 14 px。它以相同增量调整会话标题与基础文本,包括用户气泡与 composer 草稿;流内行的标题、摘要与表格跟随比正文低一档的字号,小号文本和代码保持固定字号。每次通过的变更都经 Host settings API 写入。连续快速变更按操作顺序携带命名空间 revision 串行写入,最新写入被拒时重新加载持久值。非 loopback 页面把两个选择都保留在进程内。
### 注册主题
@@ -69,6 +69,8 @@ kind: "package-reference"
`--dsw-alias-label-shimmer` 为共享文字扫光提供叠加色:浅色配色使用 30% 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 将同一份样式表、字体和许可证打包,用于欢迎页品牌文字的离线显示;普通界面保留系统字体栈。
`corner-shape.css` 平滑所有圆角:在 `@supports (corner-shape: superellipse(1.5))` 内定义 `--dsw-corner-shape`,并通过通配选择器应用到所有元素及其 `::before`/`::after`,因此不支持 `corner-shape` 的引擎保持普通圆弧。正圆形状——`border-radius: 50%` 的圆与胶囊半径——因超级椭圆会使其变形,须在所属组件样式表中把 `corner-shape: round` 与半径声明配对;corner-shape 样式表 spec 跨全部包样式表强制这一配对。
@@ -262,6 +262,8 @@ body {
--dsw-alias-toast-label: var(--dsw-static-neutral-bluish-00);
--dsw-alias-tooltip-bg: var(--dsw-static-neutral-bluish-850);
--dsw-alias-tooltip-key-bg: color-mix(in srgb, var(--dsw-alias-tooltip-bg), white 18%);
--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);
@@ -375,6 +377,8 @@ body[data-ds-dark-theme] {
--dsw-alias-switch-thumb: var(--dsw-static-neutral-bluish-400);
--dsw-alias-toast-bg: var(--dsw-static-neutral-bluish-750);
--dsw-alias-toast-label: var(--dsw-static-neutral-bluish-00);
--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-alias-tooltip-key-bg: color-mix(in srgb, var(--dsw-alias-tooltip-bg), white 18%);
--dsw-specific-bubble-highlight: var(--dsw-static-neutral-bluish-750);
@@ -48,14 +48,11 @@ body[data-ds-dark-theme] {
/* 由 @deepseek-figma-plugin/custom-variable-name 插件导出 */
/* Markdown 阶梯整体按 0.875(正文 16 → 14)等比缩放,四舍五入到整数像素;
重新从 Figma 导出时需要保留这一缩放。 */
/* 正文字号调节:--dsh-content-font-size 由主题 bootstrap 与 ThemePresenter
写在 body 上(整数 px,12–17,默认 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)。式中
max(13px,·) 的作用是让低设置取 −1 而非 −2,不是结果下限——12 设置下
层级触底 11px。small、code 属于密集次级文本,保持固定。 */
/* Bootstrap and ThemePresenter set the integer content size on body (10–22px,
default 14px). Headings and body variants share its pixel delta. Secondary
text uses size − 1 through 14px, then size − 2: 10→9, 14→13, 15→13, 22→20.
max(13px, ...) selects the delta, not a font-size floor. Small and code
typography keep their fixed sizes. */
body {
--dsh-content-font-delta: calc(var(--dsh-content-font-size, 14px) - 14px);
--dsh-content-font-size-secondary: min(calc(var(--dsh-content-font-size, 14px) - 1px), max(13px, calc(var(--dsh-content-font-size, 14px) - 2px)));
@@ -21,10 +21,10 @@ export type ThemePreference = typeof THEME_PREFERENCES[number]
export const DEFAULT_PREFERENCE: ThemePreference = 'system'
/** Smallest accepted content font size (px). */
export const FONT_SIZE_MIN = 12
export const FONT_SIZE_MIN = 10
/** Largest accepted content font size (px). */
export const FONT_SIZE_MAX = 17
export const FONT_SIZE_MAX = 22
/** Content font size when the user-settings document has no override (px). */
export const DEFAULT_FONT_SIZE = 14
@@ -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', 22)
expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('22px')
executeBootstrap('light')
expect(document.body.style.getPropertyValue('--dsh-content-font-size')).toBe('14px')
})
@@ -28,11 +28,13 @@ 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: 10 })
expect(plainConfig(configuration.fiber.config)).toEqual({ preference: 'dark', fontSize: 10 })
await configuration.update({ fontSize: 22 })
expect(plainConfig(configuration.fiber.config)).toEqual({ preference: 'dark', fontSize: 22 })
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: 9 })).rejects.toThrow()
await expect(configuration.update({ fontSize: 23 })).rejects.toThrow()
await fiber.dispose()
})
@@ -51,10 +53,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: 22 })
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('"22px"')
await fiber.dispose()
expect(collect(ctx)).toEqual([{ kind: 'script', placement: 'head', text: 'window.afterTheme=true' }])
})
@@ -1,4 +1,10 @@
{
"ui-deliverables/src/client/ChangedFiles.module.css": [
".tile: border-radius: 10px"
],
"ui-deliverables/src/client/Deliverables.module.css": [
".fileIcon: border-radius: 10px"
],
"ui-schedule/src/client/ClockPicker.module.css": [
".option: border-radius: 10px"
],
@@ -87,11 +87,19 @@ describe('FontSizeRow', () => {
})
it('disables the outward arrow at each bound', () => {
mount(17)
const b = mount(21)
fireEvent.click(arrow('Increase font size'))
expect(b.setFontSize).toHaveBeenCalledWith(22)
act(() => { b.store.actions.sync(22, 1) })
expect(screen.getByText('22')).toBeDefined()
expect(arrow('Increase font size').disabled).toBe(true)
expect(arrow('Decrease font size').disabled).toBe(false)
cleanup()
mount(12)
const c = mount(11)
fireEvent.click(arrow('Decrease font size'))
expect(c.setFontSize).toHaveBeenCalledWith(10)
act(() => { c.store.actions.sync(10, 1) })
expect(screen.getByText('10')).toBeDefined()
expect(arrow('Increase font size').disabled).toBe(false)
expect(arrow('Decrease font size').disabled).toBe(true)
})
@@ -36,31 +36,31 @@ 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', '22px')
try {
expect(make().theme.getTheme().fontSize).toBe(16)
document.body.style.setProperty('--dsh-content-font-size', '99px')
expect(make().theme.getTheme().fontSize).toBe(22)
document.body.style.setProperty('--dsh-content-font-size', '23px')
expect(make().theme.getTheme().fontSize).toBe(14)
} finally {
document.body.style.removeProperty('--dsh-content-font-size')
}
})
it('setFontSize switches, writes through the scope, and republishes; same value is a no-op', () => {
it.each([10, 22])('setFontSize(%i) switches, writes through the scope, and republishes; same value is a no-op', (fontSize) => {
const { theme, events, host } = make()
theme.setFontSize(17)
expect(theme.getTheme().fontSize).toBe(17)
expect(host.set).toHaveBeenCalledWith('fontSize', 17)
theme.setFontSize(fontSize)
expect(theme.getTheme().fontSize).toBe(fontSize)
expect(host.set).toHaveBeenCalledWith('fontSize', fontSize)
expect(events).toHaveLength(1)
theme.setFontSize(17)
theme.setFontSize(fontSize)
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 [9, 23, 14.5, Number.NaN]) {
expect(() => { theme.setFontSize(px) }).toThrow('outside 10..22')
}
expect(events).toHaveLength(0)
expect(host.set).not.toHaveBeenCalled()
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff. Show more