The client indexes tool/code-dispatch events into ConversationSnapshot.codeDispatches (parent callId -> ToolResultNode-shaped sub-calls; live mux and history replay build the identical index). ChatView renders each run_code parent as the new code variant (description summary, program as the expanded monospace body) with its sub-dispatches as always-visible indented rows — every sub-row dispatches through the SAME keyed conversation.chat.toolview hole with the same GenericToolCard fallback, so custom registrations (bash sample) take over sub-rows exactly as top-level rows. The details panel resolves sub-callIds to full logged args and complete output through the native path. Evidence: fixture turn 64 + built-bundle jsdom snapshot, real-machinery jsdom suites (nesting, error state, details, running parent, reference stability), and a recorded code-mode browser e2e round (keyless replay + aria golden). Scaffold gains a toolsMode patch knob.
144 lines
5.1 KiB
TypeScript
144 lines
5.1 KiB
TypeScript
/**
|
|
* Pure row-model derivation for tool summary rows: variant classification,
|
|
* one-line summary and expanded-body text from the frozen call slice. No
|
|
* inline output ever — full results live in the details panel.
|
|
*/
|
|
// The block union's defining home is runtime (fold-product types); this
|
|
// contract only forwards it (type-definition authority stays with the layer
|
|
// that produces the values).
|
|
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client'
|
|
|
|
export type { ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client'
|
|
|
|
/** The frozen slice the chat view hands to toolview components as `block`
|
|
* (both members are cache-stable references off ConversationSnapshot). */
|
|
|
|
/** The eight row variants (think is fed by reasoning blocks, not tool calls). */
|
|
export type ToolRowVariant = 'think' | 'search' | 'read' | 'bash' | 'write' | 'edit' | 'code' | 'others'
|
|
|
|
/** Row state semantic; colors self-supplied via StateDot (design gives none). */
|
|
export type ToolRowState = 'running' | 'ok' | 'error' | 'stopped'
|
|
|
|
/** Figma row titles per variant (design literals, not translatable copy). */
|
|
export const VARIANT_TITLES: Record<ToolRowVariant, string> = {
|
|
think: 'Think', search: 'Search', read: 'Read', bash: 'Bash',
|
|
write: 'Write', edit: 'Edit', code: 'Code', others: 'Tool call',
|
|
}
|
|
|
|
/** Known tool name -> variant. */
|
|
const TOOL_VARIANTS: Record<string, ToolRowVariant> = {
|
|
bash: 'bash',
|
|
read: 'read',
|
|
web_fetch: 'read',
|
|
web_search: 'search',
|
|
grep: 'search',
|
|
glob: 'search',
|
|
write: 'write',
|
|
edit: 'edit',
|
|
run_code: 'code',
|
|
}
|
|
|
|
/**
|
|
* Classify a tool name into its row variant.
|
|
* @param toolName - wire tool name.
|
|
* @returns matching variant, others when unknown.
|
|
*/
|
|
export function classifyTool(toolName: string): ToolRowVariant {
|
|
return TOOL_VARIANTS[toolName] ?? 'others'
|
|
}
|
|
|
|
/** Everything ToolRow needs, derived once from the frozen slice. */
|
|
export interface ToolRowModel {
|
|
variant: ToolRowVariant
|
|
title: string
|
|
summary: string
|
|
/** Expanded-body text (pretty args); null = row not expandable. */
|
|
body: string | null
|
|
state: ToolRowState
|
|
}
|
|
|
|
function parseArgs(argsRaw: string): unknown {
|
|
try {
|
|
return JSON.parse(argsRaw)
|
|
} catch {
|
|
// Non-JSON args (mid-stream truncation): summary/body fall back to the raw string.
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
function firstLine(text: string): string {
|
|
const nl = text.indexOf('\n')
|
|
return nl === -1 ? text : text.slice(0, nl)
|
|
}
|
|
|
|
function pickString(args: Record<string, unknown>, keys: readonly string[]): string | undefined {
|
|
for (const key of keys) {
|
|
const v = args[key]
|
|
if (typeof v === 'string' && v !== '') return v
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/** Summary key preference per variant (args-derived; result-derived summaries are a ledger item). */
|
|
const SUMMARY_KEYS: Record<ToolRowVariant, readonly string[]> = {
|
|
bash: ['description', 'command'],
|
|
read: ['path', 'file_path', 'url'],
|
|
search: ['query', 'pattern', 'url'],
|
|
think: [],
|
|
write: ['path', 'file_path'],
|
|
edit: ['path', 'file_path'],
|
|
code: ['description'],
|
|
others: [],
|
|
}
|
|
|
|
function deriveSummary(variant: ToolRowVariant, argsRaw: string): string {
|
|
const parsed = parseArgs(argsRaw)
|
|
if (typeof parsed !== 'object' || parsed === null) return firstLine(argsRaw)
|
|
const args = parsed as Record<string, unknown>
|
|
const picked = pickString(args, SUMMARY_KEYS[variant])
|
|
if (picked !== undefined) return firstLine(picked)
|
|
for (const v of Object.values(args)) {
|
|
if (typeof v === 'string' && v !== '') return firstLine(v)
|
|
}
|
|
return firstLine(argsRaw)
|
|
}
|
|
|
|
function deriveBody(variant: ToolRowVariant, argsRaw: string): string | null {
|
|
if (argsRaw === '') return null
|
|
const parsed = parseArgs(argsRaw)
|
|
if (parsed === undefined) return argsRaw
|
|
// The code row's expanded body IS the program (monospace via the row's
|
|
// variant styling), not the args JSON envelope around it.
|
|
if (variant === 'code' && typeof parsed === 'object' && parsed !== null) {
|
|
const code = (parsed as Record<string, unknown>).code
|
|
if (typeof code === 'string' && code !== '') return code
|
|
}
|
|
return JSON.stringify(parsed, null, 2)
|
|
}
|
|
|
|
/**
|
|
* Derive the full row model from a frozen call slice.
|
|
* @param toolName - wire tool name (dispatch-supplied; survives windowless results).
|
|
* @param block - RunningToolCall or ToolResultNode off the snapshot caches.
|
|
* @returns the row model.
|
|
*/
|
|
export function toolRowModel(toolName: string, block: ToolCallBlock): ToolRowModel {
|
|
const variant = classifyTool(toolName)
|
|
const done = 'kind' in block
|
|
const argsRaw = (done ? block.call?.argsRaw : block.argsRaw) ?? ''
|
|
const state: ToolRowState = !done ? 'running'
|
|
: block.error?.code === 'interrupted' ? 'stopped'
|
|
: block.isError ? 'error' : 'ok'
|
|
const base = argsRaw === '' ? block.callId : deriveSummary(variant, argsRaw)
|
|
// Others keeps the static "Tool call" title (figma literal); the real tool
|
|
// name rides the mutable summary slot so no information is lost.
|
|
const summary = variant === 'others' && toolName !== '' ? `${toolName} · ${base}` : base
|
|
return {
|
|
variant,
|
|
title: VARIANT_TITLES[variant],
|
|
summary,
|
|
body: deriveBody(variant, argsRaw),
|
|
state,
|
|
}
|
|
}
|