From e30f642e37a8901c6f9575f97dfd03c721c4acc7 Mon Sep 17 00:00:00 2001
From: 07akioni <07akioni2@gmail.com>
Date: Mon, 27 Jul 2026 17:23:36 +0800
Subject: [PATCH 1/7] refactor(todos): update TodoRow styling and logic, add
IconChecklistOutline16, and enhance AssistantMarkdown rendering
---
apps/web/tests/todo-display.snapshot.ts | 2 +-
.../src/client/chat/AssistantMarkdown.tsx | 8 ++++-
.../src/client/toolviews/todo-row.module.css | 35 ++++++++++++++-----
.../src/client/toolviews/todo-row.tsx | 22 ++++++++----
.../tests/coverage-tails.spec.tsx | 14 ++++++++
.../client/ui-primitives/src/icons/index.tsx | 10 ++++++
.../client/ui-primitives/tests/icons.spec.tsx | 4 +--
7 files changed, 77 insertions(+), 18 deletions(-)
diff --git a/apps/web/tests/todo-display.snapshot.ts b/apps/web/tests/todo-display.snapshot.ts
index 3116bf4242..86603b676c 100644
--- a/apps/web/tests/todo-display.snapshot.ts
+++ b/apps/web/tests/todo-display.snapshot.ts
@@ -158,7 +158,7 @@ it('renders the todo_write turn: dedicated tool row + the dock plan strip', asyn
"text": "○浏览器验收",
},
],
- "row": "☰更新任务清单1/3 已完成 · 实现 fixture 样本",
+ "row": "更新任务清单1/3 已完成 · 实现 fixture 样本",
"rowState": "ok",
}
`)
diff --git a/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx b/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
index 0e91afcc07..2e2f8a6678 100644
--- a/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
+++ b/packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
@@ -40,13 +40,19 @@ function ThinkRow({ text, running }: { text: string; running: boolean }) {
export const AssistantMarkdown = memo(function AssistantMarkdown({ blocks, streaming, interrupted }: AssistantMarkdownProps) {
const last = blocks.length - 1
+ // Tool-call heads render as tool rows in the chat view's grouping pass, so
+ // a node that is only those heads (or empty) would paint an empty root
+ // between tool groups — skip the shell unless something visible remains.
+ const hasVisible = streaming === true
+ || interrupted === true
+ || blocks.some((block) => block.kind !== 'tool-call')
+ if (!hasVisible) return null
return (
{blocks.map((block, i) => {
switch (block.kind) {
case 'text': return
case 'reasoning': return
- // Tool-call heads render as tool rows in the chat view's grouping pass.
case 'tool-call': return null
default: return
}
diff --git a/packages/client/ui-conversation/src/client/toolviews/todo-row.module.css b/packages/client/ui-conversation/src/client/toolviews/todo-row.module.css
index ff4068d49c..dd32d56b01 100644
--- a/packages/client/ui-conversation/src/client/toolviews/todo-row.module.css
+++ b/packages/client/ui-conversation/src/client/toolviews/todo-row.module.css
@@ -1,29 +1,44 @@
-/* todo_write plan-update row: title + progress summary on one line. */
+/* todo_write plan-update row: ToolRow chrome (figma 780:53675) —
+ [16 checklist] gap6 [title 14/24] gap8 [2x2 dot] gap8 [summary FILL truncate]. */
.row {
display: flex;
align-items: center;
- gap: 8px;
height: 24px;
min-width: 0;
cursor: pointer;
border-radius: 6px;
- font-size: 13px;
}
.row:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
-.badge {
+.leading {
flex: none;
- color: var(--dsw-alias-state-business-primary);
+ width: 16px;
+ height: 16px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 6px;
+ color: var(--dsw-alias-label-tertiary);
}
.title {
flex: none;
- font-weight: 510;
- color: var(--dsw-alias-label-primary);
+ font-size: 14px;
+ line-height: 24px;
+ color: var(--dsw-alias-label-primary-dimmed);
+}
+
+.sep {
+ flex: none;
+ width: 2px;
+ height: 2px;
+ border-radius: 1px;
+ margin: 0 8px;
+ background: var(--dsw-alias-label-caption);
}
.summary {
@@ -32,11 +47,15 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- color: var(--dsw-alias-label-secondary);
+ font-size: 14px;
+ line-height: 24px;
+ color: var(--dsw-alias-label-tertiary);
}
.err {
flex: none;
+ margin-left: 8px;
color: var(--dsw-alias-state-error-primary);
font-size: 11px;
+ line-height: 16px;
}
diff --git a/packages/client/ui-conversation/src/client/toolviews/todo-row.tsx b/packages/client/ui-conversation/src/client/toolviews/todo-row.tsx
index 353e7a5441..a47322b614 100644
--- a/packages/client/ui-conversation/src/client/toolviews/todo-row.tsx
+++ b/packages/client/ui-conversation/src/client/toolviews/todo-row.tsx
@@ -3,13 +3,13 @@
// hole like the bash sample (a product registration, not a sample). The row
// summarizes the written list (counts + active item) from the call args; the
// durable list itself renders in the TodoPanel above the composer, so the
-// row stays one line.
+// row stays one line. Chrome matches ToolRow (figma 780:53675).
import type { KeyboardEvent } from 'react'
import type { Context } from 'cordis'
-import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconChecklistOutline16, StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ToolRowProps } from '../contract/slots.ts'
-import { toolRowModel } from '../contract/tool-call-model.ts'
+import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts'
import css from './todo-row.module.css'
/** One parsed args item, shape-checked (model JSON: any field may be missing or mistyped). */
@@ -40,6 +40,17 @@ function summarize(argsRaw: string): string | null {
: head
}
+/** Leading-slot state substitution matches ToolRow / bash: icon yields to the
+ * state semantic while running or failed; ok keeps the checklist glyph. */
+function leadingFor(state: ToolRowState) {
+ switch (state) {
+ case 'running': return
+ case 'error': return
+ case 'stopped': return
+ default: return
+ }
+}
+
/** One-line plan update row (click opens the raw args in details). Non-ok
* execution states keep the generic row's dot semantics — a cancelled call
* wrote no todo/write, so it must not read as a completed update. */
@@ -64,10 +75,9 @@ export function TodoRow({ toolName, block, openDetails }: ToolRowProps) {
onClick={openDetails}
onKeyDown={openFromKeyboard}
>
- {model.state === 'ok'
- ?
☰
- :
}
+
{leadingFor(model.state)}
更新任务清单
+
{summary}
{model.state === 'error' &&
failed}
{model.state === 'stopped' &&
已中断}
diff --git a/packages/client/ui-conversation/tests/coverage-tails.spec.tsx b/packages/client/ui-conversation/tests/coverage-tails.spec.tsx
index 18ac9a2891..d5cbf9ec42 100644
--- a/packages/client/ui-conversation/tests/coverage-tails.spec.tsx
+++ b/packages/client/ui-conversation/tests/coverage-tails.spec.tsx
@@ -60,6 +60,20 @@ describe('tails', () => {
expect(stopped.getByText('已停止')).toBeTruthy()
})
+ it('AssistantMarkdown skips the root shell when only tool-call heads remain', () => {
+ // Tool heads are drawn by ChatView's tool groups; an empty root between
+ // groups is layout noise (no text, no pulse, no interrupted marker).
+ const empty = render(
+
,
+ )
+ expect(empty.container.firstChild).toBeNull()
+ const blank = render(
)
+ expect(blank.container.firstChild).toBeNull()
+ })
+
it('a settled others-variant row renders the sparkle icon in the leading slot', () => {
const settled: ToolResultNode = {
kind: 'tool-result', seq: 2, time: 2_000, callId: 'c5',
diff --git a/packages/client/ui-primitives/src/icons/index.tsx b/packages/client/ui-primitives/src/icons/index.tsx
index 4c2083bae1..5af9567b0d 100644
--- a/packages/client/ui-primitives/src/icons/index.tsx
+++ b/packages/client/ui-primitives/src/icons/index.tsx
@@ -653,6 +653,16 @@ export const IconDataOutline16 = ({ size = 16, className }: IconProps) => (
)
+/** ic_checklist_outline_16 (figma extract): two rings + two list bars. */
+export const IconChecklistOutline16 = ({ size = 16, className }: IconProps) => (
+
+)
+
/** ic_ds_List_Pen_outline_16 */
export const IconListPenOutline16 = ({ size = 16, className }: IconProps) => (