mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-08 00:33:24 +00:00
tui: render mini compaction boundaries (#39103)
This commit is contained in:
parent
766aaf448d
commit
9a55d125f6
4 changed files with 188 additions and 0 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -418,6 +418,7 @@ export type StreamCommit = {
|
|||
text: string
|
||||
phase: StreamPhase
|
||||
source: StreamSource
|
||||
compaction?: true
|
||||
summary?: TurnSummary
|
||||
messageID?: string
|
||||
partID?: string
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue