feat(windows): open N peer windows kept in sync by a Store change feed - #928
feat(windows): open N peer windows kept in sync by a Store change feed#928matt2e wants to merge 7 commits into
Conversation
Every window is a full peer copy of the app, and a Store-level change feed keeps them consistent: a write in any window — or in the backend itself — reaches every other window and every web client. Windows: a `new_window` command builds `win-N` windows from the parsed tauri.conf.json main-window entry, cascaded from the opener, behind File ▸ New Window (Cmd+N; New Project moves to Shift+Cmd+N so the native accelerator can't swallow the keydown). The capability glob widens to ["main", "win-*"] — without it invoke fails and the window never shows — and win-* labels are excluded from window-state tracking, so stale geometry entries never accumulate for labels the plugin can't remove. Menu events target the focused window instead of fanning out across windows, and open a new window natively when nothing is focused. Per-window identity: the PR-poll client id becomes `tauri-<label>` (the first window stays `tauri-main`) so each window's selected project and focus count independently in the scheduler's union, with the `tauri-` namespace reserved at the web boundary; last-viewed-project is keyed per window, only the first window restores it on cold start, and a new window opens on its opener's project via a one-shot seed handed through `new_window`. Change feed: every mutating store method publishes a domain-vocabulary `StoreChange` (Project / Branch / Notes / Review / Repos) — 65 methods across 13 store modules. The Store stays Tauri-agnostic behind an optional broadcast sender, so its unit tests are untouched; a coalescer above the Tauri boundary dedupes identical changes on a 50ms window and forwards the event set to every window and WebSocket client. Session-family writes deliberately publish nothing — chat polls at 500ms and session lifecycle has its own events. Publishes are gated on real movement: the PR-status and workspace-status writes emit only when a domain field actually changes, so the pollers stop churning the feed in steady state. Frontend: the change feed supersedes the imperative staleness workarounds, which are retired. Event-driven branch refetches are gated on real hydration rather than mere map membership, and a branch-changed cache drop is scoped to the project the payload names instead of every cached branch list. Web mode revalidates on WebSocket reconnect and flushes an all-null invalidation when the feed lags. App-menu wiring moves out of App.svelte into menuListener.ts, and the window label is read once and fails loudly when it can't be. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The store change feed's project-changed event triggered projectsDataStore.revalidate(), but in web mode that refetch reads through cachedCommand (list_projects at a 5min TTL, list_project_repos at 10min during rehydration). A fresh-by-TTL entry answered the event-driven refetch with the pre-mutation data, so a project created, renamed, or deleted in a native window (or another tab) stayed invisible to a web client until the TTL expired — the exact staleness the feed exists to eliminate. Tauri windows were unaffected since cachedCommand bypasses the cache there. Give project-changed a cache leg in cacheInvalidationListener, mirroring the branch handler: list_projects drops command-wide (it takes no args), and list_project_repos scopes to the named project, widening to every project only on the feed's lag recovery (null projectId). Tests cover both tiers. Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ft+Cmd+N
The previous multi-window change gave File > New Window the plain Cmd+N
accelerator and pushed New Project onto Shift+Cmd+N, and the only caller
of new_window lived in the macOS-only menu block — so Windows/Linux
builds lost their new-project chord and gained no way to open a second
window at all (flagged in review 484f9b1c).
Swap the chords back and give the command a cross-platform entry point:
- The macOS File > New Window accelerator becomes Cmd+Shift+N, so the
native menu no longer consumes plain Cmd+N before the webview sees it.
- app-new-project returns to Cmd+N, and the sidebar/list shortcut hints
follow ("New project (Cmd+N)").
- A Tauri-only app-new-window shortcut (Cmd+Shift+N) registers in
App.svelte, calling commands.newWindow with the current window's
selected project — the same seed the macOS menu path passes. On macOS
the native accelerator still wins and routes through menuListener, so
the binding is effectively the Windows/Linux affordance the review
found missing; in web mode it is omitted since the web server rejects
new_window.
svelte-check, cargo check, and the frontend test suite (696 tests) pass.
Signed-off-by: Matt Toohey <contact@matttoohey.com>
lag_flush_events and event_for each enumerate the same five wire event names, but nothing coupled them: a future StoreChange variant wired into event_for (and the frontend) but forgotten in lag_flush_events would silently lose lag recovery for exactly that surface — the staleness bug the flush exists to prevent, and one no test pinned (flagged in review 484f9b1c). Add a test asserting the event-name set of lag_flush_events equals the set produced by mapping every StoreChange variant through event_for. The variant list comes from a helper whose wildcard-free match over StoreChange makes a new variant a compile error in the test module until the helper — and therefore the assertion, and therefore lag_flush_events — is updated to match. cargo test store_events passes (4 tests); fmt clean; the two clippy warnings under --tests are pre-existing in test_utils.rs and store/tests.rs. Signed-off-by: Matt Toohey <contact@matttoohey.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94e09d2b48
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| listenToEvent<NotesChangedEvent>('notes-changed', (payload) => { | ||
| if (payload.branchId) { | ||
| invalidateBranchTimeline(payload.branchId); | ||
| } else { | ||
| window.dispatchEvent(new CustomEvent('project-notes-invalidated')); |
There was a problem hiding this comment.
Refresh child-note aggregates on branch note changes
When a project session creates or completes a child_note in another window, the store publishes notes-changed with that note's branchId, so this handler only invalidates the branch timeline. Child notes are deliberately excluded from that timeline, while ProjectSection fetches them with listChildNotes() only when the parent note opens; consequently, an already-open parent note never sees the new or updated child until it is closed and reopened. Route child-note changes to a refresh of the open project-note aggregate as well.
Useful? React with 👍 / 👎.
Every window becomes a full peer copy of the app, and a Store-level change feed keeps them consistent: a write in any window — or in the backend itself — reaches every other window and every web client.
Windows
new_windowcommand buildswin-Nwindows from the parsedtauri.conf.jsonmain-window entry, cascaded from the opener, behind File ▸ New Window (Cmd+Shift+N;Cmd+Nstays New Project so the native accelerator can't swallow the keydown). A Tauri-onlyapp-new-windowshortcut gives Windows/Linux the same affordance.["main", "win-*"]— without itinvokefails and the window never shows — andwin-*labels are excluded from window-state tracking so stale geometry entries don't accumulate for labels the plugin can't remove.tauri-<label>(first window staystauri-main) so each window's selected project and focus count independently in the scheduler's union, with thetauri-namespace reserved at the web boundary. Last-viewed-project is keyed per window; only the first window restores it on cold start, and a new window opens on its opener's project via a one-shot seed handed throughnew_window.Change feed
StoreChange(Project / Branch / Notes / Review / Repos) — 65 methods across 13 store modules. TheStorestays Tauri-agnostic behind an optional broadcast sender, so its unit tests are untouched.event_forcan emit.Frontend
project-changedin web mode can't be answered by a fresh-by-TTLlist_projects/list_project_reposentry holding pre-mutation data.App.svelteintomenuListener.ts, and the window label is read once and fails loudly when it can't be.Test coverage added for the coalescer, window commands, menu listener, cache invalidation, navigation, and the store change publishes. CI (crates-fmt/lint/test, differ-ci, staged-ci) is green on the pushed branch.
🤖 Generated with Claude Code