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)
155 lines
4.9 KiB
TypeScript
155 lines
4.9 KiB
TypeScript
import pc from "picocolors"
|
|
import type {
|
|
RunContext,
|
|
EventPayload,
|
|
MessageUpdatedProps,
|
|
MessagePartUpdatedProps,
|
|
MessagePartDeltaProps,
|
|
ToolExecuteProps,
|
|
ToolResultProps,
|
|
SessionErrorProps,
|
|
} from "./types"
|
|
|
|
export function serializeError(error: unknown): string {
|
|
if (!error) return "Unknown error"
|
|
|
|
if (error instanceof Error) {
|
|
const parts = [error.message]
|
|
if (error.cause) {
|
|
parts.push(`Cause: ${serializeError(error.cause)}`)
|
|
}
|
|
return parts.join(" | ")
|
|
}
|
|
|
|
if (typeof error === "string") {
|
|
return error
|
|
}
|
|
|
|
if (typeof error === "object") {
|
|
const obj = error as Record<string, unknown>
|
|
|
|
const messagePaths = [
|
|
obj.message,
|
|
obj.error,
|
|
(obj.data as Record<string, unknown>)?.message,
|
|
(obj.data as Record<string, unknown>)?.error,
|
|
(obj.error as Record<string, unknown>)?.message,
|
|
]
|
|
|
|
for (const msg of messagePaths) {
|
|
if (typeof msg === "string" && msg.length > 0) {
|
|
return msg
|
|
}
|
|
}
|
|
|
|
try {
|
|
const json = JSON.stringify(error, null, 2)
|
|
if (json !== "{}") {
|
|
return json
|
|
}
|
|
} catch (_) {
|
|
void _
|
|
}
|
|
}
|
|
|
|
return String(error)
|
|
}
|
|
|
|
function getSessionTag(ctx: RunContext, payload: EventPayload): string {
|
|
const props = payload.properties as Record<string, unknown> | undefined
|
|
const info = props?.info as Record<string, unknown> | undefined
|
|
const part = props?.part as Record<string, unknown> | undefined
|
|
const sessionID =
|
|
props?.sessionID ?? props?.sessionId ??
|
|
info?.sessionID ?? info?.sessionId ??
|
|
part?.sessionID ?? part?.sessionId
|
|
const isMainSession = sessionID === ctx.sessionID
|
|
if (isMainSession) return pc.green("[MAIN]")
|
|
if (sessionID) return pc.yellow(`[${String(sessionID).slice(0, 8)}]`)
|
|
return pc.dim("[system]")
|
|
}
|
|
|
|
export function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
|
|
const sessionTag = getSessionTag(ctx, payload)
|
|
const props = payload.properties as Record<string, unknown> | undefined
|
|
|
|
switch (payload.type) {
|
|
case "session.idle":
|
|
case "session.status": {
|
|
const status = (props?.status as { type?: string })?.type ?? "idle"
|
|
console.error(pc.dim(`${sessionTag} ${payload.type}: ${status}`))
|
|
break
|
|
}
|
|
|
|
case "message.part.updated": {
|
|
const partProps = props as MessagePartUpdatedProps | undefined
|
|
const part = partProps?.part
|
|
if (part?.type === "tool") {
|
|
const status = part.state?.status ?? "unknown"
|
|
console.error(pc.dim(`${sessionTag} message.part (tool): ${part.tool ?? part.name ?? "?"} [${status}]`))
|
|
} else if (part?.type === "text" && part.text) {
|
|
const preview = part.text.slice(0, 80).replace(/\n/g, "\\n")
|
|
console.error(pc.dim(`${sessionTag} message.part (text): "${preview}${part.text.length > 80 ? "..." : ""}"`))
|
|
}
|
|
break
|
|
}
|
|
|
|
case "message.part.delta": {
|
|
const deltaProps = props as MessagePartDeltaProps | undefined
|
|
const field = deltaProps?.field ?? "unknown"
|
|
const delta = deltaProps?.delta ?? ""
|
|
const preview = delta.slice(0, 80).replace(/\n/g, "\\n")
|
|
console.error(pc.dim(`${sessionTag} message.part.delta (${field}): "${preview}${delta.length > 80 ? "..." : ""}"`))
|
|
break
|
|
}
|
|
|
|
case "message.updated": {
|
|
const msgProps = props as MessageUpdatedProps | undefined
|
|
const role = msgProps?.info?.role ?? "unknown"
|
|
const model = msgProps?.info?.modelID
|
|
const agent = msgProps?.info?.agent
|
|
const details = [role, agent, model].filter(Boolean).join(", ")
|
|
console.error(pc.dim(`${sessionTag} message.updated (${details})`))
|
|
break
|
|
}
|
|
|
|
case "tool.execute": {
|
|
const toolProps = props as ToolExecuteProps | undefined
|
|
const toolName = toolProps?.name ?? "unknown"
|
|
const input = toolProps?.input ?? {}
|
|
let inputStr: string
|
|
try {
|
|
inputStr = JSON.stringify(input)
|
|
} catch {
|
|
try {
|
|
inputStr = String(input)
|
|
} catch {
|
|
inputStr = "[unserializable]"
|
|
}
|
|
}
|
|
const inputPreview = inputStr.slice(0, 150)
|
|
console.error(pc.cyan(`${sessionTag} TOOL.EXECUTE: ${pc.bold(toolName)}`))
|
|
console.error(pc.dim(` input: ${inputPreview}${inputStr.length >= 150 ? "..." : ""}`))
|
|
break
|
|
}
|
|
|
|
case "tool.result": {
|
|
const resultProps = props as ToolResultProps | undefined
|
|
const output = resultProps?.output ?? ""
|
|
const preview = output.slice(0, 200).replace(/\n/g, "\\n")
|
|
console.error(pc.green(`${sessionTag} TOOL.RESULT: "${preview}${output.length > 200 ? "..." : ""}"`))
|
|
break
|
|
}
|
|
|
|
case "session.error": {
|
|
const errorProps = props as SessionErrorProps | undefined
|
|
const errorMsg = serializeError(errorProps?.error)
|
|
console.error(pc.red(`${sessionTag} SESSION.ERROR: ${errorMsg}`))
|
|
break
|
|
}
|
|
|
|
default:
|
|
console.error(pc.dim(`${sessionTag} ${payload.type}`))
|
|
}
|
|
}
|