workspace: remove observation cursor surface
This commit is contained in:
@@ -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;
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user