fix(web): gate compaction status by worker state

This commit is contained in:
2026-09-18 00:14:34 +09:00
parent 01bddf2177
commit 6d9c9cdb48
4 changed files with 94 additions and 13 deletions
@@ -5,6 +5,7 @@ import {
formatRunElapsed,
formatRunElapsedCompact,
formatRunTokens,
visibleCompactionProgress,
} from "./run-status.ts";
function assertEquals(actual: unknown, expected: unknown): void {
@@ -98,3 +99,42 @@ Deno.test("run status formatting matches the compact TUI shape", () => {
assertEquals(formatRunTokens(3_000), "3.0k");
assertEquals(formatRunTokens(999), "999");
});
Deno.test("visible compaction requires a matching busy worker state", () => {
const manual = {
phase: "preparing",
started_at_ms: 1_700_000_000_000,
trigger: "manual",
} as const;
const automatic = {
phase: "summarizing",
started_at_ms: 1_700_000_000_000,
trigger: "request_threshold",
} as const;
const maintenance = {
last_command_id: 7,
state: {
kind: "busy",
state: { kind: "maintenance", state: "compacting" },
},
} as const;
const run = {
last_command_id: 7,
state: {
kind: "busy",
state: { kind: "run", state: "running" },
},
} as const;
const idle = {
last_command_id: 7,
state: { kind: "idle" },
} as const;
assertEquals(visibleCompactionProgress(manual, maintenance), manual);
assertEquals(visibleCompactionProgress(automatic, run), automatic);
assertEquals(visibleCompactionProgress(manual, run), null);
assertEquals(visibleCompactionProgress(automatic, maintenance), null);
assertEquals(visibleCompactionProgress(manual, idle), null);
assertEquals(visibleCompactionProgress(manual, null), null);
assertEquals(visibleCompactionProgress(null, maintenance), null);
});
@@ -1,4 +1,8 @@
import type { Event as ProtocolEvent } from "$lib/generated/protocol";
import type {
Event as ProtocolEvent,
InFlightCompaction,
WorkerStateSnapshot,
} from "$lib/generated/protocol";
export type RunActivityStats = {
startedAtMs: number | null;
@@ -71,3 +75,20 @@ export function formatRunTokens(tokens: number): string {
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
return String(tokens);
}
export function visibleCompactionProgress(
compaction: InFlightCompaction | null,
workerState: WorkerStateSnapshot | null,
): InFlightCompaction | null {
if (!compaction || workerState?.state.kind !== "busy") {
return null;
}
const busyState = workerState.state.state;
const triggerMatches = compaction.trigger === "manual"
? busyState.kind === "maintenance" &&
busyState.state === "compacting"
: busyState.kind === "run";
return triggerMatches ? compaction : null;
}
@@ -1116,3 +1116,28 @@ Deno.test("Web Console uses Notify while running and exposes durable pending con
"ordinary text must remain Submit instead of being implicitly converted to Notify",
);
});
Deno.test("Web Console gates compaction progress before rendering run status", async () => {
const consolePage = await Deno.readTextFile(
new URL(
"./../../../routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte",
import.meta.url,
),
);
const runStatus = await Deno.readTextFile(
new URL("./WorkerRunStatus.svelte", import.meta.url),
);
assert(
consolePage.includes("visibleCompactionProgress(") &&
consolePage.includes("consoleProjection.compaction") &&
consolePage.includes("consoleProjection.workerState") &&
consolePage.includes("compaction={compactionProgress}"),
"Console page should pass only state-consistent compaction progress to the status component",
);
assert(
runStatus.includes("compaction?.started_at_ms ?? startedAtMs") &&
runStatus.includes("Compacting · {compaction.phase}"),
"run status should render the compaction phase and derive elapsed time from the authoritative start timestamp",
);
});
@@ -21,6 +21,7 @@
type ComposerCompletionToken,
} from "$lib/workspace/console/composer-completion";
import WorkerRunStatus from "$lib/workspace/console/WorkerRunStatus.svelte";
import { visibleCompactionProgress } from "$lib/workspace/console/run-status";
import { resolveWorkerControlShortcut } from "$lib/workspace/console/worker-control-shortcuts";
import {
consoleWorkerViews,
@@ -248,18 +249,12 @@
liveWorkerState ?? (worker?.state === "stopped" ? "stopped" : "loading"),
);
const workerRunning = $derived(workerState === "running");
const compactionProgress = $derived.by(() => {
const state = consoleProjection.workerState?.state;
if (!state || typeof state !== "object" || !("busy" in state)) return null;
const progress = consoleProjection.compaction;
if (!progress) return null;
const busy = state.busy;
if (!busy || typeof busy !== "object") return null;
const valid = progress.trigger === "manual"
? "maintenance" in busy && busy.maintenance === "compacting"
: "run" in busy;
return valid ? progress : null;
});
const compactionProgress = $derived(
visibleCompactionProgress(
consoleProjection.compaction,
consoleProjection.workerState,
),
);
const workerPaused = $derived(workerState === "paused");
const composerEditable = $derived(protocolState === "open" && !sending);
const draftHasText = $derived(draft.content.trim().length > 0);