Disown AppKit window restoration — launches present welcome again instead of a windowless shell

macOS keeps Saved Application State per app, and on macOS 26 its mere
existence — even describing zero windows, which repeated dev kills
guarantee — counts as "a restored session": SwiftUI then treats every
scene's defaultLaunchBehavior as moot and presents nothing. The app
launched as a windowless shell with no way back, since windowOpener is
captured by the first scene that appears — so Open Recent, the
re-grant Grant click, and Dock reopen all silently buffered or
no-opped. Proven by -ApplePersistenceIgnoreState YES presenting
correctly on the same state; with the fix, welcome presented 3/3
consecutive plain launches.

Three changes:
- App.init registers ApplePersistenceIgnoreState — restoration is the
  registry's job (02 § Launch and window lifecycle), every scene
  already declares restorationBehavior(.disabled), and AppKit's layer
  was pure liability. Registered before NSApplicationMain runs, which
  is what makes a registration-domain default early enough.
- The restore bootstrap presents at every launch as the app's one
  reliable presenter; welcome is never system-presented (.suppressed)
  — the pass opens it when nothing else lands on screen.
  LaunchPlan.presentsBootstrap retired.
- captureWindowActions returns the replayed Finder-open count so the
  pass counts those as opens — a cold document launch doesn't get
  welcome stacked beside its board.

Both suites green, verify-editions 30/30.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-29 22:48:04 -04:00
parent 546ed94412
commit 90954a47d6
5 changed files with 48 additions and 28 deletions
+14 -10
View File
@@ -12,8 +12,9 @@ import os
/// Window menu whose only job is to run the pass and then dismiss itself. It exists for a few
/// hundred milliseconds and never draws.
///
/// It is presented **only** when there is something to open (`KanbanApp` decides, via
/// `LaunchPlan.presentsBootstrap`), so the ordinary launch-to-welcome path never creates it.
/// It is presented at **every** launch it is the app's one reliable presenter (see `KanbanApp`'s
/// bootstrap scene for the macOS 26 behavior that forced this), so even the plain launch-to-welcome
/// path runs through it: the pass finds nothing flagged and opens welcome itself.
///
/// ### What the pass does
///
@@ -63,24 +64,27 @@ struct RestoreBootstrapView: View {
private func restore() async {
// Captured directly rather than waiting for `CaptureOpenWindow`'s `onAppear`: this task is
// the app's first act, and `openBoard` needs the action now.
appModel.captureWindowActions(open: openWindow, dismiss: dismissWindow)
// the app's first act, and `openBoard` needs the action now. The count is a cold Finder-open
// that arrived before this window did a board already on its way to the screen, which the
// pass below must count as an open or it would put welcome up beside the user's document.
let replayedOpens = appModel.captureWindowActions(open: openWindow, dismiss: dismissWindow)
switch plan {
case .uiTestFixture:
openFixtureBoard()
case .restoreBoards, .welcome:
// `.welcome` never presents this window, so it cannot arrive here and if a future
// launch path let it, the restoration pass is the harmless answer: it finds nothing
// flagged and shows welcome, which is what `.welcome` asked for anyway.
restoreFlaggedBoards()
// `.welcome` arrives here by design this window presents at every launch, because it is
// the app's one reliable presenter (see `KanbanApp`'s bootstrap scene) and the pass is
// its answer: nothing is flagged for this edition, so it shows welcome, which is what
// `.welcome` asked for.
restoreFlaggedBoards(openedAlready: replayedOpens)
}
dismissWindow(id: WindowID.restoreBootstrap)
}
private func restoreFlaggedBoards() {
var attempted = 0
private func restoreFlaggedBoards(openedAlready: Int) {
var attempted = openedAlready
for board in appModel.boardRegistry.restorables() {
switch board {
case let .available(_, url):