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
|
|
@ -109,6 +109,16 @@ struct PaneHandle {
|
|||
pub struct PtyManager {
|
||||
panes: Mutex<HashMap<PaneId, PaneHandle>>,
|
||||
next_id: AtomicU64,
|
||||
/// Per-pane "this PTY is mid-transfer between windows; do not kill it
|
||||
/// even if some window's XtermPane unmounts" refcount. Incremented by
|
||||
/// {@link mark_transferring} when a transfer begins; decremented by
|
||||
/// {@link claim} when the target window finishes mounting. While >0,
|
||||
/// {@link kill} is a no-op for that id.
|
||||
///
|
||||
/// Refcount (vs. plain flag) so concurrent transfers — or the rare
|
||||
/// case where a transfer is retried before the previous one fully
|
||||
/// releases — don't drop the suppression early.
|
||||
transferring: Mutex<HashMap<PaneId, u32>>,
|
||||
}
|
||||
|
||||
impl PtyManager {
|
||||
|
|
@ -116,6 +126,27 @@ impl PtyManager {
|
|||
Self {
|
||||
panes: Mutex::new(HashMap::new()),
|
||||
next_id: AtomicU64::new(1),
|
||||
transferring: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Bump the transferring refcount for a pane. While >0, {@link kill} is
|
||||
/// a no-op so the source window's React unmount-cleanup can't tear
|
||||
/// down the PTY mid-transfer.
|
||||
pub fn mark_transferring(&self, id: PaneId) {
|
||||
*self.transferring.lock().entry(id).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
/// Decrement the transferring refcount. When it reaches zero the entry
|
||||
/// is removed and {@link kill} can act on this pane again.
|
||||
pub fn claim(&self, id: PaneId) {
|
||||
let mut map = self.transferring.lock();
|
||||
if let Some(rc) = map.get_mut(&id) {
|
||||
if *rc > 1 {
|
||||
*rc -= 1;
|
||||
} else {
|
||||
map.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,6 +289,14 @@ impl PtyManager {
|
|||
}
|
||||
|
||||
pub fn kill(&self, id: PaneId) -> Result<()> {
|
||||
// If a transfer is in flight for this pane, suppress the kill so
|
||||
// the source window's unmount-cleanup can't race the target
|
||||
// window's mount-claim. The target's claim() will decrement the
|
||||
// refcount; the next caller of kill() (if any) will actually kill.
|
||||
if self.transferring.lock().contains_key(&id) {
|
||||
tracing::debug!("pty kill suppressed during transfer for pane {id}");
|
||||
return Ok(());
|
||||
}
|
||||
let mut panes = self.panes.lock();
|
||||
if let Some(mut pane) = panes.remove(&id) {
|
||||
// Best-effort: ask the child to die. Dropping `master` after this
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue