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:
@@ -108,7 +108,8 @@ public final class CardComments {
|
||||
@ObservationIgnored
|
||||
public var sweepTrashResidue: (() -> Void)?
|
||||
|
||||
/// The close purge — `BoardStore.purgeCommentTrash(inCard:)`.
|
||||
/// The deferred purge — `BoardStore.purgeCommentTrash(inCard:)`, run through `purgeTrashNow()`
|
||||
/// by whoever owes it (see that method).
|
||||
@ObservationIgnored
|
||||
public var purgeTrash: (() -> Void)?
|
||||
|
||||
@@ -127,6 +128,12 @@ public final class CardComments {
|
||||
@ObservationIgnored
|
||||
public var editComment: ((ItemID, String) -> Bool)?
|
||||
|
||||
/// One inline edit **session**'s undo step — `BoardStore.registerCommentEdit(...)`, handed to each
|
||||
/// session beside its save and called once, at the session's commit point (13-native-undo.md
|
||||
/// ▸ Rules ▸ coalescing; `CommentEditSession.registerUndo`).
|
||||
@ObservationIgnored
|
||||
public var registerCommentEdit: ((ItemID, String, String) -> Void)?
|
||||
|
||||
/// Imports files into an authoring surface's `attachments/` —
|
||||
/// `BoardStore.importCommentAttachments(_:inCard:target:)`.
|
||||
@ObservationIgnored
|
||||
@@ -319,6 +326,9 @@ public final class CardComments {
|
||||
session.save = { [weak self] text in
|
||||
self?.editComment?(commentID, text) ?? false
|
||||
}
|
||||
session.registerUndo = { [weak self] prior, new in
|
||||
self?.registerCommentEdit?(commentID, prior, new)
|
||||
}
|
||||
editing = session
|
||||
}
|
||||
|
||||
@@ -409,20 +419,31 @@ public final class CardComments {
|
||||
|
||||
// MARK: - The close flush
|
||||
|
||||
/// **The window's close, in the order the brief fixes: saves first, purge last.**
|
||||
/// **The pane's half of the window's close: the saves.**
|
||||
///
|
||||
/// The inline session flushes as the body's does (a flush, never a revert — a close is not an
|
||||
/// abandon), then the composer's draft lands, and only then is `comments/.trash/` emptied. The
|
||||
/// purge going last is what makes it safe at all: it removes the folders a delete moved aside, and
|
||||
/// running it before a session's save could remove a folder that save was about to write into.
|
||||
/// abandon), and then the composer's draft lands. Ending twice does nothing the second time — the
|
||||
/// sessions latch — which is what makes the two paths that call this (a window closed on its own,
|
||||
/// and the board's close flush driving it) safe to both exist.
|
||||
///
|
||||
/// Ending twice does nothing the second time — the sessions latch, and a purge over an empty
|
||||
/// trash is a no-op — which is what makes the two paths that call this (a window closed on its
|
||||
/// own, and the board's close flush driving it) safe to both exist.
|
||||
/// **The purge is no longer here** (re-ruled 2026-07-31 — 13-native-undo.md ▸ Interaction with the
|
||||
/// trash): `comments/.trash/` is what the coarse close step's undo restores deleted comments from,
|
||||
/// so emptying it belongs *after* that step exists and only when that step does not. The order
|
||||
/// this method fixed is unchanged and still load-bearing — saves land before anything empties the
|
||||
/// trash — it is now spelled one level up, where the step is registered (`CardWindowSession`),
|
||||
/// because that is the only level that knows whether the purge is owed now or owed later.
|
||||
public func endSession() {
|
||||
editing?.endOnClose()
|
||||
editing = nil
|
||||
composer.flush()
|
||||
}
|
||||
|
||||
/// **Empties `comments/.trash/`** — the deferred purge, run by whoever currently owes it: the
|
||||
/// window's close when no coarse step took it, or that step's retirement when one did.
|
||||
///
|
||||
/// A purge over an empty trash is a no-op, which is what keeps the two owners from having to agree
|
||||
/// about anything but who calls first.
|
||||
public func purgeTrashNow() {
|
||||
purgeTrash?()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user