Skip to content

feat(windows): record every window's session as it changes; treat windows the same - #994

Merged
PathGao merged 6 commits into
masterfrom
feat/session-equal-windows
Oct 20, 2026
Merged

PathGao merged 6 commits into
masterfrom
feat/session-equal-windows

Conversation

@PathGao

@PathGao PathGao commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

What this is

Follows #990. Windows are now equal, and each one's session is recorded as it changes rather than only at quit:

  • After a crash or kill -9, every window comes back with its own tabs, its own unsaved tabs (feat(editor): keep unsaved changes when quitting #962), and its own position and size. Before, the window list was only written on quit, and unsaved tabs from other windows landed in the launch window.
  • Nothing treats the window labelled main specially any more, except that it is the window Tauri creates at launch.
  • The two restore settings are renamed in parallel, one for windows and one for content: Restore windows on launch ("Reopens your last windows and tabs. File contents are read from disk.") and Restore unsaved changes on launch ("Quitting won't ask to save. Tabs with unsaved changes come back next time."). They now sit next to each other in Settings → Files. Setting keys are unchanged.

Mechanism

Continuous session. Each window writes its own window-state/<label>.json within 500 ms of a change to its tabs, active tab, view mode, split, tag or sidebar folder. It uses #962's bounded, serialized writer, renamed createBoundedWriter. Scroll and cursor positions don't trigger writes; they ride along with the next write, and are also written on blur, when the window is hidden, on close and on quit. The set of snapshot files is the window list. Window geometry is saved through the window-state plugin 500 ms after a move or resize (coalesced), because the plugin itself only saves on exit.

Remember or forget is unchanged from #990, with two gaps closed:

  • An updater restart no longer brings other windows back stale, since they are already current.
  • If a quit is cancelled partway, the windows it had already closed are forgotten. The quit state is now Mutex<Option<Vec<label>>> instead of a bool, and set_quitting(false) drops their snapshots. Their unsaved backups become orphans.

Former main-window privileges:

Privilege Now
Crash guard Per label, the same code in every window
Orphan unsaved backups Handed out by Rust (claim_unsaved_recovery), once each, to whichever window asks first
Sidebar folder Stored in each window's own snapshot. The global folder.lastRoot key is migrated once
Startup file (argv, or macOS Opened before ready) Answered once, to the launch window
Files opened while running Focused window, else last focused, else the launch window
Legacy localStorage snapshot Read and removed once by the launch window

scripts/windowsAreEqual.test.ts fails if the frontend compares against 'main' or mentions isMain. In Rust non-test code, "main" appears only as LAUNCH_LABEL.

Startup adoption. A small plugin runs before the window-state plugin loads. If the launch window has no snapshot of its own but other windows are remembered, it adopts the first of them: snapshot, crash guard, unsaved backup and geometry. So a relaunch after closing the launch window opens no extra empty window. The snapshot is moved last, so a kill midway re-adopts the same window. An existing launch-window backup is never overwritten; the adopted one becomes an orphan instead.

Behaviour changes

  • With "Restore windows on launch" off, the sidebar folder is no longer reopened, because it is part of window state now. Unsaved changes are governed by the other setting and still come back, into the launch window.
  • An empty window open at quit comes back, as in VS Code.

Scope

  • The window-state plugin writes its file with fs::write, not an atomic rename. A kill during that write loses every window's geometry once. Tabs and unsaved changes are unaffected. This is marked ponytail: in the code.
  • In the first ~500 ms after launch, before the first snapshot, a crash loses the sidebar folder.
  • Windows and Linux were not run.

Tests

  • Bounded writer: nothing is written before restore, writes are bounded at 500 ms with the latest state winning, a forget isn't undone by a queued write, and dispose waits for an in-flight write. Each was confirmed to fail with the code broken.
  • Rust: cancelled quit forgets windows already closed; orphans are claimed once; the adoption decision (a pure function); adoption moves files and geometry without overwriting the launch window's backup; last-focused routing; geometry is saved on move and resize with the same flags as the plugin.
  • Folder: restored per window, the old key migrated once, a window's own folder wins.
  • Tests that assumed main-only restore were updated. The new-window test (feat(windows): open a new empty window with Cmd+Shift+N #991) no longer passes the removed isMainWindow option.

Verification

npm run check 0 errors, npm test 1142 pass, npx vitest run 523 pass, cargo test 206 pass, cargo fmt --check and cargo clippy --all-targets -- -D warnings clean.

macOS 27, release test bundle:

  • Two windows, each with an unsaved untitled tab. After kill -9, both came back, each with its own tab and text.
  • Two windows moved to an external display and resized (80,-1150 760×560 and 700,-950 900×651). After kill -9, both reopened at exactly those positions and sizes.
  • Closing the launch window asked and was discarded, then Quit. On relaunch, one window opened at the other window's position and size with its tab, and its files had been adopted. No empty window appeared.

"main" is now only the name of the window Tauri builds at launch. The
frontend has no branch on it; Rust tells the launch window apart in one
constant, LAUNCH_LABEL, and only at startup.

- Startup adoption: before window-state loads its file, a launch window
  with no snapshot takes over the first remembered window's snapshot,
  breadcrumb, unsaved backup and geometry, so a launch never opens an
  extra empty window. The snapshot moves last, so a launch killed partway
  adopts it again. An existing backup of the launch window is never
  overwritten; the other one stays an orphan.
- Orphan backups (no open window, no snapshot) are handed out by
  claim_unsaved_recovery, once, to the first window that asks.
- Every window runs the same startup: restore, reopen_remembered_windows
  (only the first call of a launch does anything), claim backups, ask
  for startup files. send_markdown_path answers only the launch window,
  once; a file macOS opens before that joins the launch's files instead
  of also going to whichever window has focus.
- A cancelled quit forgets the windows it already closed. Their backups
  stay and are adopted as orphans.
- Each window keeps its sidebar folder in its own snapshot. The
  install-wide folder.lastRoot is read once, by the first window to start,
  and removed.
- Forgetting a window drops its breadcrumb whatever its label.
A window's restore snapshot is written whenever its tabs, their order,
the active tab, a tab's view (edit, split, ratio, scroll sync), the tag
or the folder change, through the bounded, serialized writer #962 uses
for unsaved backups: at most one write per 500 ms, latest state wins.
Scroll and editor position are not tracked, so scrolling writes nothing;
they go out with the next tracked change and on focus loss, hide, close
and quit. So after a crash or kill every window comes back with its own
tabs and backup instead of being merged into the launch window, and an
update restart finds the other windows already current.

Every window writes a snapshot once its restore has run, an empty one
included: the set of snapshot files is the window list. Recording starts
only after the window's own restore, so its empty starting state never
replaces what it is about to read. Forgetting a window waits for a write
in flight and stops the writer first, so the file cannot come back.

createUnsavedRecovery becomes createBoundedWriter, with the value already
on disk as `initial`, and dispose resolves once the last write landed.
tauri-plugin-window-state saves only on a clean exit, so after a crash
or kill every window came back on the built-in display at its default
size, though its tabs were restored. A move or resize now schedules one
save of every window's geometry 500 ms later, through the existing
coalesced helper. The plugin and this save share geometry_flags.
Its only caller is the macOS RunEvent::Opened handler, so clippy's dead-code
lint failed the Linux and Windows builds.
@PathGao
PathGao merged commit b94472c into master Oct 20, 2026
4 checks passed
@PathGao
PathGao deleted the feat/session-equal-windows branch October 20, 2026 14:13
PathGao added a commit that referenced this pull request Oct 20, 2026
The install-wide key only existed between #964 and #994, both merged the same
day, and never shipped in a release.
PathGao added a commit that referenced this pull request Oct 20, 2026
… bits (#996)

* docs: drop Live Mode from comments about the always-on watcher

Since #986 the file watcher runs whether or not Live Mode is on, and #987
turned Live Mode on by default. Comments and test names still said the
watcher was Live Mode's, or that Live Mode was off by default.

* docs: bring comments in line with continuous snapshots and the JS menu

#994 writes each window's snapshot as it changes, not once at close, and
calls the first window the launch window rather than main. #993 builds the
menu in the frontend, so its Copy item is no longer PredefinedMenuItem::copy.

* chore(folder): drop the folder.lastRoot migration

The install-wide key only existed between #964 and #994, both merged the same
day, and never shipped in a release.

* chore: drop the menu-close-file listener

Nothing has emitted the event since 715e1b6.
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