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
+6 -2
View File
@@ -138,7 +138,7 @@ Answers reach the agent as one message, listing each answer under the question o
`GET /api/tasks/{taskId}/changes` snapshots the task's worktree as if everything were staged: it copies the worktree's index to a temporary file, runs `git add --all` against that copy (`GIT_INDEX_FILE`), and `git write-tree`s it, so committed, staged, unstaged and untracked work is included, `.gitignore` is respected, and the worktree's real index is never touched. Starting from the real index rather than `read-tree HEAD` keeps git's stat cache, so unchanged files aren't rehashed. The tree is compared with `git merge-base HEAD <default branch>`, or with `since` when given, using rename detection, giving per-file status and line counts. Work already merged into the default branch therefore drops out. The Changes view toggles between **Since main** and **This turn**, which passes the latest `user.message` event's `head` as `since`; the summary above the inbox always shows since main. `GET /api/tasks/{taskId}/diff` returns one file's patch between a `base` and `tree` from that response. Each request writes the changed blobs and a tree into the repo's object store as loose objects.
The Changes view lists the files with their counts, opens a file's diff on click (open files stay open across refreshes), and refreshes when it opens, when the selected task changes, and 1.5s after the selected task's last tool result or turn end. The Inbox view shows just its summary line (file count, +/− totals, base) for the selected task at the top.
The Changes view lists the files with their counts, opens a file's diff on click (open files stay open across refreshes), and refreshes when it opens, when the selected task changes, and 1.5s after the selected task's last tool result or turn end. It can also be pinned to a snapshot (see "Snapshots"). The Inbox view shows just its summary line (file count, +/− totals, base) for the selected task at the top.
## Snapshots
@@ -153,7 +153,11 @@ Triggers (`trigger`):
A snapshot whose tree equals the task's latest snapshot's tree is skipped. Snapshots run one at a time per task: a trigger that arrives while one is running replaces any already waiting one, and the waiting one runs when the current one finishes, so a burst of tool calls costs at most two. Failures are logged and dropped.
Merging or discarding a task stops new snapshots for it, waits for one in progress, and deletes its `refs/byre/tasks/<task-id>/` refs; the rows stay. `GET /api/tasks/{taskId}/snapshots` lists a task's rows, oldest first. `git for-each-ref refs/byre/` in the project's repo lists the live refs. The UI doesn't show snapshots yet.
Merging or discarding a task stops new snapshots for it, waits for one in progress, and deletes its `refs/byre/tasks/<task-id>/` refs; the rows stay. `GET /api/tasks/{taskId}/snapshots` lists a task's rows, oldest first. `git for-each-ref refs/byre/` in the project's repo lists the live refs.
`GET /api/tasks/{taskId}/changes?snapshot=<n>` reports a snapshot instead of the live worktree: its `tree` is the snapshot's tree, and `base` is the merge-base of the snapshot's commit and the default branch, or with `step=true` the previous snapshot's tree (the snapshot commit's parent for snapshot 1). It reads the stored shas, so it works only while the task is active and its refs keep the objects; `since` with `snapshot` is a 400. `GET /api/tasks/{taskId}/diff` takes that `base` and `tree` as usual.
The transcript is the timeline. The UI learns a task's snapshots from the `snapshot.created` events in its transcript (`snapshots` in `web/src/state.ts`). Each snapshot is marked `#n` at the right of the transcript row it belongs to: for `tool_call`, `timer` and `ask_human`, the last tool call, `ask_human` note or turn end whose `seq` is at most the snapshot's `event_seq` (the PostToolUse hook fires before the tool's result is recorded, so that is the call that made it); for `turn_end`, that turn's end line, which may be the turn after `event_seq` since the snapshot and `turn.ended` race. A row with several snapshots shows `#a–#b`, and a collapsed group of tool calls shows the range of all its calls' snapshots. Clicking a mark pins the latest snapshot it covers (`state.pinned`, cleared when the selected task changes) and shows the Changes tab in the pane the transcript isn't in, unless a pane already shows it. Pinned, the Changes header reads `snapshot <n> · <trigger> · <time>` with ← and → to step to the neighbouring snapshots, and the range buttons are **Since main** and **This step** (`step=true`, the default when pinning) plus **Live**, which unpins. A pinned view doesn't refresh on tool results, since a snapshot never changes.
## Events