Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/tui/src/mini/scrollback.writer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ export function RunEntryContent(props: {

return (
<Switch fallback={null}>
<Match when={props.commit.compaction}>
<box width="100%" flexDirection="row" alignItems="center">
<box border={["top"]} borderColor={theme().block.muted} flexGrow={1} />
<box paddingLeft={1} paddingRight={1}>
<text fg={theme().block.muted}>{props.commit.text}</text>
</box>
<box border={["top"]} borderColor={theme().block.muted} flexGrow={1} />
</box>
</Match>
<Match when={text()}>
<text width="100%" wrapMode="word" fg={style().fg} attributes={style().attrs}>
{text()!.content}
Expand Down
100 changes: 100 additions & 0 deletions packages/tui/src/mini/stream-v2.transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ type State = {
pending: Map<string, FooterQueuedPrompt>
admitted: Set<string>
stepModel: RunInput["model"]
activeCompaction?: string
}

const money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" })
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -642,6 +677,28 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
if (completed && state.shellWait?.callID === message.shellID) state.shellWait.resolve()
return
}
if (message.type === "compaction") {
const visible = state.messageIDs.has(message.id)
state.messageIDs.add(message.id)
if (message.status === "running") state.activeCompaction = message.id
if (message.status !== "running" && state.activeCompaction === message.id) state.activeCompaction = undefined
if (visible) return
if (message.status === "failed") {
if (render && message.error.type !== "aborted")
write([compactionCommit(message.id), compactionError(message.id, message.error.message)])
return
}
const fragment = { messageID: message.id, partID: "compaction:summary" }
const show = render || message.status === "running"
state.fragments.project(fragment, message.summary, show)
if (!show) return
write([
compactionCommit(message.id),
...(message.summary ? [compactionSummary(message.id, message.summary, "progress")] : []),
...(message.status === "completed" ? [compactionSummary(message.id, "", "final")] : []),
])
return
}
if (message.type !== "assistant") return
state.messageIDs.add(message.id)
let textOrdinal = 0
Expand Down Expand Up @@ -892,6 +949,48 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
write([skillCommit(messageID, event.data.name)])
return
}
if (event.type === "session.compaction.started") {
const messageID = event.data.inputID ?? messageIDFromEvent(event.id)
state.activeCompaction = messageID
if (state.messageIDs.has(messageID)) return
state.messageIDs.add(messageID)
write([compactionCommit(messageID)], { phase: "running", status: "compacting session" })
return
}
if (event.type === "session.compaction.delta") {
if (!state.activeCompaction) return
const fragment = { messageID: state.activeCompaction, partID: "compaction:summary" }
if (!state.fragments.delta(fragment, event.data.text)) return
write([compactionSummary(state.activeCompaction, event.data.text, "progress")])
return
}
if (event.type === "session.compaction.ended") {
if (!state.activeCompaction) return
const messageID = state.activeCompaction
state.activeCompaction = undefined
const update = state.fragments.end({ messageID, partID: "compaction:summary" }, event.data.text)
write([
...(event.data.text.length > 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
Expand Down Expand Up @@ -1427,6 +1526,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
state.skillMessages.clear()
state.shellCommands.clear()
state.shellStarted.clear()
state.activeCompaction = undefined
state.shellEnded.clear()
state.errors.clear()
await hydrate(attempt, { render: true, reuseVisibleWait: false })
Expand Down
1 change: 1 addition & 0 deletions packages/tui/src/mini/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ export type StreamCommit = {
text: string
phase: StreamPhase
source: StreamSource
compaction?: true
summary?: TurnSummary
messageID?: string
partID?: string
Expand Down
78 changes: 78 additions & 0 deletions packages/tui/test/mini/stream-v2.transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ function footer() {

type SessionMessages = MessageListOutput["data"]

function compaction(status: "running" | "completed", summary: string): SessionMessages[number] {
const message = {
id: "msg_compaction",
type: "compaction" as const,
reason: "auto" as const,
summary,
recent: "",
time: { created: 1 },
}
if (status === "running") return { ...message, status }
return { ...message, status }
}

function form(id: string, sessionID: string, title = id): FormInfo {
return {
id,
Expand Down Expand Up @@ -200,6 +213,71 @@ afterEach(() => {
})

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())
Expand Down
Loading