Files
deepseek-harness/packages/client/ui-conversation/src/client/toolviews/todo-row.tsx
Chinesezjc a01d974c60 Merge remote-tracking branch 'origin/master' into feat/todo-multi-in-progress
# Conflicts:
#	.agents/notes/implemented/feature/2026-07-23-web-todo-display.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-23-web-todo-display.md
#	.agents/notes/implemented/feature/2026-07-23-web-todo-display.zh.md
#	packages/client/ui-conversation/README.i18n.yaml
#	packages/client/ui-conversation/README.md
#	packages/client/ui-conversation/README.zh.md
2026-08-06 11:58:06 +08:00

99 lines
3.9 KiB
TypeScript

// todo_write toolview: plan-flavored summary row replacing the generic
// "Tool call" card, registered into the keyed 'conversation.chat.toolview'
// hole like the bash sample (a product registration, not a sample). The row
// composes ToolRow (chrome, running sweep, whole-row expand) and swaps in a
// summary of the written list (counts + active items) from the call args, with
// the parallel-active count riding ToolRow's non-shrinking summary suffix so a
// narrow row never clips it; the durable list itself renders in the TodoPanel
// above the composer, so the row stays one line until expanded.
import { IconChecklistOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
import type { Context } from 'cordis'
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
import type { ToolRowProps } from '../contract/slots.ts'
import { toolRowModel } from '../contract/tool-call-model.ts'
import { ToolRow } from '../chat/ToolRow.tsx'
import { NS } from '../locales.ts'
import { planSummary, type PlanItemLike } from './plan-summary.ts'
/** Todo row props: the toolview runtime share plus the standard locale seat. */
type TodoRowProps = ToolRowProps & PropsLocale<'conversation'>
function isItem(value: unknown): value is PlanItemLike {
return typeof value === 'object' && value !== null
}
/**
* The row's summary split at the ellipsis boundary: `text` truncates, `extra`
* is the parallel-active count that must not, so a narrow row never clips the
* one part that says several tasks are running.
*/
interface RowSummary {
text: string
extra: number
}
function summarize(argsRaw: string, t: TodoRowProps['t']): RowSummary | null {
let parsed: unknown
try {
parsed = JSON.parse(argsRaw)
} catch {
// Mid-stream truncation or malformed model JSON: fall back to the generic summary.
return null
}
// Valid JSON with an invalid shape (null root, non-array todos, null items —
// a rejected tool/call retains such args verbatim): same generic fallback.
if (typeof parsed !== 'object' || parsed === null) return null
const todos = (parsed as { todos?: unknown }).todos
if (!Array.isArray(todos) || !todos.every(isItem)) return null
const { done, total, activeContent, activeExtra } = planSummary(todos)
const head = t('todo.completed', { done, total })
return {
text: activeContent === null ? head : `${head} · ${activeContent}`,
extra: activeExtra,
}
}
/** One-line plan update row (the whole row toggles the call's Input/Output
* sections, ToolRow's unified expand). Non-ok execution states keep the
* shared 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, inspect, t }: TodoRowProps) {
const model = toolRowModel(toolName, block)
const argsRaw = ('kind' in block ? block.call?.argsRaw : block.argsRaw) ?? ''
const summary = summarize(argsRaw, t) ?? { text: model.summary, extra: 0 }
return (
<ToolRow
t={t}
variant={model.variant}
toolName={toolName}
icon={<IconChecklistOutline14 />}
title={t('todo.rowTitle')}
summary={summary.text}
summarySuffix={summary.extra > 0 ? `+${summary.extra}` : null}
body={model.body}
output={model.output}
errorSummary={model.errorSummary}
state={model.state}
inspect={inspect}
/>
)
}
/**
* The todo row as a plain registrant plugin following the chat toolview
* declaration across independent activation and reload lifetimes.
*/
export const todoToolview = {
name: 'todo-toolview',
inject: ['slots'],
/**
* Register the todo row into the chat view's keyed toolview hole.
* @param ctx - registrant context (disposal rides ctx.effect inside slots.register).
*/
apply(ctx: Context): void {
ctx.slots.inject('conversation.chat.toolview', () =>
ctx.slots.register({ name: 'conversation.chat.toolview', key: 'todo_write', locale: NS }, TodoRow))
},
}