# 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
99 lines
3.9 KiB
TypeScript
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))
|
|
},
|
|
}
|