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:
@@ -2,16 +2,25 @@ import AppKit
|
||||
|
||||
// MARK: - BoardUndoManager
|
||||
|
||||
/// What AppKit is handed for a board's windows: an `UndoManager` that owns no stack and answers
|
||||
/// every question from the session's `HistoryProviding`.
|
||||
/// The face a board's undo wears everywhere it is asked about: an `UndoManager` that owns no stack
|
||||
/// and answers every question from the session's `HistoryProviding`.
|
||||
///
|
||||
/// ### Why an adapter exists at all
|
||||
///
|
||||
/// The responder chain speaks one currency. `NSWindow` implements `undo:`/`redo:` and validates
|
||||
/// those menu items itself, reading `canUndo`, `canRedo` and `undoMenuItemTitle` off whatever
|
||||
/// `UndoManager` the window's delegate hands back (`windowWillReturnUndoManager(_:)`) — which is
|
||||
/// exactly how the system's Edit ▸ Undo row and the toolbar's nil-target pair (`BoardToolbar`) light
|
||||
/// up, disable and retitle with no code of the app's own.
|
||||
/// The command surface speaks one currency. Enablement, the composed menu title, and the crossing
|
||||
/// itself are all `UndoManager` questions — `canUndo`, `undoMenuItemTitle`, `undo()` — and every
|
||||
/// surface that offers ⌘Z asks them of *something*: the app's own Edit ▸ Undo/Redo rows read this
|
||||
/// object through `FocusedValues.undoStack` (`UndoCommands.swift`), the board toolbar's pair carries
|
||||
/// it as an explicit target (`BoardToolbar`), and AppKit itself still asks the window's delegate for
|
||||
/// it (`HostedWindowController.windowWillReturnUndoManager`, wired and correct wherever the platform
|
||||
/// is the one asking). One object answers all of them, which is what keeps them from disagreeing.
|
||||
///
|
||||
/// (Those first two used to be the same fact as the third: the rows and the toolbar pair were the
|
||||
/// system's nil-target `undo:`/`redo:`, validated by `NSWindow` against whatever its delegate vended.
|
||||
/// That route is unreachable on a SwiftUI window — the window latches an empty manager of its own
|
||||
/// during creation, before any delegate of ours installs — so the surface became the app's own,
|
||||
/// reading this object directly: 13-native-undo.md ▸ Rules ▸ the command-surface bullet, re-ruled
|
||||
/// 2026-08-08. What it reads did not change, only how it gets here.)
|
||||
///
|
||||
/// The seam, though, must not be an `NSUndoManager`: a protocol that vended one could only ever have
|
||||
/// had a single implementation, and the substrate stays behind `HistoryProviding` so a future
|
||||
@@ -50,9 +59,10 @@ import AppKit
|
||||
/// "Every read-only lock (vanished root, failed reload after wholesale ops, unwritable location)
|
||||
/// disables Undo/Redo with the other mutating commands; **the stack itself survives the lock and
|
||||
/// resumes when it clears**" (13 ▸ Rules). This is the right place for it and the only one: every
|
||||
/// surface that offers ⌘Z — the Edit menu's nil-target row, the toolbar pair, a card window's
|
||||
/// responder chain — validates through this object, so answering `false` here disables all of them
|
||||
/// at once, exactly as the lock's other victims disable through menu validation (02-architecture.md
|
||||
/// surface that offers ⌘Z — the Edit menu's own rows over the focused stack, the board toolbar's
|
||||
/// pair over the session's, a card window's over its own — validates through this object, so
|
||||
/// answering `false` here disables all of them at once, exactly as the lock's other victims disable
|
||||
/// through menu validation (02-architecture.md
|
||||
/// § "The lock's scope"). Putting it in the *provider* would have been the same answer in the wrong
|
||||
/// place: the stack is not the thing that is locked, the board is, and whatever provider a session
|
||||
/// binds — a future one included — must inherit the rule without reimplementing it.
|
||||
|
||||
Reference in New Issue
Block a user