One window per PGN file, tabbed: the database store stops being a singleton - #2
Merged
Conversation
…gleton A PGN file is a document, and the app held exactly one. This makes each open file a window — windows tab together natively, as Safari's do — with its own list, filter, selection and engine, and brings the set back on the next launch. It is the first half of the multi-file work; nothing moves between tabs yet. The rule everything rests on: **one file, one window.** Write-back regenerates the whole file from the window's cache, so a file open twice would be two caches overwriting each other's saves. Opening a file that is already open brings its window forward instead. Three things that were not obvious on the way: - The check has to be a claim made the moment a window is assigned a file, not a lookup of loaded stores. SwiftUI applies a window's value on the next update pass, so two opens of one file in the same turn both saw an empty registry and both opened — measured, then fixed. - SwiftUI has already shown a new window by the time AppKit hands it over, so setting tabbingMode is too late for it to join the group on its own; it is added to the front database window's tab group by hand. - Only the key window's engine runs. The suspend observers arrive a beat after the window does, and a window already pushed behind another by then has to suspend on attachment or it keeps searching. With two tabs and both panels open there is one Stockfish at 100% and the other never spawns. Gone with the singleton: the multi-file "merged list" that could not be saved (several files are now several tabs), the detach-every-session step on open (nothing is replaced any more), and the single last-opened path (the open set is remembered, and the last game per file). Checked with the dev hooks: two files open as one tab group; the same file twice is one window; an edit saved in one tab changes that file and leaves the other byte-identical with no stray .bak; the open set restores. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F9o9DSzZas4GsekCpG5M8T
Found by the user on the first try of the branch: quit, relaunch, no tabs. The headless test had killed the process, which leaves the list as it was; a real ⌘Q closes the windows one by one, and each one leaving rewrote the "open files" list without itself — so the last one out wrote an empty list. The list is now frozen at the start of applicationShouldTerminate: what is open at that moment is what comes back, and windows closing after it do not edit it. A window the user closes by hand still drops out, as before. A DCS_AUTO_QUIT hook takes the real terminate path after N seconds, so the test now quits the way a person does, and DCS_TRACE writes the lifecycle breadcrumbs stdout would have lost. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F9o9DSzZas4GsekCpG5M8T
Found by the user: "+" appeared, and clicking it opened a blank window. SwiftUI answers newWindowForTab: itself, by opening the window group's scene with no value, so the handler on the app delegate never ran. Rather than fight the responder chain, the empty window is the signal: a window that arrives after launch with no file can only have come from "+" (or Window ▸ New Tab). It asks for a file as soon as it has an NSWindow — chosen, it becomes that file's tab; already open elsewhere, that tab comes forward and this one closes; cancelled, it closes. The hook sends newWindowForTab: the way the button does, so the path is tested end to end: Cancel leaves one window, picking a file leaves two. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F9o9DSzZas4GsekCpG5M8T
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 of the multi-file work — a pure restructuring, no new features. Each open
.pgnis a window; windows tab together natively; a file is opened once and opening it again brings its tab forward; only the front tab's engine runs; the open set comes back on launch, each file on the game you were looking at.Why it is safe to have several files open: write-back regenerates the whole file from the window's cache, so a file open twice would be two caches overwriting each other's saves. The one-file-one-window rule is enforced by a claim made the moment a window is assigned a file — a lookup of loaded stores is not enough, because SwiftUI applies a window's value on the next update pass and two opens in one turn both see an empty registry (measured, then fixed).
What to review first (the paths that protect the user's files):
DatabaseStore.load/writeBack— per-window, single file;OpenStoresregistrySavePrompt/ quit-time save now go throughsession.storeMainWindow.restoreIfFirstandattachWindow(tab grouping, engine suspend/resume)Removed: the multi-file merged list (unsaveable),
SessionRegistry.detachAll,store.openPgn(urls), the single last-opened path.Checked with the dev hooks (
DCS_AUTO_OPEN=a,b,DCS_AUTO_TABS_OUT,DCS_AUTO_TAB_SAVE):.bakNot verified headless: the tab bar's "+" button (
newWindowForTab:on the app delegate).Phase 2 (copy games between tabs, file watching) follows as its own PR.
🤖 Generated with Claude Code
https://claude.ai/code/session_01F9o9DSzZas4GsekCpG5M8T