compaction: live progress signal + user-message cap

COMPACTING event drives a 'Compacting context…' transient in the GUI.
Cap the compacted block's user-message list at 40 with an honest omitted count.
This commit is contained in:
Rohit C Prasad
2026-07-30 06:24:39 -07:00
parent b303823934
commit f9f51c97c6
9 changed files with 144 additions and 25 deletions
+5
View File
@@ -64,9 +64,14 @@ test("the compacted divider renders mid-session and the transcript stays intact"
await box.fill("compact the context");
await box.press("Enter");
// The transient signal shows while the summarizer runs, then yields to the divider.
await expect(page.getByText("Compacting context…").first()).toBeVisible({
timeout: 10_000,
});
await expect(
page.getByText("Context compacted — earlier turns were summarized").first(),
).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("Compacting context…")).toHaveCount(0);
await expect(
page.getByText("Still on it — continuing where I left off.").first(),
).toBeVisible();
+9 -5
View File
@@ -681,12 +681,16 @@ export async function mockApi(page: import("@playwright/test").Page) {
}, 120);
return;
}
// Auto-compaction (OPE-27): the server compacts mid-run and emits the marker,
// then the turn continues normally — the divider must render inline.
// Auto-compaction (OPE-27): the server signals `compacting` (the transient
// spinner label), summarizes for a beat, then emits the marker and the turn
// continues normally — the divider must render inline.
if (/compact the context/i.test(msg.text)) {
send("compacted", { text: "Context compacted — earlier turns were summarized" });
send("assistant_message", { text: "Still on it — continuing where I left off." });
send("turn_done");
send("compacting", {});
setTimeout(() => {
send("compacted", { text: "Context compacted — earlier turns were summarized" });
send("assistant_message", { text: "Still on it — continuing where I left off." });
send("turn_done");
}, 400);
return;
}
// A turn that dies on a provider error; the follow-up {type:"retry"} recovers.
+16 -2
View File
@@ -172,6 +172,10 @@ export function App() {
const [mode, setMode] = useState("interactive");
const [connected, setConnected] = useState(false);
const [running, setRunning] = useState(false);
// Transient "Compacting context…" indicator (OPE-27): set by the `compacting` event,
// cleared by whatever the engine emits next — the summarizer call is otherwise a
// multi-second silent stall mid-turn.
const [compacting, setCompacting] = useState(false);
const [items, setItems] = useState<Item[]>([]);
const [streaming, setStreamingState] = useState("");
// Ref mirror of `streaming`: the WS handler closure is built once per socket and can't read
@@ -576,6 +580,9 @@ export function App() {
},
]);
};
// Any engine event after `compacting` means the summarizer finished (compacted /
// silent no-op / failure prompt) — the transient must never outlive it.
if (ev.type !== "compacting") setCompacting(false);
switch (ev.type) {
case "ready":
setConnected(true);
@@ -709,6 +716,9 @@ export function App() {
if (d.model) setModel(d.model);
setItems((p) => [...p, { kind: "notice", tone: "info", text: d.text || "Model switched" }]);
break;
case "compacting":
setCompacting(true);
break;
case "compacted":
// Auto-compaction marker (OPE-27): outbound-only — the transcript stays intact,
// this divider just shows where the model's memory was summarized.
@@ -1519,7 +1529,11 @@ export function App() {
<ThinkingBlock text={reasoningStream} live />
</div>
)}
{/* Compaction runs between provider turns (nothing streams during it), so
the transient takes over the waiting slot with a specific label. */}
{running && compacting && <WaitingForAgent label="Compacting context…" />}
{running &&
!compacting &&
!reasoningStream &&
(!streaming || streamMode(streaming, items, running) === "hold") &&
!lastItemIsAssistant(items) && <WaitingForAgent />}
@@ -1685,12 +1699,12 @@ function lastItemIsAssistant(items: Item[]): boolean {
return false;
}
function WaitingForAgent() {
function WaitingForAgent({ label }: { label?: string }) {
return (
<div className="waiting-transcript">
<div className="waiting-row" aria-live="polite">
<span className="waiting-spinner" />
<span>Waiting for agent...</span>
<span>{label || "Waiting for agent..."}</span>
</div>
</div>
);
+1
View File
@@ -18,6 +18,7 @@ export type EventType =
| "input_rejected"
| "interrupted"
| "model_changed"
| "compacting"
| "compacted"
| "turn_done";