Hide duplicate snapshot lines and the turn-end line before a snapshot

This commit is contained in:
Ian Gulliver
2026-10-20 21:08:00 -07:00
parent 53f20e40d3
commit 1d3c4576ff
3 changed files with 27 additions and 17 deletions
+1 -1
View File
@@ -454,7 +454,7 @@ Merging or discarding a task stops new snapshots for it, waits for one in progre
`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 (for snapshot 1, the same merge-base: the snapshot commit's parent is the worktree's `HEAD`, which already holds whatever byre committed at the end of the turn). 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. `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 (for snapshot 1, the same merge-base: the snapshot commit's parent is the worktree's `HEAD`, which already holds whatever byre committed at the end of the turn). 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 `mac/Sources/Byre/Store.swift`). A snapshot is a point between the agent's events, not part of one, so each gets its own row in the transcript, a divider reading `snapshot #n` (its trigger and time in the tooltip), placed where its `snapshot.created` event falls in the task's event order: just after the turn end it belongs to (after the triggering text for an old `message` snapshot). A snapshot row ends the current group of tool calls, so calls after it start a new group. The row's **changes** button pins the snapshot (`Store.pinned`, cleared when the selected task changes) and switches the right pane to the Changes tab; its **fork** button forks it (see "Forking") and points the right pane at the new task. 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. The transcript is the timeline. The UI learns a task's snapshots from the `snapshot.created` events in its transcript (`snapshots` in `mac/Sources/Byre/Store.swift`). A snapshot is a point between the agent's events, not part of one, so each gets its own row in the transcript, a divider reading `snapshot #n` (its trigger and time in the tooltip), placed where its `snapshot.created` event falls in the task's event order: just after the turn end it belongs to (after the triggering text for an old `message` snapshot). A snapshot that directly follows another snapshot's row with the same `tree_sha` (nothing drawn between them, so the same files and the same conversation, such as an answer's snapshot right after the turn end's) gets no row of its own; forks of it show on the earlier row. A snapshot row directly after a turn end replaces that turn end's line. A snapshot row ends the current group of tool calls, so calls after it start a new group. The row's **changes** button pins the snapshot (`Store.pinned`, cleared when the selected task changes) and switches the right pane to the Changes tab; its **fork** button forks it (see "Forking") and points the right pane at the new task. 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.
## Forking ## Forking
+14 -14
View File
@@ -1133,7 +1133,14 @@ struct RowView: View {
var body: some View { var body: some View {
if !row.hidden { if !row.hidden {
content if let seq = row.summarySeq {
content
.onScrollVisibilityChange(threshold: 0.01) { visible in
model.summaryVisible(seq, visible)
}
} else {
content
}
} }
} }
@@ -1267,19 +1274,12 @@ struct RowView: View {
PreBox(text: text) PreBox(text: text)
case .turnEnd(let error): case .turnEnd(let error):
Group { if let error {
if let error { Text(error)
Text(error) .font(Fonts.font(11))
.font(Fonts.font(11)) .foregroundStyle(Palette.danger)
.foregroundStyle(Palette.danger) } else {
} else { Rectangle().fill(Palette.border).frame(height: 1)
Rectangle().fill(Palette.border).frame(height: 1)
}
}
.onScrollVisibilityChange(threshold: 0.01) { visible in
if let seq = row.summarySeq {
model.summaryVisible(seq, visible)
}
} }
case .note(let note): case .note(let note):
+12 -2
View File
@@ -518,11 +518,21 @@ final class TranscriptBuilder {
if ev.kind == "snapshot.created" { if ev.kind == "snapshot.created" {
ctx.run = nil ctx.run = nil
let snap = try! decodePayload(ev.payload, as: Snapshot.self) let snap = try! decodePayload(ev.payload, as: Snapshot.self)
let row = Row(.snapshot(snap), taskId: rowTask)
ctx.snapshots[snapKey(snap.n)] = row
if ![.answer, .fork, .askHuman].contains(snap.trigger) && sink == nil && own { if ![.answer, .fork, .askHuman].contains(snap.trigger) && sink == nil && own {
lastReady = nil lastReady = nil
} }
release()
let prev = (ctx.handoff?.body ?? sink ?? root).rows.last { !$0.hidden }
if let prev, case .snapshot(let p) = prev.kind, p.treeSha == snap.treeSha, prev.taskId == rowTask {
ctx.snapshots[snapKey(snap.n)] = prev
continue
}
let row = Row(.snapshot(snap), taskId: rowTask)
ctx.snapshots[snapKey(snap.n)] = row
if let prev, case .turnEnd(nil) = prev.kind {
prev.hidden = true
row.summarySeq = prev.summarySeq
}
put(row) put(row)
continue continue
} }