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
+33
View File
@@ -1248,6 +1248,39 @@ public final class BoardStore: HealHost {
}
}
/// The same bracket over work that **awaits** the undo restore (06-history-undo.md) and, next,
/// the branch switch.
///
/// A sibling rather than a replacement, and the reason is a hard fact about the two callers: the
/// synchronous version above exists because `performWrite`-shaped work is synchronous, while a
/// git operation is a detached libgit2 task the main actor must not block on
/// (`GitRepository`'s isolation rule). Both keep the bracket, the reload floor and the completion
/// phrase in one place; the distinct argument label is what keeps overload resolution from having
/// to guess which one a trailing closure meant.
///
/// The refusal, the ordering and the arming are the synchronous version's, unchanged see its
/// doc comment for all three.
public func performWholesale(
announcing completion: String? = nil,
awaiting operation: () async throws -> Void
) async throws {
if let readOnlyLock {
throw BoardStoreWriteRefusal.readOnlyLocked(readOnlyLock)
}
watcherBrackets?.begin()
defer {
wholesaleReloadFloor = reloadGeneration + 1
wholesaleCompletion = completion
watcherBrackets?.end()
}
do {
try await operation()
} catch let error as BoardWriteError {
banners.post(error)
throw error
}
}
// MARK: - Lane width
/// Writes a lane's width the one commit point both width mechanisms share (03-board-ui.md §