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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user