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
+15 -12
View File
@@ -91,6 +91,10 @@ struct CardWindowView: View {
let comments: CardComments
/// This window's thumbnail memory, held by the host so it outlives a snapshot.
let thumbnails: AttachmentThumbnailCache
/// **This card's commit trail** (05 History), or `nil` on every board with no app-managed git
/// the free tier, mode none, and repo-nested boards. The `nil` *is* the section's absence rule;
/// see `historySlot`.
let history: CardHistory?
/// The whole-window file drop (05 Attachments: "the drop surface remains the **whole
/// window**"). `nil` only where a caller has no store to import through.
let fileDrop: CardWindowDropDelegate?
@@ -325,22 +329,21 @@ struct CardWindowView: View {
}
}
/// **The History section's reserved place in the stack** between Details and Actions, 05's
/// order (05 History: "the card's commit trail, read-only newest first semantic subject,
/// relative date, author").
/// **The History section** between Details and Actions, 05's order (05 History: "the card's
/// commit trail, read-only newest first semantic subject, relative date, author").
///
/// Nothing is drawn yet, deliberately: the section is conditional on a git mode that does not
/// exist here, so a header over empty space would claim a commit trail on every board and on
/// the boards where it is *absent* by design (mode none, repo-nested) it would be claiming one
/// that can never arrive. What the slot reserves is the **position**, so filling it in moves
/// nothing above or below it.
/// **Absence is the `nil`, and it is the whole rule.** "The section is absent on boards without
/// app-managed git (mode none, repo-nested) same honesty rule as the popover's git section",
/// and the free tier has no git state at all (12-editions.md The free tier and `.git`). The host
/// builds a `CardHistory` only in mode `git`, so there is no placeholder here to decide about:
/// what the slot reserves is the **position**, and on every other board that position is empty.
///
// m7-git: the trail itself, plus the two rules that come with it absence on boards without
// app-managed git (the same honesty rule as the board popover's git section, 06-history-undo.md)
// and View History, which focuses this section (11-command-nexus.md).
// A later card: View History, which focuses this section (11-command-nexus.md).
@ViewBuilder
private var historySlot: some View {
EmptyView()
if let history {
CardHistorySection(history: history)
}
}
}