From 6d9c9cdb48e027d0ef621576f4291a4c2460c4e3 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 18 Sep 2026 00:14:34 +0900 Subject: [PATCH 1/2] fix(web): gate compaction status by worker state --- .../lib/workspace/console/run-status.test.ts | 40 +++++++++++++++++++ .../src/lib/workspace/console/run-status.ts | 23 ++++++++++- .../console/worker-console.ui.test.ts | 25 ++++++++++++ .../workers/[workerRef]/console/+page.svelte | 19 ++++----- 4 files changed, 94 insertions(+), 13 deletions(-) diff --git a/web/workspace/src/lib/workspace/console/run-status.test.ts b/web/workspace/src/lib/workspace/console/run-status.test.ts index e0d990471..998009b61 100644 --- a/web/workspace/src/lib/workspace/console/run-status.test.ts +++ b/web/workspace/src/lib/workspace/console/run-status.test.ts @@ -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); +}); diff --git a/web/workspace/src/lib/workspace/console/run-status.ts b/web/workspace/src/lib/workspace/console/run-status.ts index 37495dc1c..326694e97 100644 --- a/web/workspace/src/lib/workspace/console/run-status.ts +++ b/web/workspace/src/lib/workspace/console/run-status.ts @@ -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; +} diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index 3b6629ae9..c39f231a9 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -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", + ); +}); diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte index 28f3cbd35..b9ba03797 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte @@ -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); From ecc0602f53a9a4fe0332baee704a27e934891b99 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 18 Sep 2026 00:26:40 +0900 Subject: [PATCH 2/2] test(web): cover compacting status presentation --- web/workspace/deno.json | 2 +- .../workspace/console/WorkerRunStatus.svelte | 33 +++++++++++++++---- .../lib/workspace/console/run-status.test.ts | 18 +++++++++- .../src/lib/workspace/console/run-status.ts | 15 +++++++++ .../console/worker-console.ui.test.ts | 17 +++++----- .../workers/[workerRef]/console/+page.svelte | 10 ++---- 6 files changed, 70 insertions(+), 25 deletions(-) diff --git a/web/workspace/deno.json b/web/workspace/deno.json index 58759ffb6..674b94556 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -6,7 +6,7 @@ "dev": "deno run -A npm:vite@7.2.7 dev", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787", "check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", - "test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts", + "test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts", "build": "deno run -A npm:vite@7.2.7 build", "preview": "deno run -A npm:vite@7.2.7 preview" }, diff --git a/web/workspace/src/lib/workspace/console/WorkerRunStatus.svelte b/web/workspace/src/lib/workspace/console/WorkerRunStatus.svelte index b057d9f9d..0d7e2ed63 100644 --- a/web/workspace/src/lib/workspace/console/WorkerRunStatus.svelte +++ b/web/workspace/src/lib/workspace/console/WorkerRunStatus.svelte @@ -1,7 +1,14 @@