// Per-tool display rules shared across `opencode run` output paths. // // Each known tool (shell, edit, write, subagent, etc.) has a ToolRule that controls // four display hooks: // // view → visibility policy for progress/final scrollback entries and // whether completed finals can render as structured snapshots // run → inline summary for the non-interactive `run` command output // scroll → text formatting for start/progress/final scrollback entries // snap → structured snapshot (code block, diff, task card) for rich // scrollback entries // // Tools not in TOOL_RULES get fallback formatting. import os from "os" import path from "path" import stripAnsi from "strip-ansi" import type { SessionMessageAssistantTool } from "@opencode-ai/client/promise" import { LANGUAGE_EXTENSIONS } from "../util/filetype" import { Locale } from "../util/locale" import { canonicalToolName, finiteNumber, primitiveInputSummary, toolDisplayContent, toolDisplayMetadata, webSearchProviderLabel, } from "../util/tool-display" import { formatPath } from "../util/path-format" import type { RunEntryBody, StreamCommit, ToolSnapshot } from "./types" export { canonicalToolName } from "../util/tool-display" type ToolView = { output: boolean final: boolean snap?: "code" | "diff" | "structured" } type ToolPhase = "start" | "progress" | "final" type ToolDict = Record type PatchFile = { status?: string file?: string from?: string patch?: string deletions?: number } type ToolInput = ToolDict & { id?: string path?: string pattern?: string url?: string query?: string agent?: string description?: string name?: string operation?: string line?: number character?: number content?: string command?: string workdir?: string questions?: Array<{ question?: string }> diff?: string } type ToolMetadata = ToolDict & { name?: string count?: number matches?: number diff?: string provider?: unknown files?: PatchFile[] answers?: string[][] exit?: number } type ToolFrame = { directory?: string raw: string name: string input: ToolDict meta: ToolDict state: ToolDict status: string error: string output: string time: { start?: number end?: number } } type ToolInline = { icon: string title: string description?: string mode?: "inline" | "block" body?: string } type ToolProps = { input: ToolInput metadata: ToolMetadata frame: ToolFrame } type ToolName = | "invalid" | "shell" | "write" | "edit" | "patch" | "batch" | "subagent" | "question" | "read" | "glob" | "grep" | "list" | "lsp" | "webfetch" | "websearch" | "skill" | "plan_exit" type ToolRule = { view: ToolView run: (props: ToolProps) => ToolInline scroll?: Partial string>> snap?: (props: ToolProps) => ToolSnapshot | undefined } type ToolRegistry = Record type AnyToolRule = ToolRule function dict(v: unknown): ToolDict { if (!v || typeof v !== "object" || Array.isArray(v)) { return {} } return { ...v } } function props(frame: ToolFrame): ToolProps { return { input: frame.input, metadata: frame.meta, frame, } } function text(v: unknown): string { return typeof v === "string" ? v : "" } export function toolOutputText(name: string, content: ReadonlyArray<{ type: string; text?: string }> | undefined) { if (!content) return "" // V2 shell content appends model-only status after the user-visible command output. if (canonicalToolName(name) === "shell") return content.find((item) => item.type === "text")?.text ?? "" const joined = content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n") if (canonicalToolName(name) === "read") return readDisplayText(joined) ?? joined return joined } /** Read's model content is a JSON page envelope; unwrap the human-facing text. */ export function readDisplayText(text: string): string | undefined { if (!text.startsWith("{")) return undefined const parsed = (() => { try { return JSON.parse(text) as unknown } catch { return undefined } })() const envelope = dict(parsed) if (typeof envelope.content === "string" && (envelope.type === "text-page" || envelope.encoding === "utf8")) return envelope.content if (!Array.isArray(envelope.entries)) return undefined return envelope.entries .flatMap((entry): string[] => { if (typeof entry === "string") return [entry] const path = dict(entry).path return typeof path === "string" ? [path] : [] }) .join("\n") } function normalizeInput(name: string, value: unknown) { const input = dict(value) const path = typeof input.path === "string" ? input.path : text(input.filePath) const agent = typeof input.agent === "string" ? input.agent : text(input.subagent_type) return { ...input, ...(["read", "write", "edit", "lsp"].includes(name) && path ? { path } : {}), ...(name === "subagent" && agent ? { agent } : {}), } } function normalizeFile(value: unknown): PatchFile | undefined { const file = dict(value) const name = text(file.file) || text(file.relativePath) || text(file.filePath) if (!name) return const legacy = text(file.type) const status = text(file.status) || (legacy === "add" ? "added" : legacy === "delete" ? "deleted" : legacy === "update" ? "modified" : legacy === "move" ? "moved" : legacy) const patch = typeof file.patch === "string" ? file.patch : undefined const deletions = finiteNumber(file.deletions) return { ...file, file: name, ...(status === "moved" && text(file.filePath) ? { from: text(file.filePath) } : {}), ...(status ? { status } : {}), ...(patch === undefined ? {} : { patch }), ...(deletions === undefined ? {} : { deletions }), } } function normalizeMetadata(name: string, value: unknown) { const metadata = dict(value) const files = list(metadata.files).flatMap((item) => { const file = normalizeFile(item) return file ? [file] : [] }) const sessionID = text(metadata.sessionID) || text(metadata.sessionId) return { ...metadata, ...(["edit", "patch"].includes(name) && Array.isArray(metadata.files) ? { files } : {}), ...(name === "subagent" && sessionID ? { sessionID } : {}), } } export function normalizeTool(tool: SessionMessageAssistantTool): SessionMessageAssistantTool { const name = canonicalToolName(tool.name) if (tool.state.status === "streaming") return { ...tool, name } return { ...tool, name, state: { ...tool.state, input: normalizeInput(name, tool.state.input), metadata: normalizeMetadata(name, toolDisplayMetadata(tool.state)), }, } as SessionMessageAssistantTool } function list(v: unknown): T[] { if (!Array.isArray(v)) { return [] } return v } function span(frame: ToolFrame): string { const start = frame.time.start const end = frame.time.end if (start === undefined || end === undefined || end <= start) { return "" } return Locale.duration(end - start) } function fail(ctx: ToolFrame): string { const error = toolError(ctx) if (error) { return `✖ ${ctx.name} failed: ${error}` } return `✖ ${ctx.name} failed` } function toolError(ctx: ToolFrame): string { if (ctx.error) { return ctx.error } const state = text(ctx.state.error).trim() if (state) { return state } return ctx.raw.trim() } function fallbackStart(ctx: ToolFrame): string { const extra = primitiveInputSummary(ctx.input) if (!extra) { return `⚙ ${ctx.name}` } return `⚙ ${ctx.name} ${extra}` } function fallbackFinal(ctx: ToolFrame): string { if (ctx.status === "error") { return fail(ctx) } if (ctx.status && ctx.status !== "completed") { return ctx.raw.trim() } const time = span(ctx) if (!time) { return `${ctx.name} completed` } return `${ctx.name} completed · ${time}` } export function toolPath(input?: string, opts: { home?: boolean; directory?: string } = {}): string { return formatPath(input, { base: opts.directory ?? process.cwd(), home: opts.home ? os.homedir() : undefined, forwardSlashes: true, }) } function displayPath(p: ToolProps, input?: string, opts: { home?: boolean } = {}) { return toolPath(input, { ...opts, directory: p.frame.directory }) } function fallbackInline(ctx: ToolFrame): ToolInline { const title = Object.keys(ctx.input).length > 0 ? JSON.stringify(ctx.input) : "Unknown" return { icon: "⚙", title: `${ctx.name} ${title}`, } } function count(n: number, label: string): string { return `${n} ${label}${n === 1 ? "" : "es"}` } function runGlob(p: ToolProps): ToolInline { const root = p.input.path ?? "" const title = `Glob "${p.input.pattern ?? ""}"` const suffix = root ? `in ${displayPath(p, root)}` : "" const matches = p.metadata.count const description = matches === undefined ? suffix : `${suffix}${suffix ? " · " : ""}${count(matches, "match")}` return { icon: "✱", title, ...(description && { description }), } } function runGrep(p: ToolProps): ToolInline { const root = p.input.path ?? "" const title = `Grep "${p.input.pattern ?? ""}"` const suffix = root ? `in ${displayPath(p, root)}` : "" const matches = p.metadata.matches const description = matches === undefined ? suffix : `${suffix}${suffix ? " · " : ""}${count(matches, "match")}` return { icon: "✱", title, ...(description && { description }), } } function runList(p: ToolProps): ToolInline { const dir = text(dict(p.input).path) return { icon: "→", title: dir ? `List ${displayPath(p, dir)}` : "List", } } function runRead(p: ToolProps): ToolInline { const file = displayPath(p, p.input.path) const description = primitiveInputSummary(p.frame.input, ["path"]) || undefined return { icon: "→", title: `Read ${file}`, ...(description && { description }), } } function runWrite(p: ToolProps): ToolInline { return { icon: "←", title: `Write ${displayPath(p, p.input.path)}`, mode: "block", body: p.frame.status === "completed" ? p.frame.output : undefined, } } function runWebfetch(p: ToolProps): ToolInline { const url = p.input.url ?? "" return { icon: "%", title: url ? `WebFetch ${url}` : "WebFetch", } } function runEdit(p: ToolProps): ToolInline { const file = list(p.metadata.files)[0] return { icon: "←", title: `Edit ${displayPath(p, p.input.path)}`, mode: "block", body: file?.patch ?? p.metadata.diff, } } function runWebSearch(p: ToolProps): ToolInline { const title = webSearchProviderLabel(p.metadata.provider) return { icon: "◈", title: p.input.query ? `${title} "${p.input.query}"` : title, } } function runTask(p: ToolProps): ToolInline { const kind = Locale.titlecase(p.input.agent || "unknown") const desc = p.input.description const icon = p.frame.status === "error" ? "✗" : p.frame.status === "running" ? "•" : "✓" return { icon, title: desc || `${kind} Subagent`, description: desc ? `${kind} Agent` : undefined, } } function runSkill(p: ToolProps): ToolInline { const name = p.metadata.name ?? p.input.id ?? "" return { icon: "→", title: `Skill "${name}"`, } } function runPatch(p: ToolProps): ToolInline { const files = p.metadata.files?.length ?? 0 if (files === 0) { return { icon: "%", title: "Patch", } } return { icon: "%", title: `Patch ${files} file${files === 1 ? "" : "s"}`, } } function runQuestion(p: ToolProps): ToolInline { const total = list(p.frame.input.questions).length return { icon: "→", title: `Asked ${total} question${total === 1 ? "" : "s"}`, } } function runInvalid(p: ToolProps): ToolInline { return { icon: "✗", title: "Invalid Tool", mode: "block", body: p.frame.status === "completed" ? p.frame.output : undefined, } } function runBatch(p: ToolProps): ToolInline { const calls = list(dict(p.input).tool_calls).length return { icon: "#", title: calls > 0 ? `Batch ${calls} tool${calls === 1 ? "" : "s"}` : "Batch", mode: "block", body: p.frame.status === "completed" ? p.frame.output : undefined, } } function lspTitle( input: { operation?: string path?: string line?: number character?: number }, opts: { home?: boolean; directory?: string } = {}, ): string { const op = input.operation || "request" const file = input.path ? toolPath(input.path, opts) : "" const line = typeof input.line === "number" ? input.line : undefined const char = typeof input.character === "number" ? input.character : undefined const pos = line !== undefined && char !== undefined ? `:${line}:${char}` : "" if (!file) { return `LSP ${op}` } return `LSP ${op} ${file}${pos}` } function runLsp(p: ToolProps): ToolInline { return { icon: "→", title: lspTitle(p.input, { directory: p.frame.directory }), } } function runPlanExit(p: ToolProps): ToolInline { return { icon: "→", title: "Switching to build agent", mode: "block", body: p.frame.status === "completed" ? p.frame.output : undefined, } } function patchTitle(file: PatchFile, directory?: string): string { if (file.status === "added") { return `# Created ${toolPath(file.file, { directory })}` } if (file.status === "deleted") { return `# Deleted ${toolPath(file.file, { directory })}` } if (file.status === "moved") { return `# Moved ${toolPath(file.from, { directory })} -> ${toolPath(file.file, { directory })}` } return `# Patched ${toolPath(file.file, { directory })}` } function snapWrite(p: ToolProps): ToolSnapshot | undefined { const file = p.input.path || "" const content = p.input.content || "" if (!file && !content) { return undefined } return { kind: "code", title: `# Wrote ${displayPath(p, file)}`, content, file, } } function snapEdit(p: ToolProps): ToolSnapshot | undefined { const item = list(p.metadata.files)[0] const file = item?.file || p.input.path || "" const diff = item?.patch || p.metadata.diff || "" if (!file || !diff.trim()) { return undefined } return { kind: "diff", items: [ { title: `# Edited ${displayPath(p, file)}`, diff, file, }, ], } } function snapPatch(p: ToolProps): ToolSnapshot | undefined { const files = list(p.frame.meta.files) if (files.length === 0) { return undefined } const items = files.flatMap((file) => { if (!file || typeof file !== "object") { return [] } const diff = typeof file.patch === "string" ? file.patch : "" if (!diff.trim()) { return [] } const name = file.file return [ { title: patchTitle(file, p.frame.directory), diff, file: name, deletions: typeof file.deletions === "number" ? file.deletions : 0, }, ] }) if (items.length !== files.length) { return undefined } return { kind: "diff", items, } } function snapTask(p: ToolProps): ToolSnapshot { const kind = Locale.titlecase(p.input.agent || "general") const desc = p.input.description const rows = [desc].filter((item): item is string => Boolean(item)) return { kind: "task", title: `# ${kind} Subagent`, rows, tail: "", } } function snapQuestion(p: ToolProps): ToolSnapshot { const answers = list(p.frame.meta.answers) const items = list<{ question?: string }>(p.frame.input.questions).map((item, i) => { const answer = list(answers[i]).filter((entry) => typeof entry === "string") return { question: item.question || `Question ${i + 1}`, answer: answer.length > 0 ? answer.join(", ") : "(no answer)", } }) return { kind: "question", items, tail: "", } } function scrollBashStart(p: ToolProps): string { const cmd = p.input.command ?? "" const wd = p.input.workdir ?? "" const formatted = wd && wd !== "." ? displayPath(p, wd) : "" const dir = formatted === "." ? "" : formatted if (cmd && !dir) { return `$ ${cmd}` } if (!cmd) { return dir ? `# Running in ${dir}` : "" } return `# Running in ${dir}\n$ ${cmd}` } function scrollBashProgress(p: ToolProps): string { const out = stripAnsi(p.frame.raw) const cmd = (p.input.command ?? "").trim() const fmt = (text: string) => { const body = text.replace(/^\n+/, "").replace(/\n+$/, "") return body ? `\n${body}` : "" } if (!cmd) { return out.replace(/\n+$/, "") } const wdRaw = (p.input.workdir ?? "").trim() const wd = wdRaw ? displayPath(p, wdRaw) : "" const lines = out.split("\n") const first = (lines[0] || "").trim() const second = (lines[1] || "").trim() if (wd && (first === wd || first === wdRaw) && second === cmd) { return fmt(lines.slice(2).join("\n")) } if (first === cmd || first === `$ ${cmd}`) { return fmt(lines.slice(1).join("\n")) } if (wd && (first === `${wd} ${cmd}` || first === `${wdRaw} ${cmd}`)) { return fmt(lines.slice(1).join("\n")) } return fmt(out) } function scrollShellFinal(p: ToolProps): string { if (p.frame.status === "error") { return fail(p.frame) } const code = p.metadata.exit const time = span(p.frame) if (code === undefined) { if (!time) { return "shell completed" } return `shell completed · ${time}` } return `shell completed (exit ${code})${time ? ` · ${time}` : ""}` } function scrollReadStart(p: ToolProps): string { const file = displayPath(p, p.input.path) const extra = primitiveInputSummary(p.frame.input, ["path"]) const tail = extra ? ` ${extra}` : "" return `→ Read ${file}${tail}`.trim() } function scrollWriteStart(_: ToolProps): string { return "" } function scrollEditStart(_: ToolProps): string { return "" } function scrollPatchStart(_: ToolProps): string { return "" } function patchLine(file: PatchFile, directory?: string): string { if (file.status === "added") { return `+ Created ${toolPath(file.file, { directory })}` } if (file.status === "deleted") { return `- Deleted ${toolPath(file.file, { directory })}` } if (file.status === "moved") { return `→ Moved ${toolPath(file.from, { directory })} → ${toolPath(file.file, { directory })}` } return `~ Patched ${toolPath(file.file, { directory })}` } function scrollPatchFinal(p: ToolProps): string { if (p.frame.status === "error") { return fail(p.frame) } const files = list(p.frame.meta.files) if (files.length === 0) { const time = span(p.frame) if (!time) { return "patch" } return `patch · ${time}` } const showModified = !files.some((file) => file?.status && file.status !== "modified") const shown = files.filter((file) => showModified || file.status !== "modified") const rows = shown.slice(0, 6).map((file) => patchLine(file, p.frame.directory)) if (shown.length > 6) { rows.push(`... and ${shown.length - 6} more`) } if (rows.length > 0) { return rows.join("\n") } return patchLine(files[0]!, p.frame.directory) } function scrollTaskStart(_: ToolProps): string { return "" } function taskResult(output: string): string | undefined { if (!output.trim()) { return undefined } const match = output.match(/\s*([\s\S]*?)\s*<\/task_result>/) if (match) { return match[1].trim() || undefined } const next = output .split("\n") .filter((line) => !line.startsWith("task_id:")) .join("\n") .trim() return next || undefined } function scrollTaskFinal(p: ToolProps): string { if (p.frame.status === "error") { return fail(p.frame) } const kind = Locale.titlecase(p.input.agent || "general") const row = p.input.description if (!row) { return `# ${kind} Subagent` } return `# ${kind} Subagent\n${row}` } function scrollQuestionStart(_: ToolProps): string { return "" } function scrollQuestionFinal(p: ToolProps): string { const q = p.input.questions ?? [] const a = p.metadata.answers ?? [] const time = span(p.frame) if (q.length === 0) { if (!time) { return "0 questions" } return `0 questions · ${time}` } const rows: string[] = [] for (const [i, item] of q.slice(0, 4).entries()) { const prompt = item.question const reply = a[i] ?? [] rows.push(`? ${prompt || `Question ${i + 1}`}`) rows.push(` ${reply.length > 0 ? reply.join(", ") : "(no answer)"}`) } if (q.length > 4) { rows.push(`... and ${q.length - 4} more`) } return rows.join("\n") } function scrollLspStart(p: ToolProps): string { return `→ ${lspTitle(p.input, { directory: p.frame.directory })}` } function scrollSkillStart(p: ToolProps): string { return `→ Skill "${p.metadata.name ?? p.input.id ?? ""}"` } function scrollGlobStart(p: ToolProps): string { const pattern = p.input.pattern ?? "" const head = pattern ? `✱ Glob "${pattern}"` : "✱ Glob" const dir = p.input.path ?? "" if (!dir) { return head } return `${head} in ${displayPath(p, dir)}` } function scrollGlobFinal(p: ToolProps): string { return toolError(p.frame) || fail(p.frame) } function scrollGrepStart(p: ToolProps): string { const pattern = p.input.pattern ?? "" const head = pattern ? `✱ Grep "${pattern}"` : "✱ Grep" const dir = p.input.path ?? "" if (!dir) { return head } return `${head} in ${displayPath(p, dir)}` } function scrollListStart(p: ToolProps): string { const dir = text(dict(p.input).path) if (!dir) { return "→ List" } return `→ List ${displayPath(p, dir)}` } function scrollWebfetchStart(p: ToolProps): string { const url = p.input.url ?? "" if (!url) { return "% WebFetch" } return `% WebFetch ${url}` } function scrollWebSearchStart(p: ToolProps): string { const title = webSearchProviderLabel(p.metadata.provider) const query = p.input.query ?? "" if (!query) { return `◈ ${title}` } return `◈ ${title} "${query}"` } const TOOL_RULES = { invalid: { view: { output: true, final: false, }, run: runInvalid, scroll: { start: () => "", }, }, shell: { view: { output: true, final: false, }, run: runShell, scroll: { start: scrollBashStart, progress: scrollBashProgress, final: scrollShellFinal, }, }, write: { view: { output: false, final: true, snap: "code", }, run: runWrite, snap: snapWrite, scroll: { start: scrollWriteStart, }, }, edit: { view: { output: false, final: true, snap: "diff", }, run: runEdit, snap: snapEdit, scroll: { start: scrollEditStart, }, }, patch: { view: { output: false, final: true, snap: "diff", }, run: runPatch, snap: snapPatch, scroll: { start: scrollPatchStart, final: scrollPatchFinal, }, }, batch: { view: { output: true, final: false, }, run: runBatch, scroll: { start: () => "", }, }, subagent: { view: { output: false, final: true, snap: "structured", }, run: runTask, snap: snapTask, scroll: { start: scrollTaskStart, final: scrollTaskFinal, }, }, question: { view: { output: false, final: true, snap: "structured", }, run: runQuestion, snap: snapQuestion, scroll: { start: scrollQuestionStart, final: scrollQuestionFinal, }, }, read: { view: { output: false, final: false, }, run: runRead, scroll: { start: scrollReadStart, }, }, glob: { view: { output: false, final: false, }, run: runGlob, scroll: { start: scrollGlobStart, final: scrollGlobFinal, }, }, grep: { view: { output: false, final: false, }, run: runGrep, scroll: { start: scrollGrepStart, }, }, list: { view: { output: false, final: false, }, run: runList, scroll: { start: scrollListStart, }, }, lsp: { view: { output: false, final: false, }, run: runLsp, scroll: { start: scrollLspStart, }, }, webfetch: { view: { output: false, final: false, }, run: runWebfetch, scroll: { start: scrollWebfetchStart, }, }, websearch: { view: { output: false, final: false, }, run: runWebSearch, scroll: { start: scrollWebSearchStart, }, }, skill: { view: { output: false, final: false, }, run: runSkill, scroll: { start: scrollSkillStart, }, }, plan_exit: { view: { output: true, final: false, }, run: runPlanExit, scroll: { start: () => "", }, }, } as const satisfies ToolRegistry function key(name: string): name is ToolName { return Object.prototype.hasOwnProperty.call(TOOL_RULES, name) } function rule(name?: string): AnyToolRule | undefined { if (!name || !key(name)) { return undefined } return TOOL_RULES[name] } function frame(part: SessionMessageAssistantTool, directory?: string): ToolFrame { const tool = normalizeTool(part) if (tool.state.status === "streaming") return { directory, raw: tool.state.input, name: tool.name, input: {}, meta: {}, state: dict(tool.state), status: tool.state.status, error: "", output: "", time: { start: tool.time.created }, } const output = toolOutputText(tool.name, toolDisplayContent(tool.state)) return { directory, raw: output, name: tool.name, input: normalizeInput(tool.name, tool.state.input), meta: normalizeMetadata(tool.name, tool.state.metadata), state: dict(tool.state), status: tool.state.status, error: tool.state.status === "error" ? tool.state.error.message : "", output, time: { start: tool.time.ran ?? tool.time.created, end: tool.time.completed, }, } } function toolFrame(commit: StreamCommit, raw: string): ToolFrame { const current = commit.part ? frame(commit.part, commit.directory) : undefined return { directory: commit.directory, raw, name: canonicalToolName(commit.tool || current?.name || "tool"), input: current?.input ?? {}, meta: current?.meta ?? {}, state: current?.state ?? {}, status: commit.toolState ?? current?.status ?? "", error: (commit.toolError ?? current?.error ?? "").trim(), output: current?.output ?? raw, time: current?.time ?? {}, } } function runShell(p: ToolProps): ToolInline { return { icon: "$", title: p.input.command || "", mode: "block", body: p.frame.status === "completed" ? p.frame.output.trim() : undefined, } } export function toolView(name?: string): ToolView { return ( rule(name ? canonicalToolName(name) : undefined)?.view ?? { output: true, final: true, } ) } export function toolStructuredFinal(commit: StreamCommit): boolean { const state = commit.toolState ?? commit.part?.state.status return ( commit.kind === "tool" && commit.phase === "final" && state === "completed" && Boolean(toolView(commit.tool ?? commit.part?.name).snap) ) } export function toolInlineInfo(part: SessionMessageAssistantTool, directory?: string): ToolInline { const ctx = frame(part, directory) const draw = rule(ctx.name)?.run try { if (draw) { return draw(props(ctx)) } } catch { return fallbackInline(ctx) } return fallbackInline(ctx) } export function toolScroll(phase: ToolPhase, ctx: ToolFrame): string { const draw = rule(ctx.name)?.scroll?.[phase] try { if (draw) { return draw(props(ctx)) } } catch { if (phase === "start") { return fallbackStart(ctx) } if (phase === "progress") { return ctx.raw } return fallbackFinal(ctx) } if (phase === "start") { return fallbackStart(ctx) } if (phase === "progress") { return ctx.raw } return fallbackFinal(ctx) } function toolSnapshot(commit: StreamCommit, raw: string): ToolSnapshot | undefined { const ctx = toolFrame(commit, raw) const draw = rule(ctx.name)?.snap if (!draw) { return undefined } try { return draw(props(ctx)) } catch { return undefined } } function textBody(content: string): RunEntryBody | undefined { if (!content) { return undefined } return { type: "text", content, } } function markdownBody(content: string): RunEntryBody | undefined { if (!content) { return undefined } return { type: "markdown", content, } } function structuredBody(commit: StreamCommit, raw: string): RunEntryBody | undefined { const snap = toolSnapshot(commit, raw) if (!snap) { return undefined } return { type: "structured", snapshot: snap, } } const STRUCTURED_FALLBACK_LENGTH = 4_096 function structuredFallback(value: ToolDict): RunEntryBody | undefined { if (Object.keys(value).length === 0) return const content = JSON.stringify(value, null, 2) if (!content) return const suffix = "\n... [truncated]" return { type: "code", content: content.length <= STRUCTURED_FALLBACK_LENGTH ? content : content.slice(0, STRUCTURED_FALLBACK_LENGTH - suffix.length) + suffix, filetype: "json", } } function shellOutput(command: string, raw: string): string | undefined { const body = stripAnsi(raw).replace(/^\n+/, "").replace(/\n+$/, "") if (!body) { return undefined } if (!command) { return body } return `\n${body}` } export function toolEntryBody( commit: StreamCommit, raw: string, options?: { shellOutput?: boolean }, ): RunEntryBody | undefined { if (commit.shell) { if (commit.phase === "start") { return textBody(`$ ${commit.shell.command}`) } if (commit.phase === "progress") { return textBody(shellOutput(commit.shell.command, raw) ?? "") } if (commit.toolState === "error") { const ctx = toolFrame(commit, raw) return textBody(toolScroll("final", ctx)) } return undefined } const ctx = toolFrame(commit, raw) const view = toolView(ctx.name) if (ctx.name === "shell" && commit.phase === "progress" && options?.shellOutput === false) return undefined if (ctx.name === "subagent") { if (commit.phase === "start") { return undefined } if (commit.phase === "final" && ctx.status === "completed") { const result = taskResult(ctx.output) if (result) { return markdownBody(result) } } } if (commit.phase === "progress" && !view.output) { return undefined } if (commit.phase === "final") { if (ctx.status === "error") { return textBody(toolScroll("final", ctx)) } if (!view.final) { return undefined } if (ctx.status && ctx.status !== "completed") { return textBody(ctx.raw.trim()) } if (toolStructuredFinal(commit)) { return structuredBody(commit, raw) ?? textBody(toolScroll("final", ctx)) } if (!rule(ctx.name) && !ctx.output.trim()) { return structuredFallback(ctx.meta) ?? textBody(toolScroll("final", ctx)) } } return textBody(toolScroll(commit.phase, ctx)) } export function toolFiletype(input?: string): string | undefined { if (!input) { return undefined } const ext = path.extname(input) const lang = LANGUAGE_EXTENSIONS[ext] if (["typescriptreact", "javascriptreact", "javascript"].includes(lang)) { return "typescript" } return lang }