From 9ae9b0a043c30342300f905928e4383444b935d0 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 1 Jul 2026 23:32:39 +0900 Subject: [PATCH] ui: filter worker console protocol noise --- .../src/lib/workspace-console/model.test.ts | 45 ++++- .../src/lib/workspace-console/model.ts | 168 +----------------- web/workspace/vite.config.ts | 1 + 3 files changed, 46 insertions(+), 168 deletions(-) diff --git a/web/workspace/src/lib/workspace-console/model.test.ts b/web/workspace/src/lib/workspace-console/model.test.ts index eeaec30b..a918643a 100644 --- a/web/workspace/src/lib/workspace-console/model.test.ts +++ b/web/workspace/src/lib/workspace-console/model.test.ts @@ -11,6 +11,14 @@ function assert(condition: unknown, message: string): asserts condition { } } +function assertEquals(actual: T, expected: T): void { + const actualJson = JSON.stringify(actual); + const expectedJson = JSON.stringify(expected); + if (actualJson !== expectedJson) { + throw new Error(`Expected ${expectedJson}, got ${actualJson}`); + } +} + Deno.test("workerConsoleHref encodes runtime and worker target authority", () => { assert( workerConsoleHref({ @@ -45,7 +53,7 @@ Deno.test("segmentsToText preserves protocol segment semantics", () => { ); }); -Deno.test("projectConsole projects initial console output and live protocol rows", () => { +Deno.test("projectConsole projects initial console output and live visible protocol rows", () => { const projection = projectConsole( [ { @@ -120,8 +128,8 @@ Deno.test("projectConsole projects initial console output and live protocol rows "tool event row expected", ); assert( - projection.lines.some((line) => line.kind === "usage"), - "usage event row expected", + !projection.lines.some((line) => line.kind === "usage"), + "usage should update the summary without rendering a console row", ); assert( projection.lines.some((line) => line.kind === "error" && line.error), @@ -133,6 +141,37 @@ Deno.test("projectConsole projects initial console output and live protocol rows ); }); +Deno.test("projectConsole keeps protocol lifecycle events out of the console surface", () => { + const projection = projectConsole([], [ + { + cursor: "30", + event: { event: "status", data: { status: "running" } } satisfies Event, + }, + { + cursor: "31", + event: { event: "llm_call_end", data: { llm_call: 0 } } satisfies Event, + }, + { + cursor: "32", + event: { event: "turn_end", data: { turn: 0, result: "finished" } } satisfies Event, + }, + { + cursor: "33", + event: { event: "run_end", data: { result: "finished" } } satisfies Event, + }, + { + cursor: "34", + event: { + event: "system_item", + data: { item: { kind: "note", content: "internal" } }, + } satisfies Event, + }, + ]); + + assertEquals(projection.lines, []); + assertEquals(projection.status, "running"); +}); + Deno.test("projectConsole uses snapshot for state without rendering it as console output", () => { const projection = projectConsole([], [ { diff --git a/web/workspace/src/lib/workspace-console/model.ts b/web/workspace/src/lib/workspace-console/model.ts index 26a45e11..d95d1feb 100644 --- a/web/workspace/src/lib/workspace-console/model.ts +++ b/web/workspace/src/lib/workspace-console/model.ts @@ -98,14 +98,7 @@ export function applyProtocolEvent( ); break; case "system_item": - next.lines.push( - line( - envelope.cursor, - "system", - "system item", - jsonPreview(event.data.item), - ), - ); + // System items are protocol/internal context, not console output. break; case "text_delta": appendStreaming( @@ -189,7 +182,6 @@ export function applyProtocolEvent( break; case "usage": next.usage = usageText(event.data); - next.lines.push(line(envelope.cursor, "usage", "usage", next.usage)); break; case "error": next.lines.push( @@ -212,179 +204,28 @@ export function applyProtocolEvent( break; case "status": next.status = event.data.status; - next.lines.push( - line(envelope.cursor, "status", "status", event.data.status), - ); break; case "invoke_start": - next.lines.push( - line(envelope.cursor, "status", "invoke start", event.data.kind), - ); - break; case "turn_start": - next.lines.push( - line( - envelope.cursor, - "status", - "turn start", - `turn ${event.data.turn}`, - ), - ); - break; case "turn_end": - next.lines.push( - line( - envelope.cursor, - "status", - "turn end", - `turn ${event.data.turn} · ${event.data.result}`, - ), - ); - break; case "llm_call_start": - next.lines.push( - line( - envelope.cursor, - "status", - "llm call start", - `call ${event.data.llm_call}`, - ), - ); - break; case "llm_call_end": - next.lines.push( - line( - envelope.cursor, - "status", - "llm call end", - `call ${event.data.llm_call}`, - ), - ); - break; case "llm_retry": - next.lines.push( - line( - envelope.cursor, - "status", - "llm retry", - `${event.data.error} · attempt ${event.data.failed_attempt}/${event.data.max_attempts}`, - ), - ); - break; case "llm_continuation": - next.lines.push( - line( - envelope.cursor, - "status", - "llm continuation", - `${event.data.reason} · attempt ${event.data.attempt}/${event.data.max_attempts}`, - ), - ); - break; case "run_end": - next.lines.push( - line(envelope.cursor, "status", "run end", event.data.result), - ); - break; case "alert": - next.lines.push( - line( - envelope.cursor, - "status", - `alert · ${event.data.level}`, - event.data.message, - ), - ); - break; case "memory_worker": - next.lines.push( - line(envelope.cursor, "status", "memory worker", event.data.message), - ); - break; case "segment_rotated": - next.lines.push( - line( - envelope.cursor, - "status", - "segment rotated", - jsonPreview(event.data.entry), - ), - ); - break; case "completions": - next.lines.push( - line( - envelope.cursor, - "status", - "completions", - `${event.data.kind} · ${event.data.entries.length} entries`, - ), - ); - break; case "rewind_targets": - next.lines.push( - line( - envelope.cursor, - "status", - "rewind targets", - `${event.data.targets.length} targets · head ${event.data.head_entries}`, - ), - ); - break; case "rewind_applied": - next.lines.push( - line( - envelope.cursor, - "status", - "rewind applied", - `${event.data.summary.discarded_entries} discarded · ${event.data.summary.truncated_to_entries} retained`, - ), - ); - break; case "workers_listed": - next.lines.push( - line( - envelope.cursor, - "status", - "workers listed", - jsonPreview(event.data.workers), - ), - ); - break; case "worker_restored": - next.lines.push( - line( - envelope.cursor, - "status", - "worker restored", - jsonPreview(event.data.result), - ), - ); - break; case "peer_registered": - next.lines.push( - line( - envelope.cursor, - "status", - "peer registered", - jsonPreview(event.data.result), - ), - ); - break; case "compact_start": - next.lines.push( - line(envelope.cursor, "status", "compact start", "compaction started"), - ); - break; case "compact_done": - next.lines.push( - line( - envelope.cursor, - "status", - "compact done", - event.data.new_segment_id, - ), - ); + // These are protocol/status/control events. TUI Console does not append + // them to the conversation surface; browser Console should not either. break; case "compact_failed": next.lines.push( @@ -401,9 +242,6 @@ export function applyProtocolEvent( break; case "shutdown": next.status = "shutdown"; - next.lines.push( - line(envelope.cursor, "status", "shutdown", "worker shut down"), - ); break; } diff --git a/web/workspace/vite.config.ts b/web/workspace/vite.config.ts index ebb3b63e..f23d61cf 100644 --- a/web/workspace/vite.config.ts +++ b/web/workspace/vite.config.ts @@ -4,6 +4,7 @@ import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], server: { + allowedHosts: ['develop.hareworks.net'], proxy: { '/api': { target: 'http://127.0.0.1:8787',