Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359// 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/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 { isRecord } from "../util/record"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<string, unknown>
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"
type ToolRule = { view: ToolView run: (props: ToolProps) => ToolInline scroll?: Partial<Record<ToolPhase, (props: ToolProps) => string>> snap?: (props: ToolProps) => ToolSnapshot | undefined}
type ToolRegistry = Record<ToolName, ToolRule>
type AnyToolRule = ToolRule
function dict(v: unknown): ToolDict { return isRecord(v) ? { ...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<T>(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<PatchFile>(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 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<PatchFile>(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<PatchFile>(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<unknown[]>(p.frame.meta.answers) const items = list<{ question?: string }>(p.frame.input.questions).map((item, i) => { const answer = list<string>(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, { home: true }) : "" const dir = formatted === "." ? "" : formatted if (cmd && !dir) { return `$ ${cmd}` }
if (!cmd) { return dir ? `${dir}$` : "" }
return `${dir}$ ${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, { home: true }) : "" 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<PatchFile>(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(/<task_result>\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, }, },} 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}