Skip to content

One window per PGN file, tabbed: the database store stops being a singleton - #2

Merged
dancechessmaster merged 4 commits into
mainfrom
tabs/per-window-store
Sep 7, 2026
Merged

One window per PGN file, tabbed: the database store stops being a singleton#2
dancechessmaster merged 4 commits into
mainfrom
tabs/per-window-store

Conversation

@dancechessmaster

Copy link
Copy Markdown
Contributor

Phase 1 of the multi-file work — a pure restructuring, no new features. Each open .pgn is 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; OpenStores registry
  • SavePrompt / quit-time save now go through session.store
  • MainWindow.restoreIfFirst and attachWindow (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):

  • two files → one tab group of two
  • the same file twice → one window
  • an edit saved in tab A changes file A; file B is byte-identical, no stray .bak
  • two tabs, both engine panels open → one Stockfish at 100%, the other never spawns
  • relaunch without hooks → both files restored as tabs

Not 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

dancechessmaster and others added 4 commits September 6, 2026 22:51
…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
@dancechessmaster
dancechessmaster merged commit 50064b4 into main Sep 7, 2026
1 check passed
@dancechessmaster
dancechessmaster deleted the tabs/per-window-store branch September 7, 2026 06:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant