Phase 2: drag-/right-click-a-pane-to-new-window
Right-click any pane's title bar → "Move to new window" pops it into a
fresh tiletopia window with its PTY intact. Same Tauri process; the
PtyManager is shared, so the existing PaneId stays valid and Tauri 2's
process-wide event routing keeps pane://{id}/data flowing into the new
window's XtermPane.
Mechanism (Rust-side, plan-agent's main correction over my draft):
- pty.rs: PtyManager.transferring is a per-pane refcount; kill_pane
becomes a no-op while it's >0. Source window's React unmount calls
kill_pane → silently dropped while in flight; target window's
claim_pane decrements after it has subscribed.
- window_state.rs: per-window workspaces snapshot map +
debounced-by-tokio aggregate save. Each window pushes its tabs via
push_window_workspaces; backend writes the merged
{ version: 2, workspaces: [...] } envelope. Non-main windows have
their entries dropped on CloseRequested so closing a detached window
discards its tabs (Chrome-style).
- commands: mark_pane_transferring, claim_pane, get_pane_ring (base64
scrollback ring snapshot), create_pane_window, take_pending_window_init,
push_window_workspaces.
Frontend:
- XtermPane gets `existingPaneId?: PaneId`: skip spawn, replay ring
snapshot via term.write before attaching the live data listener,
resize PTY to this window's grid, claim_pane. Scrollback replay was
the plan agent's other ship-in-v1 call — without it a transferred
Claude session looks blank until next prompt repaint.
- LeafPane: onContextMenu opens a fixed-positioned "Move to new
window" popover. Esc / outside-click dismiss.
- orchestration adds moveToNewWindow + getInitialPaneIdFor; App owns a
one-shot transferredPaneIdsRef cleared in registerPaneId.
- App mount branches on getCurrentWebviewWindow().label: main loads
workspace.json as before; non-main calls take_pending_window_init
and builds a singleton workspace around the adopted leaf.
- MCP mirror + onMcpRequest only run in main (paneIdByLeafRef is per-
window; Claude sees the main window's current tab as the single
workspace surface).
pnpm check (tsc -b) clean. 79/79 vitest pass. Rust side authored in
WSL; cargo build needs verification on Windows host before this is
runnable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1a035ad0a6
commit
8ad51787fc
12 changed files with 797 additions and 48 deletions
47
src/ipc.ts
47
src/ipc.ts
|
|
@ -53,6 +53,53 @@ export const resizePane = (id: PaneId, cols: number, rows: number): Promise<void
|
|||
|
||||
export const killPane = (id: PaneId): Promise<void> => invoke("kill_pane", { id });
|
||||
|
||||
/** Increment the "do not kill" transfer refcount for a pane. Source window
|
||||
* calls this BEFORE removing the leaf from its tree so the unmount-driven
|
||||
* kill_pane on the source becomes a no-op until the target window's
|
||||
* XtermPane has claimed it. */
|
||||
export const markPaneTransferring = (id: PaneId): Promise<void> =>
|
||||
invoke("mark_pane_transferring", { id });
|
||||
|
||||
/** Decrement the transfer refcount. Target window's XtermPane calls this
|
||||
* after subscribing to pane://{id}/data and replaying the ring snapshot. */
|
||||
export const claimPane = (id: PaneId): Promise<void> =>
|
||||
invoke("claim_pane", { id });
|
||||
|
||||
/** Snapshot of the per-pane scrollback ring as base64. Target window's
|
||||
* XtermPane writes it into xterm.js before attaching the live data
|
||||
* listener so a transferred pane doesn't open blank. */
|
||||
export const getPaneRing = (id: PaneId): Promise<string> =>
|
||||
invoke("get_pane_ring", { id });
|
||||
|
||||
// ---- multi-window pane transfer -------------------------------------------
|
||||
|
||||
export interface PendingInit {
|
||||
leafJson: string;
|
||||
paneId: PaneId;
|
||||
workspaceName: string;
|
||||
}
|
||||
|
||||
/** Open a new window and stash the pending-init payload keyed by the new
|
||||
* window's label. Returns the new label. */
|
||||
export const createPaneWindow = (payload: PendingInit): Promise<string> =>
|
||||
invoke("create_pane_window", { payload });
|
||||
|
||||
/** Read and remove the pending-init for the current window. Null when there
|
||||
* is no pending payload (main window startup, or this call already
|
||||
* consumed it). */
|
||||
export const takePendingWindowInit = (
|
||||
label: string,
|
||||
): Promise<PendingInit | null> =>
|
||||
invoke("take_pending_window_init", { label });
|
||||
|
||||
/** Push this window's workspaces snapshot to the backend aggregator. The
|
||||
* backend debounces and writes the merged envelope to workspace.json. */
|
||||
export const pushWindowWorkspaces = (
|
||||
label: string,
|
||||
workspacesJson: string,
|
||||
): Promise<void> =>
|
||||
invoke("push_window_workspaces", { label, workspacesJson });
|
||||
|
||||
export const onPaneData = (
|
||||
id: PaneId,
|
||||
cb: (b64: string) => void,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue