fix(gui): todo row keeps running/stopped execution states visible

The row rendered a status only for error, so a call cancelled before
tool/result read as a completed plan update even though no todo/write
occurred. Non-ok states now ride the generic row's StateDot semantics
(ongoing dot while running, warning dot + 已中断 marker when interrupted);
the ok badge stays for settled successful updates.
This commit is contained in:
Chinesezjc
2026-07-25 00:42:40 +08:00
parent 34eef10f04
commit 1f094ae076
2 changed files with 23 additions and 4 deletions

View File

@@ -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 (
<div className={css.row} data-sample="todo-row" onClick={openDetails}>
<span className={css.badge} aria-hidden></span>
<div className={css.row} data-sample="todo-row" data-state={model.state} onClick={openDetails}>
{model.state === 'ok'
? <span className={css.badge} aria-hidden></span>
: <StateDot state={model.state === 'running' ? 'ongoing' : model.state === 'stopped' ? 'warning' : 'error'} />}
<span className={css.title}></span>
<span className={css.summary}>{summary}</span>
{model.state === 'error' && <span className={css.err}>failed</span>}
{model.state === 'stopped' && <span className={css.err}></span>}
</div>
)
}

View File

@@ -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(<TodoRow {...rowProps({ callId: 'c1', name: 'todo_write', argsRaw: args, turn: 1, step: 1, callView: null })} />)
render(<TodoRow {...rowProps({ callId: 'c1', name: 'todo_write', argsRaw: args, turn: 1, step: 1, time: 1_000, callView: null })} />)
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(<TodoRow {...rowProps({ callId: 'c1', name: 'todo_write', argsRaw: args, turn: 1, step: 1, time: 1_000, callView: null })} />)
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(<TodoRow {...rowProps(resultNode(args, { isError: true, error: { name: 'Interrupted', code: 'interrupted' } }))} />)
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(<TodoRow {...rowProps(resultNode('not json', { isError: true }))} />)
expect(screen.getByText('failed')).toBeTruthy()