Stand up the window architecture — welcome, board, card

Four scenes (welcome, restore bootstrap, board group, card group) with
system restoration disabled in favor of the registry's open-now flags:
set when a window actually opens, cleared only on user close, so quit —
and crash — leave exactly the restoration set behind. AppModel joins
windows to sessions (shared store, registry record, card refs, held
security scope); CloseFlushCoordinator pins 02's strict close order as
a seam-injected machine (card sessions end, windows drain, store
flushes, record stamps, teardown) with named slots where m6/m7 flushes
land. HostedWindowController proxies — never replaces — SwiftUI's
window delegate to intercept windowShouldClose for the flush, report
frames, and place saved frames onto live screens. Card windows are
(board path, case-folded card id) values: reopen focuses, and a
snapshot-pure fate function dismisses on delete, tombstone, tombstoned
lane, or cross-board move.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-27 07:47:11 -04:00
parent 61e18c3dfa
commit fccdf56cf4
19 changed files with 2767 additions and 6 deletions
+67
View File
@@ -99,6 +99,21 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
public var windowFrame: WindowFrame?
/// Whether this board's window is open **right now** the restoration set, as a live marker
/// rather than an at-quit write (02-architecture.md § Launch and window lifecycle, settled).
///
/// Set when the board's window opens, cleared on *user-initiated* close; quit's teardown
/// deliberately leaves it standing, because the boards open at quit are by definition the ones
/// to restore. **Crash recovery falls out for free**: after a crash the flags describe what was
/// open at crash time, so the next launch restores exactly that no separate recovery logic, no
/// once-at-quit stamp to race teardown or miss when the app dies.
///
/// **Optional because every key here must be** (see Evolving this struct above): a registry file
/// written before this key existed decodes with `nil`, which reads as "not open" and costs the
/// user nothing. A non-optional `Bool` would have quarantined every existing file on upgrade and
/// emptied everyone's recents.
public var isOpenNow: Bool?
/// Whether committing also pushes (07-sync-collab.md). Off by default: pushing is a decision,
/// not a side effect.
public var pushOnCommit: Bool
@@ -117,6 +132,7 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
laneCount: Int? = nil,
cardCount: Int? = nil,
windowFrame: WindowFrame? = nil,
isOpenNow: Bool? = nil,
pushOnCommit: Bool = false,
remoteLocationWarned: Bool = false
) {
@@ -128,6 +144,7 @@ public struct BoardRecord: Codable, Sendable, Equatable, Identifiable {
self.laneCount = laneCount
self.cardCount = cardCount
self.windowFrame = windowFrame
self.isOpenNow = isOpenNow
self.pushOnCommit = pushOnCommit
self.remoteLocationWarned = remoteLocationWarned
}
@@ -236,6 +253,12 @@ public final class BoardRegistry {
/// A match is updated in place with a **fresh bookmark** (subsuming the stale-refresh case), the
/// caller's `displayName`, the path it was opened at, and `lastOpened` = now. No match creates a
/// record. Either way the file is saved before returning.
///
/// **`isOpenNow` is deliberately untouched here.** Recording an open and *being* open are two
/// different facts: this method is called before a window exists (and, later, by flows that
/// record a board without showing one), so the flag is set by `setOpenNow(id:)` once the window
/// has actually opened. Folding it in would flag boards that never made it onto screen and hand
/// the next launch a restoration set describing failures.
@discardableResult
public func recordOpen(of rootURL: URL, displayName: String) -> UUID {
let bookmark = Self.makeBookmark(for: rootURL)?.data ?? Data()
@@ -279,6 +302,50 @@ public final class BoardRegistry {
}
}
// MARK: - The open-now marker
/// Marks this board as open called when its window has actually opened, not when the open was
/// merely attempted (02-architecture.md § Launch and window lifecycle).
public func setOpenNow(id: UUID) {
update(id) { $0.isOpenNow = true }
}
/// Clears the marker **user-initiated close only**.
///
/// Quit's teardown must never call this, and that omission is the entire restoration mechanism:
/// "quit's teardown closes deliberately leave it standing (the boards were open at quit by
/// definition; teardown distinguishes user-close from quit-close, and that distinction is the
/// whole mechanism)". A crash calls nothing at all, which is why crash recovery needs no code of
/// its own the flags already describe what was open when the app died.
public func clearOpenNow(id: UUID) {
update(id) { $0.isOpenNow = false }
}
/// The boards to reopen at launch: every record still flagged open, **oldest `lastOpened`
/// first**.
///
/// Ascending, unlike `recents()`, because these are reopened in order and the result should be
/// the stacking the user left behind the most recently opened board ends up frontmost because
/// it opens last. Classification is `recents()`' own bookmark resolution, reused rather than
/// re-implemented: a flagged board on an unmounted volume is `unavailable` here for exactly the
/// reason it is unavailable there, and the launch flow renders it as a failed restoration
/// (§ "A restored board that fails surfaces on welcome, row-level") instead of hanging on a
/// mount.
///
/// The preference gates only whether this is *consulted*; the flags are maintained regardless.
public func restorables() -> [RecentBoard] {
recents()
.filter { $0.record.isOpenNow == true }
// Ascending, with the same id tie-break `recents()` uses inverted, so two boards opened
// in the same millisecond still come back in one stable order rather than whichever
// `sorted(by:)` felt like.
.sorted { lhs, rhs in
lhs.record.lastOpened == rhs.record.lastOpened
? lhs.record.id.uuidString < rhs.record.id.uuidString
: lhs.record.lastOpened < rhs.record.lastOpened
}
}
// MARK: - Per-board settings
public func updateWindowFrame(id: UUID, frame: WindowFrame) {