fix: render compaction on worker status

This commit is contained in:
2026-09-16 04:25:37 +09:00
parent 1d06d4b08a
commit df22526a0d
5 changed files with 27 additions and 41 deletions
+10 -18
View File
@@ -279,6 +279,7 @@ pub struct App {
run_error_messages: Vec<String>, run_error_messages: Vec<String>,
/// Current compaction identity/revision used to fence snapshot/live updates. /// Current compaction identity/revision used to fence snapshot/live updates.
active_compaction: Option<(String, u64)>, active_compaction: Option<(String, u64)>,
compaction_progress: Option<protocol::InFlightCompaction>,
/// Presentation-only Internal Worker projections keyed by session identity. /// Presentation-only Internal Worker projections keyed by session identity.
/// They are rendered in separate selectable views and never mixed into `blocks`. /// They are rendered in separate selectable views and never mixed into `blocks`.
pub internal_workers: Vec<InternalWorkerView>, pub internal_workers: Vec<InternalWorkerView>,
@@ -367,6 +368,7 @@ impl App {
shutdown_confirm: None, shutdown_confirm: None,
blocks: Vec::new(), blocks: Vec::new(),
active_compaction: None, active_compaction: None,
compaction_progress: None,
run_error_messages: Vec::new(), run_error_messages: Vec::new(),
internal_workers: Vec::new(), internal_workers: Vec::new(),
selected_internal_worker_session_id: None, selected_internal_worker_session_id: None,
@@ -1401,17 +1403,7 @@ impl App {
} }
} }
Event::CompactionProgress { compaction } => { Event::CompactionProgress { compaction } => {
if compaction.is_some() { self.compaction_progress = compaction;
if self.last_streaming_compact_mut().is_none() {
self.blocks.push(Block::Compact(CompactEvent::Streaming {
started_at: Instant::now(),
}));
}
} else if let Some(Block::Compact(CompactEvent::Streaming { .. })) =
self.blocks.last()
{
self.blocks.pop();
}
} }
Event::CompactStart { lifecycle } => { Event::CompactStart { lifecycle } => {
let should_apply = match &self.active_compaction { let should_apply = match &self.active_compaction {
@@ -1702,11 +1694,7 @@ impl App {
} }
} }
self.active_compaction = None; self.active_compaction = None;
if compaction.is_some() && self.last_streaming_compact_mut().is_none() { self.compaction_progress = compaction;
self.blocks.push(Block::Compact(CompactEvent::Streaming {
started_at: Instant::now(),
}));
}
} }
fn append_assistant_text(&mut self, text: &str) { fn append_assistant_text(&mut self, text: &str) {
@@ -4322,11 +4310,15 @@ mod completion_flow_tests {
}), }),
..InFlightSnapshot::default() ..InFlightSnapshot::default()
}); });
assert_eq!(compact_block_count(&app), 1); assert_eq!(compact_block_count(&app), 0);
assert_eq!(
app.compaction_progress.as_ref().map(|item| item.phase),
Some(protocol::CompactionPhase::Summarizing)
);
app.handle_worker_event(Event::CompactionProgress { compaction: None }); app.handle_worker_event(Event::CompactionProgress { compaction: None });
assert_eq!(compact_block_count(&app), 0); assert!(app.compaction_progress.is_none());
} }
#[test] #[test]
@@ -7,9 +7,11 @@
requests: number; requests: number;
uploadTokens: number; uploadTokens: number;
outputTokens: number; outputTokens: number;
compaction?: { phase: string } | null;
}; };
let { startedAtMs, requests, uploadTokens, outputTokens }: Props = $props(); let { startedAtMs, requests, uploadTokens, outputTokens, compaction = null }: Props =
$props();
let nowMs = $state(Date.now()); let nowMs = $state(Date.now());
$effect(() => { $effect(() => {
@@ -27,6 +29,10 @@
<div class="worker-run-status" role="status" aria-live="off"> <div class="worker-run-status" role="status" aria-live="off">
<Spinner /> <Spinner />
{#if compaction}
<span>Compacting · {compaction.phase}</span>
<span aria-hidden="true">|</span>
{/if}
<span>{elapsed}</span> <span>{elapsed}</span>
<span aria-hidden="true"></span> <span aria-hidden="true"></span>
<span>{requests} {requestLabel}</span> <span>{requests} {requestLabel}</span>
@@ -1092,11 +1092,9 @@ Deno.test("snapshot restores running compaction without staged content", () => {
const projection = projectConsole([{ eventId: "snapshot", event: snapshot }]); const projection = projectConsole([{ eventId: "snapshot", event: snapshot }]);
assertEquals(projection.lines.length, 1); assertEquals(projection.lines.length, 0);
assertEquals(projection.lines[0].id, "compaction-runtime"); assertEquals(projection.compaction?.phase, "summarizing");
assertEquals(projection.lines[0].streaming, true); assertEquals(projection.compaction?.trigger, "manual");
assertEquals(projection.lines[0].body, "compacting · summarizing");
assertEquals(projection.lines[0].body.includes("staged"), false);
}); });
Deno.test("compaction service activity stays nested in one lifecycle item", () => { Deno.test("compaction service activity stays nested in one lifecycle item", () => {
@@ -173,6 +173,7 @@ export type ConsoleProjection = {
taskNextId: number; taskNextId: number;
status: string | null; status: string | null;
workerState: WorkerStateSnapshot | null; workerState: WorkerStateSnapshot | null;
compaction: InFlightCompaction | null;
usage: string | null; usage: string | null;
runActivity: RunActivityStats; runActivity: RunActivityStats;
cwd: string | null; cwd: string | null;
@@ -246,6 +247,7 @@ export function emptyConsoleProjection(): ConsoleProjection {
taskNextId: 1, taskNextId: 1,
status: null, status: null,
workerState: null, workerState: null,
compaction: null,
usage: null, usage: null,
runActivity: emptyRunActivityStats(), runActivity: emptyRunActivityStats(),
cwd: null, cwd: null,
@@ -722,23 +724,7 @@ function applyInFlightCompaction(
projection: ConsoleProjection, projection: ConsoleProjection,
progress: InFlightCompaction | null, progress: InFlightCompaction | null,
): ConsoleProjection { ): ConsoleProjection {
const id = "compaction-runtime"; return { ...projection, compaction: progress };
const lines = projection.lines.filter((line) => line.id !== id);
if (!progress) return { ...projection, lines };
return {
...projection,
lines: [
...lines,
{
id,
kind: "status",
title: "Compaction",
body: `compacting · ${progress.phase.replaceAll("_", " ")}`,
source: "event",
streaming: true,
},
],
};
} }
function applyCompactionLifecycle( function applyCompactionLifecycle(
@@ -863,6 +849,7 @@ export function applyProtocolEvent(
taskNextId: projection.taskNextId, taskNextId: projection.taskNextId,
status: projection.status, status: projection.status,
workerState: projection.workerState, workerState: projection.workerState,
compaction: projection.compaction,
usage: projection.usage, usage: projection.usage,
runActivity: applyRunActivityEvent( runActivity: applyRunActivityEvent(
projection.runActivity, projection.runActivity,
@@ -1003,6 +990,7 @@ export function applyProtocolEvent(
event.data.in_flight.compaction, event.data.in_flight.compaction,
); );
next.lines = withCompaction.lines; next.lines = withCompaction.lines;
next.compaction = withCompaction.compaction;
} }
for (const line of next.lines) { for (const line of next.lines) {
const compaction = line.compaction; const compaction = line.compaction;
@@ -2013,6 +2001,7 @@ function snapshotProjectionFromSession(
taskNextId: 1, taskNextId: 1,
status: null, status: null,
workerState: null, workerState: null,
compaction: null,
usage: null, usage: null,
runActivity: emptyRunActivityStats(), runActivity: emptyRunActivityStats(),
cwd, cwd,
@@ -1907,6 +1907,7 @@
requests={consoleProjection.runActivity.requests} requests={consoleProjection.runActivity.requests}
uploadTokens={consoleProjection.runActivity.uploadTokens} uploadTokens={consoleProjection.runActivity.uploadTokens}
outputTokens={consoleProjection.runActivity.outputTokens} outputTokens={consoleProjection.runActivity.outputTokens}
compaction={consoleProjection.compaction}
/> />
{/if} {/if}