tui: render mini compaction boundaries (#39103)

This commit is contained in:
Simon Klee 2026-07-27 14:19:03 +02:00 committed by GitHub
commit 9a55d125f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 188 additions and 0 deletions

View file

@ -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}

View file

@ -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" })
@ -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<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
@ -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
@ -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 })

View file

@ -418,6 +418,7 @@ export type StreamCommit = {
text: string
phase: StreamPhase
source: StreamSource
compaction?: true
summary?: TurnSummary
messageID?: string
partID?: string