The undo command surface rebuilds — app-owned rows and explicit toolbar targets over FocusedValues
Edit ▸ Undo/Redo become the app's own replaced rows and the board toolbar pair takes explicit targets, both reading the focused session's BoardUndoManager through FocusedValues.undoStack (board windows publish the session's manager, card windows their own) — the nil-target route died with the SwiftUI window latch, 13-native-undo.md ▸ Rules ▸ command surface, re-ruled 2026-08-08. The rows enact the routing predicate themselves: text focus routes ⌘Z to the first responder's own manager, title and enablement included, re-derived at fire time with a beep for the stale window. NativeHistoryProvider turns @Observable so both surfaces re-derive on stack changes; a checkpoint-notification ticker covers plain text managers. .responderAction leaves ToolbarItemSpec with its only user; windowWillReturnUndoManager stays wired for AppKit's own asks. Live-probed on the fixture board (21/21): the row retitles to "Undo Add Lane" and crosses via real ⌘Z key events, ⇧⌘Z redoes via a window-server chord, the toolbar pair validates and fires, search-field and body-editor ⌘Z stay text undo with board stacks untouched, and a card window crosses its own stack with no fall-through. 2698 unit tests green. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -236,6 +236,12 @@ struct BoardWindowHost: View {
|
||||
// purge-alert host, which the trash's two confirmed commands raise, and its search
|
||||
// field, which Edit ▸ Find focuses and the caret-chord commands yield to.
|
||||
.focusedSceneValue(\.boardStore, store)
|
||||
// **What Edit ▸ Undo/Redo cross with this window in front** — the session's stack, which
|
||||
// those rows now read for themselves (13-native-undo.md ▸ Rules ▸ the command-surface
|
||||
// bullet, re-ruled 2026-08-08; `UndoCommands.swift`). Read afresh from the model rather
|
||||
// than stored, for `windowUndoManager`'s reason exactly: a torn-down board answers
|
||||
// nothing rather than a stack with no board behind it.
|
||||
.focusedSceneValue(\.undoStack, appModel.session(for: ref)?.undoManager)
|
||||
.focusedSceneValue(\.boardSearch, boardSearch)
|
||||
// The window's identity beside its store — File ▸ Duplicate flushes a *session*, which
|
||||
// is keyed on the window rather than on the board it is showing.
|
||||
@@ -709,11 +715,20 @@ struct BoardWindowHost: View {
|
||||
}
|
||||
}
|
||||
|
||||
// This window's answer to "what does ⌘Z act on" (13-native-undo.md ▸ Rules; 06 ▸ Undo
|
||||
// routing) — the *session's* stack, read afresh on every ask so a torn-down board answers
|
||||
// nothing rather than a stack with no board behind it. The Edit menu's Undo/Redo rows and
|
||||
// the toolbar's pair are nil-target `undo:`/`redo:`, so this one line is what lights them
|
||||
// up: `NSWindow` validates and crosses them against exactly this manager.
|
||||
// This window's answer to "what does ⌘Z act on" (13-native-undo.md ▸ Rules; ▸ Undo routing)
|
||||
// — the *session's* stack, read afresh on every ask so a torn-down board answers nothing
|
||||
// rather than a stack with no board behind it.
|
||||
//
|
||||
// **It no longer lights the command surface**, and that sentence used to be this comment's
|
||||
// whole point (13 ▸ Rules ▸ the command-surface bullet, re-ruled 2026-08-08): the Edit
|
||||
// menu's rows and the toolbar's pair were nil-target `undo:`/`redo:` resolving through
|
||||
// `NSWindow.undoManager`, which a SwiftUI window latches empty during creation, before this
|
||||
// controller's delegate can install — so `windowWillReturnUndoManager` is never consulted
|
||||
// and the surface validated against a stack nothing registers into. Both faces read the
|
||||
// session's manager directly now: the rows through `FocusedValues.undoStack` published a few
|
||||
// lines up in `body`, the toolbar pair through the explicit target `BoardToolbar` is handed
|
||||
// below. This closure stays wired because it is still the right answer wherever *AppKit*
|
||||
// asks a delegate for a manager.
|
||||
windowController.windowUndoManager = { appModel.session(for: ref)?.undoManager }
|
||||
|
||||
// The window-title widget (03-board-ui.md § Board popover) — **board windows only**, which
|
||||
@@ -750,7 +765,11 @@ struct BoardWindowHost: View {
|
||||
search: boardSearch,
|
||||
zoom: appModel.zoom,
|
||||
appearance: appModel.appearance,
|
||||
session: appModel.dragSession
|
||||
session: appModel.dragSession,
|
||||
// The same manager the Edit menu's rows reach through the focus system — one object, two
|
||||
// faces, which is what "the toolbar mirrors the menu" means for this pair now that
|
||||
// neither of them goes through the responder chain (13 ▸ Rules, re-ruled 2026-08-08).
|
||||
undo: appModel.session(for: ref)?.undoManager
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user