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 390361d20b..665fc4dfab 100644
--- a/packages/client/ui-conversation/src/client/toolviews/todo-row.tsx
+++ b/packages/client/ui-conversation/src/client/toolviews/todo-row.tsx
@@ -6,6 +6,7 @@
// row stays one line.
import type { Context } from 'cordis'
+import { StateDot } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ToolRowProps } from '../contract/slots.ts'
import { toolRowModel } from '../contract/tool-call-model.ts'
import css from './todo-row.module.css'
@@ -38,17 +39,22 @@ function summarize(argsRaw: string): string | null {
: head
}
-/** One-line plan update row (click opens the raw args in details). */
+/** 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. */
export function TodoRow({ toolName, block, openDetails }: ToolRowProps) {
const model = toolRowModel(toolName, block)
const argsRaw = ('kind' in block ? block.call?.argsRaw : block.argsRaw) ?? ''
const summary = summarize(argsRaw) ?? model.summary
return (
-
-
☰
+
+ {model.state === 'ok'
+ ? ☰
+ : }
更新任务清单
{summary}
{model.state === 'error' && failed}
+ {model.state === 'stopped' && 已中断}
)
}
diff --git a/packages/client/ui-conversation/tests/todo-panel.spec.tsx b/packages/client/ui-conversation/tests/todo-panel.spec.tsx
index 196077e6a4..8971bba2cd 100644
--- a/packages/client/ui-conversation/tests/todo-panel.spec.tsx
+++ b/packages/client/ui-conversation/tests/todo-panel.spec.tsx
@@ -94,10 +94,23 @@ describe('TodoRow', () => {
it('omits the active clause when no item is in progress and reads running-call args', () => {
const args = JSON.stringify({ todos: [{ content: 'x', status: 'completed' }] })
- render(
)
+ render(
)
expect(screen.getByText('1/1 已完成')).toBeTruthy()
})
+ it('keeps the non-ok execution states visible: running dot, interrupted marker', () => {
+ // A running call (no result yet) shows the ongoing dot, never the ok badge.
+ const args = JSON.stringify({ todos: LIST })
+ const running = render(
)
+ expect(running.container.querySelector('[data-state="running"]')).not.toBeNull()
+ expect(running.container.querySelector('[data-state="running"] svg')).not.toBeNull()
+ running.unmount()
+ // A cancelled call wrote no todo/write: the row must not read as a completed update.
+ const stopped = render(
)
+ expect(stopped.container.querySelector('[data-state="stopped"]')).not.toBeNull()
+ expect(stopped.getByText('已中断')).toBeTruthy()
+ })
+
it('falls back to the generic summary on malformed args and flags errors', () => {
render(
)
expect(screen.getByText('failed')).toBeTruthy()