Implement undo and redo as forward commits

GitHistoryProvider is the second HistoryProviding implementation:
its stack IS HEAD's first-parent ancestry, reseeded on load (redo
empty), re-synced to HEAD before every crossing so agents'
self-commits become the top and ⌘Z steps back exactly one commit;
any arriving commit clears redo (a heal-only window deliberately
does not). Restores are forward commits through the ordinary
signature path — GitRestoreOperation materializes only the
current-vs-target diff as working-tree writes and resolves no
reset/checkout symbol at all; heal commits are transparent
in-session (pointer passes over, restores exclude heal-owned paths,
identity carried on landed windows via PlannedCommit.kind →
GitLandedCommit). Subjects "Undo:/Redo: <crossed subject>"; menu
labels never nest in-session; the root commit is not a step
(crossing it would restore the empty tree).

Provider binding flips: makeHistoryProvider(store, tier, git) —
free binds native everywhere, Pro binds the git provider on git
boards and NOTHING on mode-none/repo-nested (the pair disables
through existing validation); add-git mid-session live-binds via
HistoryStore.didAddGit → bindHistoryProvider (the flip only ever
adds).

SessionSettleGate is the reusable Save All / Discard / Cancel step:
restores whose diff touches an open Edit session or raw-source
buffer gate on it (Save All applies with validation — a refused
buffer cancels the whole restore focused on the offender; Discard
reverts via CardBodyEditSession.discardBuffer and reconciles against
the working tree, deliberately skipping the second flush); untouched
sessions ride through undisturbed. Built for the branch-switch card
to reuse. BoardStore gains the async performWholesale sibling.

CardHistorySection fills the m6 EmptyView slot: read-only, newest
first, follows the card across lane moves by folder-component match
(the UUID is the identity — no rename detection), absent off git
mode and off Pro.

2332 tests / 403 suites green; InertGitTests untouched.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-31 15:54:22 -04:00
parent 563999655f
commit 142c6e75fe
19 changed files with 3126 additions and 82 deletions
+22
View File
@@ -226,6 +226,28 @@ public final class CardBodyEditSession {
return outcome
}
/// **Throws the buffer away and takes disk's word for it** the Discard branch of the
/// save-or-discard step (06-history-undo.md Branch switching: "Discard reverts buffers and
/// uncommitted saves to HEAD").
///
/// It reverts the *buffer* and ends the session; the uncommitted on-disk saves are the operation
/// behind the step's to undo, because only that operation knows which state it is restoring to
/// (`GitRestoreOperation.plan(at:target:excluding:reconciling:)` reconciles the card's folder
/// against the working tree for exactly this reason). Splitting it that way is what keeps the two
/// halves from being two answers able to disagree: one pass writes the card's files, once.
///
/// The pending debounce is cancelled first, which is the load-bearing half a surviving timer
/// would write the discarded text back over the restored card a moment later.
public func discardBuffer() {
cancelPending()
text = disk
sessionOriginBody = nil
if isEditing {
isEditing = false
editSessionDidChange?(false)
}
}
/// `DirtyBufferGuard`'s `attemptSave`: the same flush, with a real failure raised instead of
/// reported.
///