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:
@@ -362,6 +362,13 @@ struct CardWindowHost: View {
|
||||
// item reaches the frontmost one's body surface through this, exactly as board-window
|
||||
// items reach their window's store (`FocusedBoardStoreKey`).
|
||||
.focusedSceneValue(\.cardBody, bodyPresentation)
|
||||
// **This window's own stack, published for Edit ▸ Undo/Redo** — the second of 13's two
|
||||
// levels reaching the menu (13-native-undo.md ▸ Rules ▸ two levels; ▸ the
|
||||
// command-surface bullet, re-ruled 2026-08-08). One key serves both levels because a
|
||||
// card window's manager is the same face type as a board session's, so which of them a
|
||||
// row crosses is decided by which window is in front and by nothing else — no
|
||||
// fall-through, as a property of the value rather than a rule (`UndoCommands.swift`).
|
||||
.focusedSceneValue(\.undoStack, session.undo.manager)
|
||||
// View ▸ Raw Source (⌥⌘E) reaches the frontmost card window the same way, and Edit Body
|
||||
// reads it too — "View ▸ Edit Body (⌘E) disables while source mode is active"
|
||||
// (05-card-window.md ▸ Raw source outlet).
|
||||
@@ -878,15 +885,22 @@ struct CardWindowHost: View {
|
||||
|
||||
// **This window's own stack** (13-native-undo.md ▸ Rules ▸ two levels, re-ruled 2026-07-31 —
|
||||
// superseding the shared-stack wiring): "a card window owns its own stack for the session it
|
||||
// represents ... and `window.undoManager` answers with it (standard per-window AppKit
|
||||
// scoping)". ⌘Z with this window in front walks the gestures made *here*, newest first, and
|
||||
// when they run out it beeps — "no fall-through: exhausting the window's stack ... never
|
||||
// reaches board history" (06-history-undo.md ▸ Undo routing). What board history gets is the
|
||||
// one coarse step this session registers when the window closes.
|
||||
// represents". ⌘Z with this window in front walks the gestures made *here*, newest first,
|
||||
// and when they run out it beeps — "no fall-through: exhausting the window's stack ... never
|
||||
// reaches board history" (13 ▸ Undo routing). What board history gets is the one coarse step
|
||||
// this session registers when the window closes.
|
||||
//
|
||||
// The card's *text* surfaces are untouched by this — the body editor and the raw-source
|
||||
// editor each vend their own manager to the responder chain, which is what keeps typing undo
|
||||
// above either stack (06 ▸ Undo routing, unchanged).
|
||||
// **The Edit menu does not arrive through here**, which is what the same rule's 2026-08-08
|
||||
// re-ruling changed: `window.undoManager` is latched empty by SwiftUI before this
|
||||
// controller's delegate installs, so the rows read this same manager through
|
||||
// `FocusedValues.undoStack` instead (published in `body` above; `UndoCommands.swift`). The
|
||||
// closure stays wired for AppKit's own asks, which is the one caller it always had a right
|
||||
// to.
|
||||
//
|
||||
// The card's *text* surfaces are untouched by either wiring — the body editor and the
|
||||
// raw-source editor each vend their own manager to the responder chain, and the rows route
|
||||
// to the first responder's manager while one of them holds the keyboard, which is what keeps
|
||||
// typing undo above either stack (13 ▸ Undo routing, unchanged).
|
||||
windowController.windowUndoManager = { [session] in session.undo.manager }
|
||||
|
||||
windowController.onAttach = { window in
|
||||
|
||||
Reference in New Issue
Block a user