// Thin bridge between transport output and the footer API. // // Transports produce immutable StreamCommit[] rows and typed mutable-footer // updates. This module forwards both to the footer API, adding trace writes // along the way. It also // defaults status updates to phase "running" if the caller didn't set a // phase -- a convenience so transport code doesn't have to repeat that. import type { FooterApi, FooterEvent, FooterPatch, FooterSubagentState, StreamCommit } from "./types" type Trace = { write(type: string, data?: unknown): void } type OutputInput = { footer: FooterApi trace?: Trace } type StreamOutput = { commits: StreamCommit[] updates?: Extract[] } // Default to "running" phase when a status string arrives without an explicit phase. function patch(next: FooterPatch): FooterPatch { if (typeof next.status === "string" && next.phase === undefined) { return { phase: "running", ...next, } } return next } function summarize(value: unknown): unknown { if (typeof value === "string") { if (value.length <= 160) { return value } return { type: "string", length: value.length, preview: `${value.slice(0, 160)}…`, } } if (Array.isArray(value)) { return { type: "array", length: value.length, } } if (!value || typeof value !== "object") { return value } return { type: "object", keys: Object.keys(value), } } function traceCommit(commit: StreamCommit) { return { ...commit, text: summarize(commit.text), textLength: commit.text.length, part: commit.part ? { id: commit.part.id, tool: commit.part.name, state: { status: commit.part.state.status, input: summarize(commit.part.state.input), metadata: "metadata" in commit.part.state ? summarize(commit.part.state.metadata) : undefined, content: "content" in commit.part.state ? summarize(commit.part.state.content) : undefined, error: "error" in commit.part.state ? summarize(commit.part.state.error) : undefined, }, time: commit.part.time, } : undefined, } } function traceSubagentState(state: FooterSubagentState) { return { tabs: state.tabs, details: Object.fromEntries( Object.entries(state.details).map(([sessionID, detail]) => [ sessionID, { sessionID, commits: detail.commits.map(traceCommit), }, ]), ), permissions: state.permissions.map((item) => ({ id: item.id, sessionID: item.sessionID, action: item.action, resources: item.resources, source: item.source, tool: item.tool ? { id: item.tool.id, name: item.tool.name, status: item.tool.state.status, input: summarize(item.tool.state.input), } : undefined, metadata: item.metadata ? { keys: Object.keys(item.metadata), } : undefined, })), forms: state.forms.map((item) => ({ id: item.id, sessionID: item.sessionID, title: item.title, fields: item.fields.map((field) => ({ key: field.key, type: field.type })), location: item.location, })), } } // Forwards transport output to the footer: commits go to scrollback, patches update the status bar. export function writeSessionOutput(input: OutputInput, out: StreamOutput): void { for (const commit of out.commits) { input.trace?.write("ui.commit", commit) input.footer.append(commit) } for (const update of out.updates ?? []) { if (update.type === "stream.patch") { const next = { ...update, patch: patch(update.patch) } input.trace?.write("ui.patch", next.patch) input.footer.event(next) continue } if (update.type === "stream.subagent") { input.trace?.write("ui.subagent", traceSubagentState(update.state)) input.footer.event(update) continue } input.trace?.write("ui.patch", { view: update.view }) input.footer.event(update) } }