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:
2026-08-08 18:55:34 -04:00
parent 78c32776d4
commit 1fd19dfb12
13 changed files with 926 additions and 201 deletions
+22 -8
View File
@@ -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