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
|
|
@ -6,11 +6,13 @@ mod hosts;
|
|||
mod mcp;
|
||||
mod mcp_policy;
|
||||
mod pty;
|
||||
mod window_state;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::mcp::{McpServerHandle, McpState, PendingActions};
|
||||
use crate::pty::PtyManager;
|
||||
use crate::window_state::{PendingInits, WindowsState, MAIN_WINDOW_LABEL};
|
||||
|
||||
pub fn run() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
|
|
@ -40,6 +42,15 @@ pub fn run() {
|
|||
// Pending action registry — separate managed state so mcp_action_reply can
|
||||
// grab it without needing to lock McpState or reach into TileService.
|
||||
let pending_actions: Arc<PendingActions> = Arc::new(PendingActions::default());
|
||||
// Cross-window workspace aggregator: every window pushes its tab list
|
||||
// here; backend debounces + writes the merged envelope to workspace.json.
|
||||
let windows_state: Arc<WindowsState> = Arc::new(WindowsState::default());
|
||||
// Pane-transfer pending-init registry: source window stashes a payload
|
||||
// keyed by the new window's label; target window pulls it during mount.
|
||||
let pending_inits: Arc<PendingInits> = Arc::new(PendingInits::default());
|
||||
|
||||
let windows_state_for_event = Arc::clone(&windows_state);
|
||||
let pending_inits_for_event = Arc::clone(&pending_inits);
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
|
|
@ -48,12 +59,34 @@ pub fn run() {
|
|||
.manage(mcp_state)
|
||||
.manage(McpServerHandle::default())
|
||||
.manage(pending_actions)
|
||||
.manage(windows_state)
|
||||
.manage(pending_inits)
|
||||
.on_window_event(move |window, event| {
|
||||
// When a non-main window closes, drop its workspaces from the
|
||||
// aggregator AND any unconsumed pending-init payload so neither
|
||||
// resurrect on next launch. Matches Chrome-style "closing a
|
||||
// detached window discards its tabs" intent.
|
||||
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
||||
let label = window.label().to_string();
|
||||
if label != MAIN_WINDOW_LABEL {
|
||||
pending_inits_for_event.by_label.lock().remove(&label);
|
||||
windows_state_for_event
|
||||
.forget(window.app_handle().clone(), &label);
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::list_distros,
|
||||
commands::spawn_pane,
|
||||
commands::write_to_pane,
|
||||
commands::resize_pane,
|
||||
commands::kill_pane,
|
||||
commands::mark_pane_transferring,
|
||||
commands::claim_pane,
|
||||
commands::get_pane_ring,
|
||||
commands::create_pane_window,
|
||||
commands::take_pending_window_init,
|
||||
commands::push_window_workspaces,
|
||||
commands::save_workspace,
|
||||
commands::load_workspace,
|
||||
commands::list_ssh_hosts,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue