Build the auto-commit engine
Every settled change on a git-mode board commits, debounced 2s past drag/typing churn, staged whole-root with .gitignore respected. GitCommitOperation reaches the vendored libgit2 directly (same 1.9.2 pin SwiftGitX resolves — importable, not duplicated) for signature-capable commits; add-git's config materialization is gone, identity resolves at commit time (repo-local config, else derived default) per the 2026-07-31 ruling in 06. CommitAttribution classifies per file off EchoLedger receipts: user identity on app-mediated windows, Lanework External <[email protected]> on foreign, the modified-by refinement (<slug>@agents.lanework .invalid) when every foreign file agrees, heal-marked receipts split into their own commit — window split foreign → heal → user. Edit-session granularity: ~700ms saves stay uncommitted, staging excludes open session folders (closure-resolved so mid-session moves stage around the new location), session end nudges the debounce so each session lands exactly one body commit. Flush-before-overwrite gates on known-foreign windows and commits synchronously ahead of the write; close/quit flush the pipeline via CloseFlushCoordinator's committerFlush. index.lock backs off briefly then re-debounces silently; clean tree no-ops; genuine failures ride the standing history-suspension banner and retry next debounce. Abnormal repo states (detached HEAD, merge/rebase/cherry-pick in progress) hold the engine with a 15s re-check; unborn HEAD commits "Initial board state" whole-tree; dirty tree at open catches up through the same engine. Message seam (CommitMessageComposing) ships interim — the semantic composer is the next card. Discovery diffs HEAD against an in-memory index with rename detection (git status alone never pairs a bare mv), and a failed survey reads as "could not look", never "nothing changed". 46 new tests / 8 suites, all real repositories via bundled libgit2. 2240 tests / 383 suites green; InertGitTests untouched. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -67,6 +67,21 @@ public final class CardBodyEditSession {
|
||||
/// Whether the buffer holds keystrokes the file does not.
|
||||
public var isDirty: Bool { text != disk }
|
||||
|
||||
/// **Whether an Edit session is open right now** — the body column is showing the editor.
|
||||
///
|
||||
/// The fact pro-m1's committer stages around: "a board change committing mid-session excludes
|
||||
/// the session card's folder from staging, so a lane move never sweeps half-typed body text into
|
||||
/// its commit" (06-history-undo.md ▸ Rules ▸ Auto-commit).
|
||||
///
|
||||
/// **Open, not dirty**, deliberately: an *open* session is what 06 names, and the exclusion has
|
||||
/// to cover the moment between a landed ~700 ms save and the next keystroke — precisely when the
|
||||
/// buffer is clean and the file holds a half-typed paragraph no commit should carry yet. A
|
||||
/// session opened and never typed in costs one card folder its commits until the flip back, and
|
||||
/// nothing else. (Recorded as a judgment call: 06 says "open Edit sessions", the branch-switch
|
||||
/// rule qualifies its own gate with "unsaved keystrokes, or on-disk saves the session hasn't
|
||||
/// committed", and the two readings differ only for the untouched session.)
|
||||
public private(set) var isEditing = false
|
||||
|
||||
// MARK: Seams
|
||||
|
||||
/// The debounce interval — **~700 ms** (05 ▸ Edit), and settable so a test does not have to
|
||||
@@ -95,6 +110,22 @@ public final class CardBodyEditSession {
|
||||
@ObservationIgnored
|
||||
public var registerUndo: ((_ priorBody: String, _ newBody: String) -> Void)?
|
||||
|
||||
/// **The session boundary, announced** — called with `true` when an Edit session opens and
|
||||
/// `false` when it ends, and with nothing in between.
|
||||
///
|
||||
/// `CardWindowHost` points it at the board's auto-committer, which registers the card's folder to
|
||||
/// stage around while the session stands and **nudges** when it ends (06-history-undo.md ▸ Rules
|
||||
/// ▸ Auto-commit: the Edit→Preview flip is "the effective Save button", and raw-source entry and
|
||||
/// window close end the session too). That nudge is what turns a session's several debounced
|
||||
/// saves into exactly one commit: they commit nothing while the folder is excluded, and the
|
||||
/// whole diff becomes committable at once when it is not.
|
||||
///
|
||||
/// A closure for `save`'s reason exactly — this type is a buffer and a clock, and it stays
|
||||
/// testable by having no idea what a repository is. `nil` (the free tier, a storeless test) means
|
||||
/// nothing is listening, which is the same shape every other seam here takes.
|
||||
@ObservationIgnored
|
||||
public var editSessionDidChange: ((_ isEditing: Bool) -> Void)?
|
||||
|
||||
/// What disk said before this session's **first** landed save — the step's before-value, held
|
||||
/// from the first write until the session ends.
|
||||
///
|
||||
@@ -158,6 +189,17 @@ public final class CardBodyEditSession {
|
||||
return saveNow()
|
||||
}
|
||||
|
||||
/// The start of one Edit session — the flip into Edit, or a window that opened straight into it
|
||||
/// because its card's body was empty (`CardBodyMode.opening(body:)`).
|
||||
///
|
||||
/// Idempotent, because the mode can be re-asserted by a menu validation pass or a re-published
|
||||
/// focus value, and a second announcement would register a session that is already registered.
|
||||
public func beginEditSession() {
|
||||
guard !isEditing else { return }
|
||||
isEditing = true
|
||||
editSessionDidChange?(true)
|
||||
}
|
||||
|
||||
/// The end of one Edit session — the flip back to Preview, raw-source entry, or the window
|
||||
/// closing. Flushes, and marks the boundary pro-m1's auto-commit coalesces on (see the type's
|
||||
/// doc comment).
|
||||
@@ -173,6 +215,14 @@ public final class CardBodyEditSession {
|
||||
registerUndo?(origin, disk)
|
||||
}
|
||||
sessionOriginBody = nil
|
||||
// **Last**, after the flush and after the undo step: the committer's nudge must find the
|
||||
// session's final bytes already on disk, or the commit it arms would carry the file as it
|
||||
// stood one keystroke ago. Guarded on `isEditing` so a window closing from Preview — which
|
||||
// calls this too, and should — announces nothing.
|
||||
if isEditing {
|
||||
isEditing = false
|
||||
editSessionDidChange?(false)
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user