feat(windows): open N peer windows kept in sync by a Store change feed - #928
Conversation
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 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>
Every window is built from the same `main` entry in tauri.conf.json, which hard-codes `"title": "Staged"`, and nothing ever called `set_title` — so AppKit's auto-populated Window submenu listed `Staged` once per window and told them apart not at all. Name each window after what it is filtered down to, per the plan note (steps 1-4; step 5 is manual GUI verification). The signal is the filter selection: `projectFiltersStore` is module-scoped and every window is its own webview with its own module graph, so it is already window-local, and it is not persisted, so a cold window starts at the default title with no restore path. - `features/layout/windowTitle.ts` holds a pure `formatWindowTitle(Set)`: status filters first in a fixed Unread-then-Running order, then repo filters, joined with ` · ` (repos among themselves with `, `), capped at two named repos plus `+N more`; the empty set returns DEFAULT_WINDOW_TITLE. Repos are named from their filter key alone — never from `repoBadgeStore`, whose async badge load would make the title flip mid-session — so a stale filter naming a since-deleted repo still titles the window, matching `activeRepoFilters`' independence from hydration. - The emphasis rule moves to `shared/repoLabel.ts` and `RepoLabel.svelte` now consumes it, so the chip and the title can't drift. - `applyWindowTitle`, alongside the formatter, dedupes unchanged titles (so the effect's first run, which always recomputes the default the window already has, issues no IPC) and serializes calls behind the previous one with a post-await generation check, so rapid toggling can't let a superseded title land last. - `WindowHandle` gains `setTitle`; the Tauri branch forwards to the real window and `noopWindow` sets `document.title`, which is a genuine win in web mode where the browser tab is the window list. - `core:window:allow-set-title` joins capabilities/default.json. The note is right that it is absent from tauri's `core:window` default set (verified against gen/schemas/acl-manifests.json: the default set is getters only — `allow-title` yes, `allow-set-title` no), and its absence fails at runtime rather than at build time, so a Rust test pins both the permission and the `win-*` window glob that extends it to secondary windows. - A third top-level `$effect` in App.svelte wires the two together. One correction to the note: its format table renders `block/builderbot` + subpath `apps/staged` as `staged`, but RepoLabel's actual rule — the one the note asks to extract and share — emphasises the *whole* subpath, so the title reads `apps/staged`. Changing the shared helper to the last segment would silently restyle the existing repo chips, so the code's rule wins; the two readings only differ for multi-segment subpaths. Windows/Linux will show these titles in the native titlebar, since `titleBarStyle`/`hiddenTitle` are macOS-only — accepted, per the note's risk section. Verification: `npm test` 731 tests across 60 files pass (13 new in windowTitle.test.ts covering the format table, status ordering independent of insertion order, subpath emphasis, the `+N more` cutoff, stale filters, and applyWindowTitle's dedupe/last-write-wins/error guards); `npm run check` (svelte-check + tsc) reports 0 errors, 0 warnings over 4859 files; `cargo test window_commands` passes 5 tests including the new capability pin; `cargo fmt --check` and prettier are clean. The Window-menu behaviour itself (AppKit still showing NSWindow.title under a hidden title bar) is the note's step 5 and remains unverified by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
`delete_branch` and `delete_project_repo` both resolve their enrichment id before the row goes and comment that it is "published only if the delete lands" — but neither checked `conn.execute`'s row count, so the publish was unconditional. Deleting an already-gone row (a realistic race now that two windows can act on the same branch) resolved the id to `None` and published the frontend's *widest* tier: a command-wide `list_branches_for_project` / `list_project_repos` cache drop plus a refetch in every window. It self-healed, but it turned a lost race into a fleet-wide refetch (flagged in review be789859). Guard both publishes on rows affected, as `delete_note` and `delete_review` already do, so the code matches its comment and the losing racer stays silent. A change-feed test pins both halves: the delete that lands still carries the pre-delete enrichment, the repeat delete publishes nothing. Also close the automated gap the same review flagged around `getWindowLabel()`. It reads Tauri's private `__TAURI_INTERNALS__` (the label must be available synchronously at module-init time, which `getCurrentWindow()` can't offer), so an upgrade that reshapes the internals silently collapses every window to the shared `main` identity — until now caught only by a console tripwire in front of a user. `getCurrentWindow()` resolves the label from the same globals off the same shape, so a new test points the real, unmocked package at the stub `getWindowLabel()` expects and asserts the two agree; a reshaping bump fails `npm test` instead. Verified non-vacuous by reshaping the stub — the test fails at the moved property read. Verification: `cargo test change_feed` passes 7 tests (1 new); `npm test` passes 737 across 60 files (1 new); `npm run check` reports 0 errors over 4860 files; `cargo fmt --check` and prettier are clean. The two clippy warnings under `--tests` remain pre-existing (test_utils.rs:16, store/tests.rs:1201). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
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