The board window says its name once — the scene declares the title SwiftUI keeps hiding

`HostedWindowController.hideTitle()` was the same shape of bug a429a7e fixed for
`titlebarAppearsTransparent`: an out-of-band `NSWindow.titleVisibility` write, correct the
instant it ran, undone by SwiftUI's own next pass over the window's configuration — a tree
that declares nothing resolves `.visible`, and SwiftUI writes that back over the out-of-band
`.hidden` on the very next `@State`-driven re-render this board window's own liveness causes.
The system title reappeared beside the board-popover widget, "occasionally" — whenever that
next re-render happened to land.

Confirmed with an A/B harness (no interactive display in this session, so not reproduced on
screen; mechanism established in code, per the card's own fallback): a bare out-of-band write
held indefinitely against resize and key-status changes alone, but reverted on the very next
`@State`-driven render and stayed reverted — reasserting from `body`'s own construction or
from `.onChange` both lost the same race, since SwiftUI's resync runs later than either. The
only thing that held was declaring the posture in the tree itself, mirroring
`.toolbarBackgroundVisibility`'s role in a429a7e.

`KanbanApp`'s board `WindowGroup` now declares `.windowToolbarStyle(.unified(showsTitle:
false))`. It is a scene modifier, not a per-window one, so — unlike `.toolbarBackgroundVisibility`
— it cannot wait for a board's load to finish before taking effect; every board window it
creates keeps the system title hidden from its very first frame. `boardLoadingTitlebarAccessory`
covers the gap that opens before the loading window has a store to build the real widget from: a
small, non-interactive, plain-text stand-in carrying the registry record's cached name, installed
the moment the window attaches and swapped by identity for the real widget the moment the store
loads — so the loading window's chrome still carries a name throughout, per 02-architecture.md.
`hideTitle()`'s own write stays; it is no longer what keeps the title hidden, but it is still
correct for the one render turn before the scene's own re-assertion catches up.

Confined to `BoardWindowHost.swift`, `BoardInfoPopover.swift` and `KanbanApp.swift` —
`WindowAccessor.swift`'s shared `hideTitle()`/`titleVisibility` machinery is untouched, since a
concurrent fix is addressing the card window's version of this same bug through that file.

New regression tests (`BoardLoadingTitlebarStandInTests`, `KanbanTests/BoardLoadingTests.swift`)
pin the stand-in's layout and the identity-based swap. Full suite green (3219 tests) except the
pre-existing, documented environment-sensitive `PointerLatencyTests`, confirmed unaffected by
rerunning them in isolation.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 12:06:01 -04:00
parent 50e26efdb9
commit f4b2de55fb
4 changed files with 183 additions and 10 deletions
+48 -10
View File
@@ -81,6 +81,17 @@ struct BoardWindowHost: View {
/// get around to it.
@State private var openWalk = BoardOpenWalk()
/// The loading window's plain-text stand-in for the titlebar widget
/// (`boardLoadingTitlebarAccessory`), held only so `configureWindow` can find and remove it once
/// the real, interactive widget is ready to take its place.
///
/// Not routed through `windowController`'s own accessory slot (`installTitlebarAccessory`),
/// which is install-once and holds exactly one accessory for this window's whole life this one
/// is deliberately temporary, so it is added and removed with the raw `NSWindow` API instead,
/// from the same `onAttach` closure that already survives the provisional-window swap
/// (`configureLoadingWindow`).
@State private var loadingAccessory: NSTitlebarAccessoryViewController?
/// This board's registry record, from the moment `recordOpen` mints it which is what the
/// loading window's title reads (`Self.loadingTitle`). `nil` only for the one body evaluation
/// that precedes `start()`.
@@ -680,14 +691,27 @@ struct BoardWindowHost: View {
/// is replaced wholesale by the flushing version once the board is open (see below); a single
/// closure branching on `phase` would be the same thing spelled as a state read.
///
/// The title bar keeps AppKit's own title display for now the string is the record's cached
/// name (`windowTitle`) and `hideTitle()` follows only once the board-popover widget is there
/// to say the name instead. Hiding it here would leave a loading window with no name anywhere in
/// its chrome, which is precisely what 02 asks the loading state to carry.
/// The title bar carries the record's cached name from the first frame (`windowTitle`) not
/// through AppKit's own title display, which the board scene's `.windowToolbarStyle(.unified
/// (showsTitle: false))` (`KanbanApp`) keeps hidden on every board window unconditionally, but
/// through a plain-text stand-in widget (`boardLoadingTitlebarAccessory`) installed here.
/// `configureWindow(store:recordID:)` swaps it for the real, interactive one once the
/// board-popover widget is there to say the name instead leaving the loading window with no
/// name anywhere in its chrome for even one frame is precisely what 02 asks the loading state
/// not to do.
private func configureLoadingWindow(recordID: UUID) {
windowController.onAttach = { window in
guard let saved = appModel.boardRegistry.record(id: recordID)?.windowFrame else { return }
window.setFrame(HostedWindowController.placementOnCurrentScreens(for: saved), display: true)
if let saved = appModel.boardRegistry.record(id: recordID)?.windowFrame {
window.setFrame(HostedWindowController.placementOnCurrentScreens(for: saved), display: true)
}
// Fresh per attach, deliberately: this closure re-fires on the provisional-window swap
// (the comment below), and a stand-in built for a window that is about to be discarded
// would be a stale reference this state never sees again.
let accessory = boardLoadingTitlebarAccessory(
title: Self.loadingTitle(record: appModel.boardRegistry.record(id: recordID), url: ref.url)
)
loadingAccessory = accessory
window.addTitlebarAccessoryViewController(accessory)
}
// The window may already be attached `viewDidMoveToWindow` fires before this task's first
// suspension so the placement is applied directly too rather than waiting for a callback
@@ -775,6 +799,15 @@ struct BoardWindowHost: View {
// asks a delegate for a manager.
windowController.windowUndoManager = { appModel.session(for: ref)?.undoManager }
// The loading window's plain-text stand-in (`configureLoadingWindow`) has done its job
// found by identity, `HostedWindowController.removeTitlebarAccessory`'s own pattern, because
// nothing else promises this is the only accessory the window carries by the time it exists.
if let loadingAccessory, let window = windowController.window,
let index = window.titlebarAccessoryViewControllers.firstIndex(where: { $0 === loadingAccessory }) {
window.removeTitlebarAccessoryViewController(at: index)
}
self.loadingAccessory = nil
// The window-title widget (03-board-ui.md § Board popover) **board windows only**, which
// is why it is installed here rather than in `WindowAccessor`: welcome, the bootstrap and
// card windows share that machinery and have no board to describe. It goes in after the
@@ -794,10 +827,15 @@ struct BoardWindowHost: View {
// keeps feeding the Window menu, Exposé, VoiceOver and restoration; only the title bar's own
// rendering of that string is suppressed.
//
// **After the load, and only after it**, which is why it is not in the loading half above:
// this line and the widget it defers to are one exchange, and a loading window that hid its
// title before the widget existed would carry no name at all against 02's "its chrome
// carrying the registry record's cached title".
// **Not this call's job anymore, keeping it hidden** the duplicate-name bug this call once
// let through, occasionally, once the board had been open long enough to re-render: a
// SwiftUI scene window's `titleVisibility` is SwiftUI's to hold, and it writes the tree's
// resolved value back on every pass that re-applies a window's configuration, so this
// out-of-band write alone would survive only until the next board-driven re-render.
// `KanbanApp`'s board scene now declares
// `.windowToolbarStyle(.unified(showsTitle: false))` unconditionally, which is what SwiftUI
// keeps re-asserting; this call stays as the same value one turn earlier correct from the
// instant it runs, not the one thing making it last.
windowController.hideTitle()
// The board's customizable toolbar (03-board-ui.md Toolbar) installed here for the