diff --git a/web/workspace/src/lib/workspace/console/model.test.ts b/web/workspace/src/lib/workspace/console/model.test.ts index 39732416..92c2688f 100644 --- a/web/workspace/src/lib/workspace/console/model.test.ts +++ b/web/workspace/src/lib/workspace/console/model.test.ts @@ -165,6 +165,108 @@ Deno.test("segment rotation retains a live error beside the real SegmentStart hi ); }); +Deno.test("reload snapshot projects provenance-annotated history entries", () => { + const metadata = { + entry_id: "history-entry-1", + origin: { + kind: "model_output", + worker: { + workspace_id: "workspace-secret", + runtime_id: "runtime-secret", + worker_id: "worker-secret", + }, + }, + }; + const annotated = (item: unknown) => ({ item, metadata }); + const projection = projectConsole([{ + eventId: "annotated-reload", + event: snapshotEvent("/repo", [ + { + kind: "annotated_segment_start", + ts: 1, + session_id: "session-1", + system_prompt: null, + config: {}, + history: [annotated({ + kind: "message", + role: "assistant", + content: [{ kind: "text", text: "older committed reply" }], + })], + }, + { + kind: "annotated_user_input", + ts: 2, + segments: [{ kind: "text", content: "latest user message" }], + history: [annotated({ + kind: "message", + role: "user", + content: [{ kind: "text", text: "latest user message" }], + })], + }, + { + kind: "annotated_assistant_item", + ts: 3, + entry: annotated({ + kind: "message", + role: "assistant", + content: [{ kind: "text", text: "latest committed reply" }], + }), + }, + { + kind: "annotated_assistant_item", + ts: 4, + entry: annotated({ + kind: "tool_call", + call_id: "annotated-call", + name: "Read", + arguments: '{"file_path":"/repo/a.md"}', + }), + }, + { + kind: "annotated_tool_result", + ts: 5, + entry: annotated({ + kind: "tool_result", + call_id: "annotated-call", + summary: "Read 1 line from /repo/a.md", + content: "1→content", + is_error: false, + }), + }, + { + kind: "annotated_system_item", + ts: 6, + entry: annotated({ + kind: "notification", + message: "Worker completed", + }), + }, + ]), + }]); + + assertEquals( + projection.lines.map((line) => + `${line.kind}:${line.toolCallLabel ?? line.body}` + ), + [ + "assistant:older committed reply", + "user:latest user message", + "assistant:latest committed reply", + "tool:Read(1 file)", + "system:Worker completed", + ], + ); + const visible = JSON.stringify(projection.lines); + assert( + !visible.includes("workspace-secret"), + "history metadata must not enter Console rows", + ); + assert( + !visible.includes("runtime-secret"), + "history origin must remain non-visible metadata", + ); +}); + Deno.test("workerConsoleHref encodes runtime and worker target authority", () => { assert( workerConsoleHref({ diff --git a/web/workspace/src/lib/workspace/console/model.ts b/web/workspace/src/lib/workspace/console/model.ts index d9e56953..86d4ce8b 100644 --- a/web/workspace/src/lib/workspace/console/model.ts +++ b/web/workspace/src/lib/workspace/console/model.ts @@ -1960,24 +1960,34 @@ function applyLogEntry( applyLoggedItem(projection, `${eventId}-history-${index}`, item) ); break; - case "user_input": - projection.lines.push( - line( - eventId, - "user", - "User", - segmentsToText(arrayField(entry, "segments") as Segment[]), - ), + case "annotated_segment_start": + arrayField(entry, "history").forEach((historyEntry, index) => + applyLoggedHistoryEntry( + projection, + `${eventId}-history-${index}`, + historyEntry, + ) ); break; + case "user_input": + case "annotated_user_input": + applyLoggedUserInput(projection, eventId, entry); + break; case "system_item": projection.lines.push(systemItemLine(eventId, entry["item"])); applyTaskSystemItem(projection, entry["item"]); break; + case "annotated_system_item": + applyLoggedSystemEntry(projection, eventId, entry["entry"]); + break; case "assistant_item": case "tool_result": applyLoggedItem(projection, eventId, entry["item"]); break; + case "annotated_assistant_item": + case "annotated_tool_result": + applyLoggedHistoryEntry(projection, eventId, entry["entry"]); + break; case "run_errored": projection.lines.push( line( @@ -2056,6 +2066,52 @@ function compactMessageForState( } } +function applyLoggedUserInput( + projection: ConsoleProjection, + eventId: string, + entry: Record, +): void { + let body = segmentsToText(arrayField(entry, "segments") as Segment[]); + if (!body && stringField(entry, "kind") === "annotated_user_input") { + body = loggedUserText(arrayField(entry, "history")); + } + projection.lines.push(line(eventId, "user", "User", body)); +} + +function loggedUserText(history: unknown[]): string { + for (const historyEntry of history) { + if (!isRecord(historyEntry) || !isRecord(historyEntry["item"])) continue; + const item = historyEntry["item"]; + if ( + stringField(item, "kind") !== "message" || + stringField(item, "role") !== "user" + ) { + continue; + } + return loggedContentText(arrayField(item, "content")); + } + return ""; +} + +function applyLoggedHistoryEntry( + projection: ConsoleProjection, + eventId: string, + historyEntry: unknown, +): void { + if (!isRecord(historyEntry)) return; + applyLoggedItem(projection, eventId, historyEntry["item"]); +} + +function applyLoggedSystemEntry( + projection: ConsoleProjection, + eventId: string, + historyEntry: unknown, +): void { + if (!isRecord(historyEntry)) return; + projection.lines.push(systemItemLine(eventId, historyEntry["item"])); + applyTaskSystemItem(projection, historyEntry["item"]); +} + function applyLoggedItem( projection: ConsoleProjection, eventId: string,