diff --git a/packages/tui/src/mini/scrollback.writer.tsx b/packages/tui/src/mini/scrollback.writer.tsx index 907abe0b74c..74d39d7705d 100644 --- a/packages/tui/src/mini/scrollback.writer.tsx +++ b/packages/tui/src/mini/scrollback.writer.tsx @@ -123,6 +123,15 @@ export function RunEntryContent(props: { return ( + + + + + {props.commit.text} + + + + {text()!.content} diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index 5ba7870d5c1..09462654e31 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -146,6 +146,7 @@ type State = { pending: Map admitted: Set stepModel: RunInput["model"] + activeCompaction?: string } const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }) @@ -368,6 +369,40 @@ function skillCommit(messageID: string, name: string): StreamCommit { } } +function compactionCommit(messageID: string): StreamCommit { + return { + kind: "system", + source: "system", + messageID, + partID: "compaction:header", + text: "Compaction", + phase: "start", + compaction: true, + } +} + +function compactionSummary(messageID: string, text: string, phase: "progress" | "final"): StreamCommit { + return { + kind: "assistant", + source: "assistant", + messageID, + partID: "compaction:summary", + text, + phase, + } +} + +function compactionError(messageID: string, text: string): StreamCommit { + return { + kind: "error", + source: "system", + messageID, + partID: "compaction:error", + text, + phase: "start", + } +} + async function resolveSelectedModel( input: StreamInput, sdk: OpenCodeClient, @@ -642,6 +677,28 @@ export async function createSessionTransport(input: StreamInput): Promise update.previous.length + ? [compactionSummary(messageID, event.data.text.slice(update.previous.length), "progress")] + : []), + compactionSummary(messageID, "", "final"), + ]) + return + } + if (event.type === "session.compaction.failed") { + if (!state.activeCompaction) return + const messageID = state.activeCompaction + state.activeCompaction = undefined + if (event.data.error.type === "aborted") { + write([compactionSummary(messageID, "", "final")]) + return + } + write([ + compactionSummary(messageID, "", "final"), + compactionError(messageID, event.data.error.message), + ]) + return + } if (event.type === "session.shell.started") { state.shellCommands.set(event.data.shell.id, event.data.shell.command) const wait = state.shellWait @@ -1427,6 +1526,7 @@ export async function createSessionTransport(input: StreamInput): Promise { }) describe("V2 mini transport", () => { + test("renders projected compactions as labeled transcript boundaries", async () => { + const events = feed() + events.push(connected()) + const ui = footer() + const transport = await createSessionTransport({ + sdk: sdk({ + streams: [events], + messages: { + ses_1: [compaction("completed", "## Transport")], + }, + }), + sessionID: "ses_1", + thinking: false, + replay: true, + footer: ui.api, + }) + + expect(ui.commits).toMatchObject([ + { text: "Compaction", compaction: true, messageID: "msg_compaction" }, + { text: "## Transport", phase: "progress", messageID: "msg_compaction" }, + { text: "", phase: "final", messageID: "msg_compaction" }, + ]) + await transport.close() + }) + + test("shows an active compaction boundary before live summary output without history replay", async () => { + const events = feed() + events.push(connected()) + const ui = footer() + const transport = await createSessionTransport({ + sdk: sdk({ + streams: [events], + active: () => ({ ses_1: { type: "running" } }), + messages: { + ses_1: [compaction("running", "")], + }, + }), + sessionID: "ses_1", + thinking: false, + footer: ui.api, + }) + + events.push({ + id: "evt_compaction_delta", + created: 2, + type: "session.compaction.delta", + data: { sessionID: "ses_1", text: "Transport" }, + }) + events.push({ + id: "evt_compaction_ended", + created: 3, + type: "session.compaction.ended", + durable: durable("ses_1", 3), + data: { sessionID: "ses_1", reason: "auto", text: "Transport", recent: "" }, + }) + + while (!ui.commits.some((commit) => commit.phase === "final")) await Bun.sleep(0) + expect(ui.commits).toMatchObject([ + { text: "Compaction", compaction: true, messageID: "msg_compaction" }, + { text: "Transport", phase: "progress", messageID: "msg_compaction" }, + { text: "", phase: "final", messageID: "msg_compaction" }, + ]) + await transport.close() + }) + test("reports session title changes", async () => { const events = feed() events.push(connected())