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
+142
View File
@@ -0,0 +1,142 @@
import AppKit
import SwiftUI
/// The welcome window (02-architecture.md § Windows).
///
/// ### What this is, and what it is not yet
///
/// The settled shape is Xcode's: "branding + actions left, recents right (board icon, name,
/// location, lane/card counts, sorted by last opened)". This is the left half, plus the one thing
/// that cannot wait the list of boards that failed to open, because 02 § Launch and window
/// lifecycle forbids a launch-time failure from being silently dropped and welcome is where it must
/// surface.
///
/// The layout is therefore already an `HStack` with one column in it. The recents column drops in
/// beside it; nothing here has to move.
// m4-welcome: the recents column, New Board / Open Recent, per-row Forget and Reveal in Finder, and
// the row-level failure rendering 02 specifies (a failed board's own row carrying fail-fast's
// specifics, or the unavailable state per Graceful orphaning) all land with the welcome milestone.
struct WelcomeView: View {
@Environment(AppModel.self) private var appModel
var body: some View {
HStack(spacing: 0) {
branding
.frame(width: 300)
.frame(maxHeight: .infinity)
.padding(32)
Divider()
failures
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(32)
}
// Fixed, with `.windowResizability(.contentSize)` on the scene: welcome is a launcher, not a
// workspace, and Xcode's the window this one is modelled on does not resize either. The
// one thing that can grow without bound is the failure list, which scrolls.
.frame(width: 760, height: 460)
}
// MARK: Branding and actions
private var branding: some View {
VStack(alignment: .leading, spacing: 0) {
Image(nsImage: NSApp.applicationIconImage)
.resizable()
.frame(width: 96, height: 96)
.accessibilityHidden(true)
Text("Lanework")
.font(.system(size: 34, weight: .light))
.padding(.top, 12)
Text(versionSummary)
.font(.callout)
.foregroundStyle(.secondary)
Spacer(minLength: 24)
Button("Open Board…") {
appModel.presentOpenPanel()
}
.controlSize(.large)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var versionSummary: String {
let info = Bundle.main.infoDictionary
let short = info?["CFBundleShortVersionString"] as? String ?? ""
let build = info?["CFBundleVersion"] as? String ?? ""
return "Version \(short) (\(build))"
}
// MARK: Failed opens
@ViewBuilder
private var failures: some View {
if appModel.launchFailures.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Text("No boards open")
.font(.title3)
Text("Open a board folder to get started.")
.foregroundStyle(.secondary)
}
} else {
VStack(alignment: .leading, spacing: 12) {
Text("Couldn't open")
.font(.title3)
ScrollView {
VStack(alignment: .leading, spacing: 12) {
ForEach(appModel.launchFailures) { failure in
VStack(alignment: .leading, spacing: 2) {
Text(failure.displayName)
.font(.headline)
Text(failure.message)
.font(.callout)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
Text(failure.path)
.font(.caption)
.foregroundStyle(.tertiary)
.lineLimit(1)
.truncationMode(.middle)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
Button("Clear") {
appModel.clearLaunchFailures()
}
}
}
}
}
// MARK: - Settings
/// The app's preferences (, 11-command-nexus.md).
///
/// One control, which is the whole of v1: "Restore open boards at launch". The preference gates only
/// whether the registry's open-now flags are *consulted* at launch the flags themselves are
/// maintained either way, which is what keeps crash recovery working for a user who has restoration
/// turned off and then turns it back on.
struct SettingsView: View {
@AppStorage(AppPreferences.restoreOpenBoardsAtLaunchKey)
private var restoreOpenBoardsAtLaunch = true
var body: some View {
Form {
Toggle("Restore open boards at launch", isOn: $restoreOpenBoardsAtLaunch)
}
.formStyle(.grouped)
.frame(width: 420)
.fixedSize()
}
}