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
+112
View File
@@ -346,6 +346,118 @@ struct BoardRegistryTests {
#expect(ids(registry.recents()) == [firstID, thirdID, secondID])
}
// MARK: The open-now marker
@Test("Opening flags a board, a user close unflags it, and quit deliberately leaves it standing")
func openNowTracksWindowsAndSurvivesQuit() async throws {
let storage = try RegistryStorage()
defer { storage.tearDown() }
let fixture = try makeBoard()
defer { fixture.tearDown() }
let registry = BoardRegistry(storageURL: storage.url)
let id = registry.recordOpen(of: fixture.root, displayName: "Work")
#expect(registry.record(id: id)?.isOpenNow == nil, "recording an open is not opening a window")
#expect(registry.restorables().isEmpty)
registry.setOpenNow(id: id)
#expect(registry.record(id: id)?.isOpenNow == true)
#expect(ids(registry.restorables()) == [id])
// A user close. The flag goes, and with it the board's place in the next launch.
registry.clearOpenNow(id: id)
#expect(registry.record(id: id)?.isOpenNow == false)
#expect(registry.restorables().isEmpty)
// A quit. The teardown stamps counts and does *not* clear the flag that omission is the
// whole restoration mechanism, so it is asserted rather than assumed, and asserted across a
// reload of the file because a relaunch is what consumes it.
registry.setOpenNow(id: id)
registry.recordClose(id: id, laneCount: 2, cardCount: 5)
let afterRelaunch = BoardRegistry(storageURL: storage.url)
#expect(afterRelaunch.record(id: id)?.isOpenNow == true, "the flags describe what was open at quit")
#expect(ids(afterRelaunch.restorables()) == [id])
#expect(afterRelaunch.record(id: id)?.laneCount == 2)
}
@Test("Restorables are the flagged records only, oldest first")
func restorablesReopenInLastOpenedOrder() async throws {
let storage = try RegistryStorage()
defer { storage.tearDown() }
let first = try makeBoard()
defer { first.tearDown() }
let second = try makeBoard()
defer { second.tearDown() }
let third = try makeBoard()
defer { third.tearDown() }
let registry = BoardRegistry(storageURL: storage.url)
let firstID = registry.recordOpen(of: first.root, displayName: "First")
try await Task.sleep(for: .milliseconds(5))
let secondID = registry.recordOpen(of: second.root, displayName: "Second")
try await Task.sleep(for: .milliseconds(5))
let thirdID = registry.recordOpen(of: third.root, displayName: "Third")
registry.setOpenNow(id: firstID)
registry.setOpenNow(id: thirdID)
// Ascending, the exact inverse of `recents()` these are reopened in order, so the board
// opened last at quit opens last again and ends up frontmost.
#expect(ids(registry.restorables()) == [firstID, thirdID])
#expect(ids(registry.recents()) == [thirdID, secondID, firstID], "recents is unchanged, and still newest first")
#expect(!ids(registry.restorables()).contains(secondID), "a board that was not open does not restore")
// A flagged board whose folder has gone still comes back classified, not dropped. The
// launch flow renders it as a failed restoration rather than pretending it was never open.
third.tearDown()
let rows = registry.restorables()
#expect(ids(rows) == [firstID, thirdID])
guard case .unavailable = rows[1] else {
Issue.record("expected the deleted board to classify unavailable, got \(rows[1])")
return
}
}
@Test("A registry file written before the open-now key existed still decodes")
func oldRegistryFilesDecodeWithoutTheOpenNowKey() async throws {
let storage = try RegistryStorage()
defer { storage.tearDown() }
// Byte-for-byte the shape this file had one milestone ago: no `isOpenNow` anywhere. The
// struct's evolution rule says a new key must be optional precisely so this file survives
// a required key would have failed to decode, quarantined the file, and emptied the user's
// recents on upgrade.
let id = UUID()
let garbage = Data("not a bookmark".utf8).base64EncodedString()
let json = """
[
{
"bookmark" : "\(garbage)",
"cardCount" : 9,
"displayName" : "Archive",
"id" : "\(id.uuidString)",
"laneCount" : 4,
"lastKnownPath" : "/Volumes/Archive/Boards/Archive",
"lastOpened" : "2026-01-01T09:00:00.000Z",
"pushOnCommit" : true,
"remoteLocationWarned" : true
}
]
"""
try Data(json.utf8).write(to: storage.url)
let registry = BoardRegistry(storageURL: storage.url)
#expect(registry.recents().count == 1, "the file decoded; nothing was quarantined")
#expect(registry.record(id: id)?.isOpenNow == nil, "a missing key reads as 'not open'")
#expect(registry.restorables().isEmpty)
#expect(registry.record(id: id)?.laneCount == 4, "and every other field survived")
// And the key writes through from here on.
registry.setOpenNow(id: id)
#expect(BoardRegistry(storageURL: storage.url).record(id: id)?.isOpenNow == true)
}
// MARK: Bookmarks in a sandboxed host
@Test("A bookmark is always produced, and resolves back to the same folder")