workspace: remove observation cursor surface

This commit is contained in:
2026-07-11 09:31:49 +09:00
parent 54eaa44681
commit 10762a3c8b
9 changed files with 172 additions and 377 deletions
@@ -56,28 +56,28 @@ Deno.test("segmentsToText preserves protocol segment semantics", () => {
Deno.test("projectConsole projects visible protocol rows", () => {
const projection = projectConsole([
{
cursor: "10",
eventId: "10",
event: {
event: "user_message",
data: { segments: [{ kind: "text", content: "input" }] },
} satisfies Event,
},
{
cursor: "11",
eventId: "11",
event: {
event: "text_delta",
data: { text: "stream" },
} satisfies Event,
},
{
cursor: "12",
eventId: "12",
event: {
event: "thinking_done",
data: { text: "reasoning" },
} satisfies Event,
},
{
cursor: "13",
eventId: "13",
event: {
event: "tool_result",
data: {
@@ -89,14 +89,14 @@ Deno.test("projectConsole projects visible protocol rows", () => {
} satisfies Event,
},
{
cursor: "14",
eventId: "14",
event: {
event: "usage",
data: { input_tokens: 12, output_tokens: 5 },
} satisfies Event,
},
{
cursor: "15",
eventId: "15",
event: {
event: "error",
data: { code: "invalid_request", message: "bad frame" },
@@ -141,35 +141,35 @@ Deno.test("projectConsole projects visible protocol rows", () => {
Deno.test("projectConsole groups tool call lifecycle into one Call block", () => {
const projection = projectConsole([
{
cursor: "40",
eventId: "40",
event: {
event: "tool_call_start",
data: { id: "call-1", name: "Bash" },
} satisfies Event,
},
{
cursor: "41",
eventId: "41",
event: {
event: "tool_call_args_delta",
data: { id: "call-1", json: '{"command":"pw' },
} satisfies Event,
},
{
cursor: "42",
eventId: "42",
event: {
event: "tool_call_args_delta",
data: { id: "call-1", json: 'd"}' },
} satisfies Event,
},
{
cursor: "43",
eventId: "43",
event: {
event: "tool_call_done",
data: { id: "call-1", name: "Bash", arguments: '{"command":"pwd"}' },
} satisfies Event,
},
{
cursor: "44",
eventId: "44",
event: {
event: "tool_result",
data: {
@@ -206,14 +206,14 @@ Deno.test("projectConsole groups tool call lifecycle into one Call block", () =>
Deno.test("projectConsole keeps streaming tool call updates in the same Call block", () => {
const projection = projectConsole([
{
cursor: "45",
eventId: "45",
event: {
event: "tool_call_start",
data: { id: "call-2", name: "Read" },
} satisfies Event,
},
{
cursor: "46",
eventId: "46",
event: {
event: "tool_call_args_delta",
data: { id: "call-2", json: '{"file_path":"/tmp/a.md"}' },
@@ -235,7 +235,7 @@ Deno.test("projectConsole keeps streaming tool call updates in the same Call blo
Deno.test("projectConsole aggregates Read calls without showing file content", () => {
const projection = projectConsole([
{
cursor: "50",
eventId: "50",
event: {
event: "tool_call_done",
data: {
@@ -246,7 +246,7 @@ Deno.test("projectConsole aggregates Read calls without showing file content", (
} satisfies Event,
},
{
cursor: "51",
eventId: "51",
event: {
event: "tool_result",
data: {
@@ -258,7 +258,7 @@ Deno.test("projectConsole aggregates Read calls without showing file content", (
} satisfies Event,
},
{
cursor: "52",
eventId: "52",
event: {
event: "tool_call_done",
data: {
@@ -269,7 +269,7 @@ Deno.test("projectConsole aggregates Read calls without showing file content", (
} satisfies Event,
},
{
cursor: "53",
eventId: "53",
event: {
event: "tool_result",
data: {
@@ -307,7 +307,7 @@ Deno.test("projectConsole aggregates Read calls without showing file content", (
Deno.test("projectConsole renders Edit calls with structured diff lines", () => {
const projection = projectConsole([
{
cursor: "60",
eventId: "60",
event: {
event: "tool_call_done",
data: {
@@ -322,7 +322,7 @@ Deno.test("projectConsole renders Edit calls with structured diff lines", () =>
} satisfies Event,
},
{
cursor: "61",
eventId: "61",
event: {
event: "tool_result",
data: {
@@ -350,7 +350,7 @@ Deno.test("projectConsole renders Edit calls with structured diff lines", () =>
Deno.test("projectConsole preserves in-progress assistant protocol stream", () => {
const projection = projectConsole([
{
cursor: "13",
eventId: "13",
event: { event: "text_delta", data: { text: "new" } } satisfies Event,
},
]);
@@ -367,26 +367,26 @@ Deno.test("projectConsole preserves in-progress assistant protocol stream", () =
Deno.test("projectConsole keeps protocol lifecycle events out of the console surface", () => {
const projection = projectConsole([
{
cursor: "30",
eventId: "30",
event: { event: "status", data: { status: "running" } } satisfies Event,
},
{
cursor: "31",
eventId: "31",
event: { event: "llm_call_end", data: { llm_call: 0 } } satisfies Event,
},
{
cursor: "32",
eventId: "32",
event: {
event: "turn_end",
data: { turn: 0, result: "finished" },
} satisfies Event,
},
{
cursor: "33",
eventId: "33",
event: { event: "run_end", data: { result: "finished" } } satisfies Event,
},
{
cursor: "34",
eventId: "34",
event: {
event: "system_item",
data: { item: { kind: "note", content: "internal" } },
@@ -401,7 +401,7 @@ Deno.test("projectConsole keeps protocol lifecycle events out of the console sur
Deno.test("projectConsole uses snapshot for state without rendering it as console output", () => {
const projection = projectConsole([
{
cursor: "20",
eventId: "20",
event: {
event: "snapshot",
data: {
@@ -49,7 +49,7 @@ export type ConsoleLine = {
body: string;
detail?: string;
diff?: ConsoleDiffLine[];
cursor?: string | null;
eventId?: string | null;
source: "event";
streaming?: boolean;
error?: boolean;
@@ -60,7 +60,7 @@ export type ConsoleProjection = {
lines: ConsoleLine[];
status: string | null;
usage: string | null;
lastCursor: string | null;
lastEventId: string | null;
};
export type WorkerTarget = {
@@ -93,7 +93,7 @@ export function workerConsolePath(
);
}
export type ConsoleEventInput = { cursor: string; event: ProtocolEvent };
export type ConsoleEventInput = { eventId: string; event: ProtocolEvent };
export function projectConsole(
events: ConsoleEventInput[] = [],
@@ -102,7 +102,7 @@ export function projectConsole(
lines: [],
status: null,
usage: null,
lastCursor: null,
lastEventId: null,
});
return {
...projection,
@@ -112,13 +112,13 @@ export function projectConsole(
export function applyProtocolEvent(
projection: ConsoleProjection,
envelope: { cursor: string; event: ProtocolEvent },
envelope: { eventId: string; event: ProtocolEvent },
): ConsoleProjection {
const next: ConsoleProjection = {
lines: [...projection.lines],
status: projection.status,
usage: projection.usage,
lastCursor: envelope.cursor,
lastEventId: envelope.eventId,
};
const event = envelope.event;
@@ -126,7 +126,7 @@ export function applyProtocolEvent(
case "user_message":
next.lines.push(
line(
envelope.cursor,
envelope.eventId,
"user",
"User",
segmentsToText(event.data.segments),
@@ -139,7 +139,7 @@ export function applyProtocolEvent(
case "text_delta":
appendStreaming(
next,
envelope.cursor,
envelope.eventId,
"assistant",
"assistant streaming",
event.data.text,
@@ -149,20 +149,20 @@ export function applyProtocolEvent(
finalizeStreaming(
next,
"assistant",
envelope.cursor,
envelope.eventId,
"assistant",
event.data.text,
);
break;
case "thinking_start":
next.lines.push(
line(envelope.cursor, "thinking", "Thinking...", "", undefined, true),
line(envelope.eventId, "thinking", "Thinking...", "", undefined, true),
);
break;
case "thinking_delta":
appendStreaming(
next,
envelope.cursor,
envelope.eventId,
"thinking",
"Thinking...",
event.data.text,
@@ -172,22 +172,22 @@ export function applyProtocolEvent(
finalizeStreaming(
next,
"thinking",
envelope.cursor,
envelope.eventId,
"Thought",
event.data.text,
);
break;
case "tool_call_start":
upsertToolCall(next, envelope.cursor, event.data.id, {
upsertToolCall(next, envelope.eventId, event.data.id, {
name: event.data.name,
state: "pending",
});
break;
case "tool_call_args_delta":
appendToolArgs(next, envelope.cursor, event.data.id, event.data.json);
appendToolArgs(next, envelope.eventId, event.data.id, event.data.json);
break;
case "tool_call_done":
upsertToolCall(next, envelope.cursor, event.data.id, {
upsertToolCall(next, envelope.eventId, event.data.id, {
name: event.data.name,
arguments: event.data.arguments,
argsStream: event.data.arguments,
@@ -195,7 +195,7 @@ export function applyProtocolEvent(
});
break;
case "tool_result":
attachToolResult(next, envelope.cursor, event.data.id, {
attachToolResult(next, envelope.eventId, event.data.id, {
summary: event.data.summary,
output: event.data.output,
isError: event.data.is_error,
@@ -207,7 +207,7 @@ export function applyProtocolEvent(
case "error":
next.lines.push(
line(
envelope.cursor,
envelope.eventId,
"error",
`error · ${event.data.code}`,
event.data.message,
@@ -220,7 +220,7 @@ export function applyProtocolEvent(
case "snapshot":
next.status = event.data.status;
for (const block of event.data.in_flight?.blocks ?? []) {
next.lines.push(inFlightLine(envelope.cursor, block));
next.lines.push(inFlightLine(envelope.eventId, block));
}
break;
case "status":
@@ -251,7 +251,7 @@ export function applyProtocolEvent(
case "compact_failed":
next.lines.push(
line(
envelope.cursor,
envelope.eventId,
"error",
"compact failed",
event.data.error,
@@ -292,7 +292,7 @@ export function segmentsToText(segments: Segment[]): string {
}
function line(
cursor: string,
eventId: string,
kind: ConsoleLineKind,
title: string,
body: string,
@@ -301,12 +301,12 @@ function line(
error = false,
): ConsoleLine {
return {
id: `event-${cursor}-${kind}-${slugify(title)}-${body.length}`,
id: `event-${eventId}-${kind}-${slugify(title)}-${body.length}`,
kind,
title,
body,
detail,
cursor,
eventId,
source: "event",
streaming,
error,
@@ -315,7 +315,7 @@ function line(
function appendStreaming(
projection: ConsoleProjection,
cursor: string,
eventId: string,
kind: "assistant" | "thinking",
title: string,
delta: string,
@@ -325,16 +325,16 @@ function appendStreaming(
);
if (existing) {
existing.body += delta;
existing.cursor = cursor;
existing.eventId = eventId;
return;
}
projection.lines.push(line(cursor, kind, title, delta, undefined, true));
projection.lines.push(line(eventId, kind, title, delta, undefined, true));
}
function finalizeStreaming(
projection: ConsoleProjection,
kind: "assistant" | "thinking",
cursor: string,
eventId: string,
title: string,
body: string,
): void {
@@ -345,21 +345,21 @@ function finalizeStreaming(
existing.body = body || existing.body;
existing.streaming = false;
existing.title = title;
existing.cursor = cursor;
existing.eventId = eventId;
return;
}
projection.lines.push(line(cursor, kind, title, body));
projection.lines.push(line(eventId, kind, title, body));
}
function upsertToolCall(
projection: ConsoleProjection,
cursor: string,
eventId: string,
id: string,
update: Partial<Omit<ToolCallView, "id">>,
): ConsoleLine {
let existing = findToolCallLine(projection, id);
if (!existing) {
existing = toolLine(cursor, {
existing = toolLine(eventId, {
id,
name: update.name ?? "Tool",
argsStream: update.argsStream ?? "",
@@ -371,7 +371,7 @@ function upsertToolCall(
});
projection.lines.push(existing);
} else {
existing.cursor = cursor;
existing.eventId = eventId;
existing.toolCall = {
...existing.toolCall!,
...update,
@@ -385,11 +385,11 @@ function upsertToolCall(
function appendToolArgs(
projection: ConsoleProjection,
cursor: string,
eventId: string,
id: string,
delta: string,
): void {
const existing = upsertToolCall(projection, cursor, id, {
const existing = upsertToolCall(projection, eventId, id, {
state: "streaming_args",
});
existing.toolCall!.argsStream += delta;
@@ -398,13 +398,13 @@ function appendToolArgs(
function attachToolResult(
projection: ConsoleProjection,
cursor: string,
eventId: string,
id: string,
result: Pick<ToolCallView, "summary" | "output" | "isError">,
): void {
const existing = findToolCallLine(projection, id);
if (!existing) {
const fallback = toolLine(cursor, {
const fallback = toolLine(eventId, {
id,
name: "Tool",
argsStream: "",
@@ -418,7 +418,7 @@ function attachToolResult(
projection.lines.push(fallback);
return;
}
existing.cursor = cursor;
existing.eventId = eventId;
existing.toolCall = {
...existing.toolCall!,
...result,
@@ -436,14 +436,14 @@ function findToolCallLine(
);
}
function toolLine(cursor: string, toolCall: ToolCallView): ConsoleLine {
function toolLine(eventId: string, toolCall: ToolCallView): ConsoleLine {
const item: ConsoleLine = {
id: `tool-call-${toolCall.id}`,
kind: "tool",
title: `Call · ${toolCall.name}`,
body: "",
detail: undefined,
cursor,
eventId,
source: "event",
streaming: true,
error: false,
@@ -531,7 +531,7 @@ function readAggregateLine(group: ConsoleLine[]): ConsoleLine {
title: "Call · Read",
body,
detail: calls.map(readDetail).join("\n\n"),
cursor: group.at(-1)?.cursor,
eventId: group.at(-1)?.eventId,
source: "event",
streaming: inProgress,
error: hasError,
@@ -760,11 +760,11 @@ function usageText(
} · cache ${data.cache_read_input_tokens ?? "unknown"}`;
}
function inFlightLine(cursor: string, block: InFlightBlock): ConsoleLine {
function inFlightLine(eventId: string, block: InFlightBlock): ConsoleLine {
switch (block.kind) {
case "text":
return line(
cursor,
eventId,
"in_flight",
"in-flight assistant text",
block.text,
@@ -773,7 +773,7 @@ function inFlightLine(cursor: string, block: InFlightBlock): ConsoleLine {
);
case "thinking":
return line(
cursor,
eventId,
"in_flight",
"in-flight thinking",
block.text,
@@ -781,7 +781,7 @@ function inFlightLine(cursor: string, block: InFlightBlock): ConsoleLine {
!block.finished,
);
case "tool_call":
return toolLine(cursor, {
return toolLine(eventId, {
id: block.id,
name: block.name,
argsStream: block.args,
@@ -253,7 +253,6 @@ export type WorkerInputResult = {
};
export type ClientWorkerEventWsEnvelope = {
cursor: string;
event_id: string;
runtime_id: string;
worker_id: string;
@@ -42,7 +42,7 @@
let consoleBodyElement: HTMLElement | null = null;
let autoFollowConsole = $state(true);
const CONSOLE_BOTTOM_THRESHOLD_PX = 48;
let observedEvents = $state<Array<{ cursor: string; event: ClientWorkerEventWsFrame & { kind: 'event' } }>>([]);
let observedEvents = $state<Array<{ eventId: string; event: ClientWorkerEventWsFrame & { kind: 'event' } }>>([]);
let seenObservationEventIds = new Set<string>();
let nextReloadToken = 0;
let reloadToken = $state(0);
@@ -55,7 +55,7 @@
const consoleTarget = $derived({ runtimeId, workerId });
const projection = $derived(
projectConsole(observedEvents.map((item) => ({ cursor: item.cursor, event: item.event.envelope.payload })))
projectConsole(observedEvents.map((item) => ({ eventId: item.eventId, event: item.event.envelope.payload })))
);
const lines = $derived(projection.lines);
const diagnostics = $derived(mergeDiagnostics(worker?.diagnostics ?? [], streamDiagnostics));
@@ -185,7 +185,7 @@
observedEvents = [
...observedEvents,
{
cursor: frame.envelope.cursor,
eventId: frame.envelope.event_id,
event: frame
}
].slice(-500);