Navigate to snapshots from the transcript
This commit is contained in:
+94
-7
@@ -1,7 +1,8 @@
|
||||
import { get, post, type ByreEvent } from "./api.js";
|
||||
import { get, post, type ByreEvent, type Snapshot } 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"];
|
||||
|
||||
@@ -11,11 +12,23 @@ type Run = {
|
||||
first: HTMLElement;
|
||||
group: { el: HTMLDetailsElement; summary: HTMLElement; body: HTMLElement } | undefined;
|
||||
calls: Call[];
|
||||
snaps: number[];
|
||||
};
|
||||
|
||||
type Anchor = {
|
||||
seq: number;
|
||||
turnEnd: boolean;
|
||||
el: HTMLElement;
|
||||
snaps: number[];
|
||||
mark: HTMLElement | undefined;
|
||||
run: Run | undefined;
|
||||
};
|
||||
|
||||
type Ctx = {
|
||||
tools: Map<string, HTMLElement>;
|
||||
run: Run | undefined;
|
||||
anchors: Anchor[];
|
||||
unplaced: Snapshot[];
|
||||
root: string;
|
||||
lastMessage: HTMLElement | undefined;
|
||||
turnStart: HTMLElement | undefined;
|
||||
@@ -70,6 +83,8 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
return {
|
||||
tools: new Map(),
|
||||
run: undefined,
|
||||
anchors: [],
|
||||
unplaced: [],
|
||||
lastMessage: undefined,
|
||||
turnStart: undefined,
|
||||
root: task?.worktree_path ?? "",
|
||||
@@ -230,7 +245,7 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
const addToRun = (el: HTMLElement, call: Call | undefined) => {
|
||||
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;
|
||||
}
|
||||
@@ -246,7 +261,53 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
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: number, el: HTMLElement, turnEnd: boolean) => {
|
||||
const anchor: 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: Anchor, n: number) => {
|
||||
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: Snapshot) => {
|
||||
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: { el: HTMLElement; text: string; timer: number } | undefined;
|
||||
@@ -281,7 +342,7 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const foldTurn = (start: HTMLElement, end: HTMLElement) => {
|
||||
const foldTurn = (start: HTMLElement, end: HTMLElement): HTMLElement => {
|
||||
const summary = h("summary");
|
||||
summary.append(...end.childNodes);
|
||||
const body = h("div", { class: "folded-turn" });
|
||||
@@ -292,6 +353,7 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
body.append(node);
|
||||
node = next;
|
||||
}
|
||||
return summary;
|
||||
};
|
||||
|
||||
const appendEvents = (events: ByreEvent[]) => {
|
||||
@@ -316,8 +378,14 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
contextChanged = true;
|
||||
continue;
|
||||
}
|
||||
if (ev.kind === "snapshot.created") {
|
||||
placeSnapshot(p as unknown as Snapshot);
|
||||
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);
|
||||
@@ -330,7 +398,7 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -338,6 +406,9 @@ export function mountTranscript(root: HTMLElement): () => void {
|
||||
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();
|
||||
@@ -661,6 +732,22 @@ function renderToolCall(p: Record<string, unknown>, ctx: Ctx): HTMLElement {
|
||||
);
|
||||
}
|
||||
|
||||
function snapMark(ns: number[]): HTMLElement | null {
|
||||
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: Call[]): HTMLElement {
|
||||
const counts = new Map<string, { call: Call; n: number }>();
|
||||
for (const c of calls) {
|
||||
|
||||
Reference in New Issue
Block a user