Skip to content

fix: acknowledge tab transfer after destination render#237

Closed
PathGao wants to merge 11 commits into
alecdotdev:masterfrom
PathGao:codex/fix-pr-214
Closed

fix: acknowledge tab transfer after destination render#237
PathGao wants to merge 11 commits into
alecdotdev:masterfrom
PathGao:codex/fix-pr-214

Conversation

@PathGao

@PathGao PathGao commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Fixes the cross-window transfer loss window identified in #214. The broker retains the staged payload until the destination validates, inserts, and renders the tab, then acknowledges the source.\n\nValidation:\n- node --test --import tsx scripts/tabTransfer.test.ts\n- npm run check

PathGao and others added 11 commits July 25, 2026 04:44
…e persistence

Multi-window groundwork on the Rust side:

- tab_transfer.rs: an in-memory transactional broker. A source window
  stages a serialized tab, a destination created as 'window-<token>'
  claims it (the token rides in the window label — the asset protocol
  404s on URL queries), and the source is acknowledged via a targeted
  'tab-transfer-claimed' event so it deletes its tab only after the
  hand-off is confirmed. Dirty content never touches disk or
  localStorage in transit.
- create_transfer_window: sync command (window creation needs the main
  thread on macOS) using the same builder chrome as the main window.
- File-open delivery picks the focused viewer window, else any viewer:
  the single-instance callback and macOS RunEvent::Opened previously
  hardcoded 'main' and silently dropped files once main was closed.
  Menu events now emit_to their window instead of broadcasting.
- File watchers are keyed per window label (one shared slot meant any
  window toggling auto-reload killed every other window's watcher) and
  'file-changed' targets the owning window only.
- Window-state snapshots are written through save/load/clear_window_state
  commands: setItem is an async message to the WebKit storage process
  and loses its flush race when the last window's close exits the
  process; an awaited invoke holds the close open until fs::write
  returns.
- Capabilities cover 'window-*' so secondary windows get the same
  permission set as main.
- tauri-plugin-window-state maps 'window-*' labels to one shared
  'secondary' entry; macOS secondaries opt into their shadow (main's
  shadow(false) is resurrected by the plugin's frame restore, a fresh
  secondary gets no restore and rendered shadowless).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TransferableTab deliberately differs from the persisted v2 snapshot:
the transfer payload MUST carry rawContent/originalContent/isDirty
(moving a dirty tab is the point) while the persistence snapshot must
NOT (content's sole authority is the disk file; a snapshot that
survives a power-off gap would resurrect stale content under a live
tab and re-arm the stale-buffer + auto-save hazard). A content-
carrying snapshot is safe here only because it spans ~a second inside
one process with the source tab alive until acknowledged.

validateTransferPayload is strict — every field type-checked, no
coercion, no defaults: a tab whose content fields are not strings
must never be constructed.

Arrival titling keeps the tab's identity: an untitled tab is
re-numbered only when the destination already has that exact title
(impossible for a fresh detach window; ready for a future
move-to-existing-window). Adds menu.moveToNewWindow to all 26
language tables and 16 node tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ab events

The menu item is disabled for the HOME tab and single-tab windows
(moving the only tab would just churn windows). Tab-strip context
menus previously used global emit(), which broadcasts in Tauri 2 —
'New File' from a context menu would create a tab in EVERY window;
they now emitTo their own window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Only the main window persists and restores the window-state
  snapshot: localStorage (and the state file) is one shared slot per
  origin, so every window restoring it duplicated the whole tab set
  into detached windows, and the last window closed overwrote everyone
  else's state. Secondary labels are per-session, so their snapshots
  could never be restored anyway — main remembers, secondaries are
  ephemeral, matching browser session-restore semantics.
- persistWindowState goes through the Rust write-through commands and
  drops the localStorage keys after the first write (read once for
  migration; a downgraded build starts fresh instead of misreading).
- A window whose label carries a transfer token claims its tab from
  the broker on startup; invalid payloads are rejected outright rather
  than building an empty-shell tab.
- handleDetach stages the snapshot, creates the window through Rust,
  and deletes the source tab only on claim acknowledgement; timeout or
  creation failure cancels and the tab stays. No canCloseTab() — moving
  preserves dirty state, movement is not closing. Guarded during a
  close-review walk. Detach previously passed only a file path in a
  URL, so dirty and untitled tabs could not move at all.
- Every per-window listener is window-bound (listen() receives global
  broadcasts AND targeted events, so this composes with the emit_to
  migration in any order).
- The reviewing window raises itself before its close-review dialogs:
  the walk's modals are in-app, and another window covering them made
  the close button look dead.
- File-load failures surface as a toast (TCC permission denials died
  silently in the console, leaving an inexplicably empty tab).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The old snapshot's cleanup previously ran only at close and only with
restore-on-reopen enabled, so users who disabled the setting kept the
stale localStorage copy forever. Migration now completes at startup:
restore, immediately persist through Rust (so a crash between steps
cannot lose the snapshot), then drop both localStorage keys
unconditionally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Startup deletes the localStorage keys after migrating, so their
presence means an older build wrote them since our last run — e.g.
during a downgrade period. Reading the Rust file first would restore
a stale pre-downgrade snapshot over the one the older build just
wrote. localStorage-if-present is therefore always at least as fresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hand-copied render/write-back/refresh trio at the claim site had
already drifted (missing the _lastRenderedRawContent marker). One shared
path means future pipeline changes reach the transfer flow for free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
send_markdown_path serves process-global state (argv plus the macOS
Opened-before-ready stash) and every window's init consumed it, so
each detached window re-opened the file the app was launched with.
Gate the consumer to the main window and make the stash a one-shot
take(). Escaped QA because every test launch was a bare terminal
start — launch mode (bare / argv file / Finder open) is a test
dimension on par with window count.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When Finder delivers a file, Finder itself is frontmost, so
is_focused() is false for every Markpad window and delivery degraded
to arbitrary window-map order — users could not predict where a
double-clicked file would open. Track the last-focused viewer label
and use it as the middle rung: focused viewer, else last-focused
still-alive viewer, else any.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@PathGao

PathGao commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Closing as duplicate: this rebased fix branch now updates PR #214 directly, so #214 contains the same head commit.

@PathGao PathGao closed this Jul 24, 2026
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