Navigate to snapshots from the transcript

This commit is contained in:
Ian Gulliver
2026-10-15 00:08:32 -07:00
parent 75c76eff0a
commit c778741a4a
23 changed files with 763 additions and 202 deletions
+76 -5
View File
@@ -1,7 +1,8 @@
import { get, post } from "./api.js";
import { ago, errorText, h, replace, showAlert, showConfirm } from "./dom.js";
import { markdown } from "./markdown.js";
import { currentSession, findTask, onEach, state } from "./state.js";
import { openChanges } from "./panes.js";
import { currentSession, findTask, onEach, pinSnapshot, state } from "./state.js";
const models = ["claude-opus-5-5", "claude-sonnet-5-5", "claude-haiku-5-5", "claude-fable-5-1"];
export function mountTranscript(root) {
const head = h("header", { class: "task-header" });
@@ -47,6 +48,8 @@ export function mountTranscript(root) {
return {
tools: new Map(),
run: undefined,
anchors: [],
unplaced: [],
lastMessage: undefined,
turnStart: undefined,
root: task?.worktree_path ?? "",
@@ -191,7 +194,7 @@ export function mountTranscript(root) {
const addToRun = (el, call) => {
const run = ctx.run;
if (!run) {
ctx.run = { first: el, group: undefined, calls: call ? [call] : [] };
ctx.run = { first: el, group: undefined, calls: call ? [call] : [], snaps: [] };
put(el);
return;
}
@@ -207,7 +210,50 @@ export function mountTranscript(root) {
run.group = { el: group, summary, body };
}
run.group.body.append(el);
replace(run.group.summary, runSummary(run.calls));
replace(run.group.summary, runSummary(run.calls), snapMark(run.snaps));
};
const addAnchor = (seq, el, turnEnd) => {
const anchor = { seq, turnEnd, el, snaps: [], mark: undefined, run: turnEnd ? undefined : ctx.run };
ctx.anchors.push(anchor);
if (!turnEnd) {
return;
}
const waiting = ctx.unplaced;
ctx.unplaced = [];
for (const snap of waiting) {
placeSnapshot(snap);
}
};
const markAnchor = (anchor, n) => {
anchor.snaps.push(n);
anchor.mark?.remove();
anchor.mark = snapMark(anchor.snaps) ?? undefined;
if (anchor.mark) {
anchor.el.append(anchor.mark);
}
const run = anchor.run;
if (!run) {
return;
}
run.snaps.push(n);
if (run.group) {
replace(run.group.summary, runSummary(run.calls), snapMark(run.snaps));
}
};
const placeSnapshot = (snap) => {
const before = ctx.anchors.findLast((a) => a.seq <= snap.event_seq);
if (snap.trigger !== "turn_end" || before?.turnEnd) {
if (before) {
markAnchor(before, snap.n);
}
return;
}
const after = ctx.anchors.find((a) => a.turnEnd && a.seq > snap.event_seq);
if (after) {
markAnchor(after, snap.n);
return;
}
ctx.unplaced.push(snap);
};
let stream;
const dropStream = () => {
@@ -251,6 +297,7 @@ export function mountTranscript(root) {
body.append(node);
node = next;
}
return summary;
};
const appendEvents = (events) => {
const pinned = transcript.scrollHeight - transcript.scrollTop - transcript.clientHeight < 80;
@@ -274,8 +321,14 @@ export function mountTranscript(root) {
contextChanged = true;
continue;
}
if (ev.kind === "snapshot.created") {
placeSnapshot(p);
continue;
}
if (ev.kind === "agent.tool_call") {
addToRun(renderToolCall(p, ctx), { name: String(p["name"]), target: toolTarget(p["input"], ctx.root) });
const el = renderToolCall(p, ctx);
addToRun(el, { name: String(p["name"]), target: toolTarget(p["input"], ctx.root) });
addAnchor(ev.seq, el.querySelector("summary"), false);
continue;
}
const el = renderEvent(ev, ctx);
@@ -288,7 +341,7 @@ export function mountTranscript(root) {
}
ctx.run = undefined;
if (ev.kind === "turn.ended" && p["handoff"] === true && ctx.turnStart?.parentNode === transcript) {
foldTurn(ctx.turnStart, el);
addAnchor(ev.seq, foldTurn(ctx.turnStart, el), true);
ctx.turnStart = undefined;
continue;
}
@@ -296,6 +349,9 @@ export function mountTranscript(root) {
if (ev.kind === "user.message") {
ctx.turnStart = el;
}
if (ev.kind === "turn.ended" || ev.kind === "inbox.created") {
addAnchor(ev.seq, el, ev.kind === "turn.ended");
}
}
if (contextChanged) {
renderHead();
@@ -580,6 +636,21 @@ function renderToolCall(p, ctx) {
ctx.tools.set(String(p["id"]), result);
return h("details", { class: "tool" }, h("summary", {}, h("span", { class: "tool-name" }, String(p["name"])), " ", h("span", { class: "tool-brief" }, toolBrief(p["input"], ctx.root))), h("pre", { class: "tool-input" }, JSON.stringify(p["input"], null, 2)), result);
}
function snapMark(ns) {
if (ns.length === 0) {
return null;
}
const lo = Math.min(...ns);
const hi = Math.max(...ns);
const b = h("button", { type: "button", class: "snap-mark", title: `Show snapshot ${hi} in the Changes tab` }, lo === hi ? `#${hi}` : `#${lo}–#${hi}`);
b.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
pinSnapshot(hi);
openChanges(b);
};
return b;
}
function runSummary(calls) {
const counts = new Map();
for (const c of calls) {