The undo stack is HEAD's first-parent ancestry, live

Fills the gap: foreign commits — auto-committed agent work and agent
self-commits — push onto the in-session stack as steps, with a
pre-flight HEAD re-sync before every undo/redo. One rule in-session
and across relaunch; arriving commits clear redo.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
This commit is contained in:
2026-07-26 16:18:57 -04:00
parent 03ec2f0879
commit 607e1b837b
+1
View File
@@ -15,6 +15,7 @@ Git is the undo substrate — on boards that have git. **Git is opt-in per board
- **Cadence constraint** (agreed): the auto-commit cadence must not make the history of a remote-shared board unbearable — one commit per drag is fine for a local undo trail but noisy as a shared log. The debounce/batching design here must serve both consumers; the push/pull side is settled in 07-sync-collab.md (optional push-on-commit, automatic fetch-rebase-push on rejected pushes). - **Cadence constraint** (agreed): the auto-commit cadence must not make the history of a remote-shared board unbearable — one commit per drag is fine for a local undo trail but noisy as a shared log. The debounce/batching design here must serve both consumers; the push/pull side is settled in 07-sync-collab.md (optional push-on-commit, automatic fetch-rebase-push on rejected pushes).
- **Undo never rewrites history.** Undo (⌘Z) and redo (⇧⌘Z) restore earlier states as **new forward commits** — never reset, never force. The whole trail stays inspectable in any git client. The one deliberate rewrite anywhere in the app is pull's rebase of **unpushed local** commits (07-sync-collab.md); published history is never touched. - **Undo never rewrites history.** Undo (⌘Z) and redo (⇧⌘Z) restore earlier states as **new forward commits** — never reset, never force. The whole trail stays inspectable in any git client. The one deliberate rewrite anywhere in the app is pull's rebase of **unpushed local** commits (07-sync-collab.md); published history is never touched.
- **Undo restore vs open Edit sessions** (settled): a restore materializes only the diff between the current tree and the target state, so a card whose open Edit session the diff doesn't touch is simply unaffected — its uncommitted ~700 ms saves and the stage-around rule continue undisturbed, and most undos never meet an editor at all. When the diff *does* touch a session card, the restore **gates on the branch-switch save-or-discard step** (Branch switching below — Save All / Discard / Cancel, same machinery, same rationale): silently flushing would commit a tree the user deliberately hasn't saved, a checkout over uncommitted on-disk saves would destroy text no commit protects (the one place "both versions exist as commits" could otherwise fail), and a surviving dirty buffer's next debounced save would write pre-undo text over the restored card — a ⌘Z that visibly doesn't happen. With sessions settled the restore runs on a settled tree. Redo is symmetric. Open raw-source buffers get the branch-switch settle treatment too (Branch switching below). - **Undo restore vs open Edit sessions** (settled): a restore materializes only the diff between the current tree and the target state, so a card whose open Edit session the diff doesn't touch is simply unaffected — its uncommitted ~700 ms saves and the stage-around rule continue undisturbed, and most undos never meet an editor at all. When the diff *does* touch a session card, the restore **gates on the branch-switch save-or-discard step** (Branch switching below — Save All / Discard / Cancel, same machinery, same rationale): silently flushing would commit a tree the user deliberately hasn't saved, a checkout over uncommitted on-disk saves would destroy text no commit protects (the one place "both versions exist as commits" could otherwise fail), and a surviving dirty buffer's next debounced save would write pre-undo text over the restored card — a ⌘Z that visibly doesn't happen. With sessions settled the restore runs on a settled tree. Redo is symmetric. Open raw-source buffers get the branch-switch settle treatment too (Branch switching below).
- **The stack is HEAD's first-parent ancestry, live** (settled): foreign commits — watcher-auto-committed agent work and agents' *self*-commits alike — push onto the in-session undo stack as ordinary steps as they land. The stack re-syncs its top to HEAD before every undo/redo (self-commits move HEAD outside the app's committer; the pre-flight sync is how the stack learns), so ⌘Z always steps back exactly **one** commit — it can never silently revert twenty minutes of agent work landed since the user's last operation. Any commit arriving from anywhere clears the redo stack (classic behavior; redo also starts empty on the relaunch reseed below). In-session and post-relaunch behavior are thereby one rule — the reseed is the same ancestry walk from scratch.
- **Undo survives relaunch**: the undo stack reseeds from HEAD's first-parent ancestry on load; redo starts empty. In-session it behaves as classic dual stacks; after relaunch, past restore commits reappear as ordinary undoable steps. Deliberate: no sidecar state, nothing ever lost. Interaction with pull (07-sync-collab.md): a pull rebases unpushed local commits, so the in-session stack must remap onto the rewritten commits — the pre-rebase hashes are orphaned. A pleasant consequence of the reseed rule: the fetched remote commits sit in HEAD's first-parent ancestry, so after the next relaunch remote work becomes ordinary undoable steps too. - **Undo survives relaunch**: the undo stack reseeds from HEAD's first-parent ancestry on load; redo starts empty. In-session it behaves as classic dual stacks; after relaunch, past restore commits reappear as ordinary undoable steps. Deliberate: no sidecar state, nothing ever lost. Interaction with pull (07-sync-collab.md): a pull rebases unpushed local commits, so the in-session stack must remap onto the rewritten commits — the pre-rebase hashes are orphaned. A pleasant consequence of the reseed rule: the fetched remote commits sit in HEAD's first-parent ancestry, so after the next relaunch remote work becomes ordinary undoable steps too.
- **Undo is board-local.** A cross-board move-out undone at the source resurrects the card even though it lives on in the destination — per-board histories cannot and must not mutate other boards. The resulting same-UUID fork across boards is legitimate (boards are independent identity namespaces); if the two ever meet through a move-in, the import boundary remints the arrival (01-storage-format.md's identity lifecycle). - **Undo is board-local.** A cross-board move-out undone at the source resurrects the card even though it lives on in the destination — per-board histories cannot and must not mutate other boards. The resulting same-UUID fork across boards is legitimate (boards are independent identity namespaces); if the two ever meet through a move-in, the import boundary remints the arrival (01-storage-format.md's identity lifecycle).
- The git surface lives in the **board popover** (03-board-ui.md): branch/source display, branch switching and creation, and the commit-identity name/email fields (see Interaction with external writers below) — alongside board rename and styling. - The git surface lives in the **board popover** (03-board-ui.md): branch/source display, branch switching and creation, and the commit-identity name/email fields (see Interaction with external writers below) — alongside board rename and styling.