Skip to content

Several windows at once - #3

Open
auslander969 wants to merge 1 commit into
gregmos:mainfrom
auslander969:multi-window
Open

auslander969 wants to merge 1 commit into
gregmos:mainfrom
auslander969:multi-window

Conversation

@auslander969

Copy link
Copy Markdown
Contributor

Opens more than one window at a time. Built on 0.2.2, so it sits on top of the macOS window work in #2 rather than around it.

Ctrl+Shift+N opens another window on the same folder with nothing in it. Ctrl+Shift+W closes one. Ctrl+W still closes the document, and the native Window submenu is untouched — Close Window stays absent, as review #5 has it.

What had to be settled

A window is its own webview with its own store, so most of this was already true: two windows share nothing and neither has to know what the other is showing. Four things they would otherwise fight over now live in src-tauri/src/windows.rs, with the front end's half in src/app/windows.ts.

Which window a file belongs to. A path from Finder, a second launch or the command line is parked on the window the user was last in, and only that window is nudged. The old single queue would have had every window racing to drain it. PendingPaths/PendingFolders became one parcel per window label, drained by take_opening.

One file, one window. Two windows on one document would be two buffers, two drafts under the same name and two saves racing each other — §8 territory. Opening a file another window already has brings that window forward instead. Unless this window opened something of its own: then the focus stays put and the status bar says where the other one went, because being thrown into another window over one of three files would lose the other two.

state.json. Version 2 is a list with one entry per window. Version 1 was the single object one window wrote and still reads as the one window it was, so an existing file upgrades rather than disappearing. No window can see the others' entries, so the file is assembled and written in Rust; parseSession is unchanged and still parses one entry, which is why its tests are untouched.

Closing a window drops its entry. Quitting keeps them all — and so does closing the last window, because that is how the app is quit on Windows and how file → exit ends everywhere, and quitting means coming back where you left off.

settings.json. One file, so a theme changed in one window changes in all of them. Without it the other windows carry on with the old value and write it back over the new one on the way out.

Quitting

⌘Q, the Dock's Quit and file → exit all emit plain:quit-requested to every window; each runs the close scenario it already had and destroys itself, and the app goes when the last one has. exit_app is gone — the exit guard from #2 just needed webview_windows().is_empty() instead of asking for main.

One consequence worth naming: cancelling the unsaved-changes question in the third window leaves the first two closed. Nothing unsaved is lost — those two were saved or explicitly discarded — but it is not the "all or nothing" some apps do. Happy to do the two-phase version if you would rather.

The watcher

One watcher per window rather than one shared. A window now only hears about its own files, and a window opening a document no longer makes every other window's library be indexed again. The shared version I tried first was worse than the original in both respects.

watch is also async now, so the tree walk that starting a watcher does happens off the main thread. That one is a pre-existing stall rather than something multi-window introduced — with a large library the window does not draw until the walk finishes — but two windows made it easy to hit. Splitting it into its own commit is no trouble if you would prefer it separately.

Also

Recovery drafts belong to the app, not a window, so only the first window of a run offers them; three windows each offering the same file would be three chances to answer one question, and two of them wrong. Nameless buffers carry the window label in their id so two windows counting from one each cannot collide on a draft name.

Tested

npx vitest run (673 pass, 52 files) and cargo test (90 pass) on macOS. New cases cover version 1/version 2 state.json and the one-file-one-window rule, including that a window still opens the file if the registry cannot be reached.

By hand on macOS 15, debug build under a separate bundle identifier so it could run alongside a released Plain:

  • launch → one window; Ctrl+Shift+N → a second, empty, same library
  • quit → state.json v2 with both windows and what each had open
  • relaunch → both windows come back, each with its own document
  • window labels are stable across runs, so the window-state plugin gives each one its old size and place

Not verified, and worth your eye: Windows — I have no machine for it, and the fs-scope and association paths there are yours. Closing one window of several and confirming its entry goes: the path is small and reviewed, but I did not exercise it. Cascade placement for a new window is best-effort and looked like the OS was choosing anyway.

🤖 Generated with Claude Code

`Ctrl+Shift+N` opens another window on the same folder with nothing in it,
`Ctrl+Shift+W` closes one, and `file → exit` still ends the app: every
window gets the unsaved-changes question and the app goes when the last of
them has closed itself.

A window is its own webview with its own store, so most of this is already
true — two windows share nothing, and neither has to know what the other is
showing. What had to be settled is the four things they would otherwise
fight over. They live in src-tauri/src/windows.rs, with the front end's half
in src/app/windows.ts.

Which window a file belongs to. A path from Finder, from a second launch or
from the command line is parked on the window the user was last in, and only
that window is nudged — rather than every window racing to drain one queue.

One file, one window. Two windows on one document would be two buffers, two
drafts under the same name and two saves racing each other (spec §8), so
opening a file another window already has brings that window forward
instead. Unless this window opened something of its own: then the focus
stays here and the status bar says where the other one went.

state.json. Version 2 is a list with one entry per window; version 1 was the
single object one window wrote, and still reads as the one window it was. No
window can see the others' entries, so the file is assembled and written in
Rust. Closing a window drops its entry; quitting keeps them all, and so does
closing the last window, because that is how the app is quit on Windows.

settings.json. One file, so a theme changed in one window changes in all of
them. Without that the other windows would carry on with the old value and
write it back over the new one on the way out.

The watcher is now one per window rather than one shared: a window only
hears about its own files, and a window opening a document does not make
every other window's library be indexed again. Starting one walks the whole
tree, which is long enough to be seen on a large library, so `watch` is
`async` and does that off the main thread — on the main thread it is the
window not drawing for as long as it takes.

Recovery drafts are the app's, not a window's, so only the first window of a
run offers them. Three windows each offering to restore the same file would
be three chances to answer one question, and two of them wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gregmos added a commit that referenced this pull request Sep 12, 2026
`Ctrl+Shift+N` opens another window on the same library, empty and
cascaded from this one; `Ctrl+Shift+W` closes it. Windows live as long as
the app runs and do not come back after a restart — the files do, through
`recent`, which is now shared between windows. state.json stays one
snapshot written by the first window alone.

What the windows share is kept small: which window a path from outside
goes to (the focused one), which window already holds a file (one file,
one buffer — checked on the canonical path after the read), the settings
(a change in one window applies in all), and `recent`. Each window has
its own watcher; quitting asks every window about its own unsaved work,
and the app goes when the last one has closed itself.

Window creation and watcher setup are async commands — a synchronous
command that builds a webview deadlocks WebView2 on Windows. The close
scenario is single-entry, so a quit arriving over an open dialog neither
doubles it nor destroys the window twice.

Grew out of PR #3 by @auslander969 (Several windows at once); the
session-restore half of it was dropped by agreement, and its Windows
defects are excluded by construction. Spec: review/waves/w13-windows-spec.md.

Co-Authored-By: auslander969 <134359370+auslander969@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E9xq5TL7X83gzMZHauso67
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