Give card windows their own undo stacks and coarsen the close
Phase B of the two-level undo card: every card-window gesture — comment
post/delete/edit, body Edit sessions, style and details changes —
registers fine-grained on the window's own stack (window.undoManager
answers with it; board ⌘Z never sees mid-session card steps; an empty
window stack beeps, never falls through). Window close folds the stack
into one coarse values-based board step ("Edit card 'X'") — per-target
per-field later-wins merge, so foreign mid-session writes stay out by
construction, a no-net-change session registers nothing, and any stale
component skips the whole step. The comments/.trash purge defers with
the coarse step via a step-retirement seam on the providers: it runs
when the step leaves the board stack or the board session ends; the git
provider retires dropped steps on register, which keeps Pro's
purge-at-close-flush structural with no tier check. Interim on git
boards: gestures still auto-commit per debounce until phase C's
close-flush commit.
2432 tests in 418 suites green.
Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
@@ -77,6 +77,16 @@ public final class CommentEditSession {
|
||||
@ObservationIgnored
|
||||
public var save: ((String) -> Bool)?
|
||||
|
||||
/// Where this session's **one undo step** goes, called at its commit point with the bytes the
|
||||
/// session opened on and the bytes it leaves — `CardBodyEditSession.registerUndo`'s seam one level
|
||||
/// down, and for its reason exactly: a session is one step, never one per debounced tick
|
||||
/// (13-native-undo.md ▸ Rules ▸ coalescing).
|
||||
///
|
||||
/// `CardComments.beginEdit` points it at `BoardStore.registerCommentEdit`, on the *window's*
|
||||
/// stack. `nil` — a session with no window behind it — registers nothing.
|
||||
@ObservationIgnored
|
||||
public var registerUndo: ((_ priorBody: String, _ newBody: String) -> Void)?
|
||||
|
||||
/// How many saves have actually been attempted through `save`.
|
||||
@ObservationIgnored
|
||||
public private(set) var saveAttempts = 0
|
||||
@@ -142,7 +152,9 @@ public final class CommentEditSession {
|
||||
public func commit() -> Bool {
|
||||
guard !hasEnded else { return false }
|
||||
hasEnded = true
|
||||
return flush()
|
||||
let wrote = flush()
|
||||
registerStep()
|
||||
return wrote
|
||||
}
|
||||
|
||||
/// **Cancel, or Escape** — reverts to session-start bytes and ends (05; 11-command-nexus.md's
|
||||
@@ -179,11 +191,25 @@ public final class CommentEditSession {
|
||||
public func endOnClose() -> Bool {
|
||||
guard !hasEnded else { return false }
|
||||
hasEnded = true
|
||||
return flush()
|
||||
let wrote = flush()
|
||||
registerStep()
|
||||
return wrote
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// The session's one step, at whichever of its two ends got here first — and **only when the
|
||||
/// session actually changed the file**: `disk` is what this session knows the file says, so
|
||||
/// `disk == sessionStart` covers both the session that only read and the one that typed its way
|
||||
/// back to where it started (`CardBodyEditSession.endEditSession`'s guard, restated).
|
||||
///
|
||||
/// Cancel deliberately never reaches here: it writes the start bytes back, so its net effect is
|
||||
/// nothing and a step would only offer to undo an undo.
|
||||
private func registerStep() {
|
||||
guard disk != sessionStart else { return }
|
||||
registerUndo?(sessionStart, disk)
|
||||
}
|
||||
|
||||
private func scheduleSave() {
|
||||
cancelPending()
|
||||
let interval = debounceInterval
|
||||
|
||||
Reference in New Issue
Block a user