feat(cli-run): add streaming delta, think block rendering, and rich tool headers

Adds message.part.delta event handling for real-time streaming output,
reasoning/think block display with in-place updates, per-agent profile
colors, padded text output, and semantic tool headers with icons.

🤖 Generated with [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2026-02-17 23:33:06 +09:00
parent 4bb8fa4a7f
commit eaf315a8d7
10 changed files with 945 additions and 73 deletions
+143 -37
View File
@@ -1,38 +1,144 @@
import pc from "picocolors"
const SINGLE_VALUE_FIELDS = ["command", "filePath"] as const
const MULTI_VALUE_FIELDS = [
"description",
"pattern",
"query",
"url",
"category",
"subagent_type",
"lang",
"run_in_background",
] as const
export function formatToolInputPreview(input: Record<string, unknown>): string {
for (const key of SINGLE_VALUE_FIELDS) {
if (!input[key]) continue
const maxLen = key === "command" ? 80 : 120
return ` ${pc.dim(String(input[key]).slice(0, maxLen))}`
}
const parts: string[] = []
let totalLen = 0
for (const key of MULTI_VALUE_FIELDS) {
const val = input[key]
if (val === undefined || val === null) continue
const str = String(val)
const truncated = str.length > 50 ? str.slice(0, 47) + "..." : str
const entry = `${key}=${truncated}`
if (totalLen + entry.length > 120) break
parts.push(entry)
totalLen += entry.length + 1
}
return parts.length > 0 ? ` ${pc.dim(parts.join(" "))}` : ""
export interface ToolHeader {
icon: string
title: string
description?: string
}
export function formatToolHeader(toolName: string, input: Record<string, unknown>): ToolHeader {
if (toolName === "glob") {
const pattern = str(input.pattern)
const root = str(input.path)
return {
icon: "✱",
title: pattern ? `Glob "${pattern}"` : "Glob",
description: root ? `in ${root}` : undefined,
}
}
if (toolName === "grep") {
const pattern = str(input.pattern)
const root = str(input.path)
return {
icon: "✱",
title: pattern ? `Grep "${pattern}"` : "Grep",
description: root ? `in ${root}` : undefined,
}
}
if (toolName === "list") {
const path = str(input.path)
return {
icon: "→",
title: path ? `List ${path}` : "List",
}
}
if (toolName === "read") {
const filePath = str(input.filePath)
return {
icon: "→",
title: filePath ? `Read ${filePath}` : "Read",
description: formatKeyValues(input, ["filePath"]),
}
}
if (toolName === "write") {
const filePath = str(input.filePath)
return {
icon: "←",
title: filePath ? `Write ${filePath}` : "Write",
}
}
if (toolName === "edit") {
const filePath = str(input.filePath)
return {
icon: "←",
title: filePath ? `Edit ${filePath}` : "Edit",
description: formatKeyValues(input, ["filePath", "oldString", "newString"]),
}
}
if (toolName === "webfetch") {
const url = str(input.url)
return {
icon: "%",
title: url ? `WebFetch ${url}` : "WebFetch",
description: formatKeyValues(input, ["url"]),
}
}
if (toolName === "websearch_web_search_exa") {
const query = str(input.query)
return {
icon: "◈",
title: query ? `Web Search "${query}"` : "Web Search",
}
}
if (toolName === "grep_app_searchGitHub") {
const query = str(input.query)
return {
icon: "◇",
title: query ? `Code Search "${query}"` : "Code Search",
}
}
if (toolName === "task") {
const desc = str(input.description)
const subagent = str(input.subagent_type)
return {
icon: "#",
title: desc || (subagent ? `${subagent} Task` : "Task"),
description: subagent ? `agent=${subagent}` : undefined,
}
}
if (toolName === "bash") {
const command = str(input.command)
return {
icon: "$",
title: command || "bash",
description: formatKeyValues(input, ["command"]),
}
}
if (toolName === "skill") {
const name = str(input.name)
return {
icon: "→",
title: name ? `Skill "${name}"` : "Skill",
}
}
if (toolName === "todowrite") {
return {
icon: "#",
title: "Todos",
}
}
return {
icon: "⚙",
title: toolName,
description: formatKeyValues(input, []),
}
}
function formatKeyValues(input: Record<string, unknown>, exclude: string[]): string | undefined {
const entries = Object.entries(input).filter(([key, value]) => {
if (exclude.includes(key)) return false
return typeof value === "string" || typeof value === "number" || typeof value === "boolean"
})
if (!entries.length) return undefined
return entries
.map(([key, value]) => `${key}=${String(value)}`)
.join(" ")
}
function str(value: unknown): string | undefined {
if (typeof value !== "string") return undefined
const trimmed = value.trim()
return trimmed.length ? trimmed : undefined
}