diff --git a/README.md b/README.md index d6e09c9..c3038c5 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ A Windows desktop app for running and arranging many WSL terminals at once. Buil | Key | Action | |---|---| | `Right-click pane toolbar → Move to new window` | Pop the active pane into a fresh tiletopia window (PTY survives the move; scrollback ring replays) | +| `Drag pane toolbar past the window edge` | Same as the right-click action — release the drag well outside the window to detach into a new window | **Navigation** @@ -92,7 +93,7 @@ A Windows desktop app for running and arranging many WSL terminals at once. Buil - **SSH host manager** — Titlebar 🔑 SSH hosts opens the manager. Add hostname / user / port / identity file / jump host / extra ssh args. Saved hosts appear in every pane's dropdown. - **Saved passwords** — Optionally save a host's password — stored in Windows Credential Manager (DPAPI-encrypted), never written to hosts.json. When ssh prompts on connect it's typed automatically. Hosts with a saved password show 🔒 in the list. - **Clickable links** — http and https URLs in terminal output get underlined and open in your default browser on click. -- **Drag pane headers to swap** — Grab a pane's title bar and drag it onto another pane to swap their tree positions. Useful for reorganizing without keyboard. +- **Drag pane headers to swap or detach** — Grab a pane's title bar and drag onto another pane to swap their tree positions. Drag well outside the window edge (more than ~60px past) and release to detach the pane into a new window — same mechanism as the right-click 'Move to new window' action, PTY stays alive. - **Workspace persistence** — Layout, labels, distro choices, and SSH hosts auto-save to %APPDATA%/com.megaproxy.tiletopia (debounced 500ms). Closed panes don't come back — only the structure is restored, shells spawn fresh on next launch. - **Tabs (workspaces)** — Each tab is an independent tile layout — useful for keeping one tab per project. PTYs in non-active tabs keep running (a Claude session in tab A keeps going while you work in tab B). New tab starts with one default-shell pane; close confirms when the tab has live panes. Tabs auto-save to the same workspace.json. - **MCP server (let Claude drive the workspace)** — Titlebar 🤖 opens the MCP control panel. Start the server, then for Claude Desktop click 'Download .mcpb' and drag the file into Settings → Extensions — zero-config because the bundle reads your bearer token from %APPDATA% at launch (no copy-paste, survives token rotation). For Claude Code (terminal CLI) use the fallback snippet in the panel: it wires npx mcp-remote as a stdio shim because Claude Code's HTTP-MCP client ignores static bearer auth and tries OAuth instead. URL + token persist across restarts; Regenerate the token in the panel if it leaks. Default-deny per pane: toggle 🤖 on each pane's toolbar to expose it to MCP. diff --git a/src/lib/layout/LeafPane.tsx b/src/lib/layout/LeafPane.tsx index ef1f8c2..7fd9ee7 100644 --- a/src/lib/layout/LeafPane.tsx +++ b/src/lib/layout/LeafPane.tsx @@ -268,6 +268,12 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) { [orch.beginHeaderDrag, orch.setHeaderDragOver, leaf.id], ); + /** How far past a viewport edge the cursor must travel before a release + * is treated as "drag pane out of window" instead of "drop on empty + * space inside this window". Picked so an accidental release on the OS + * titlebar (~30px tall) stays inside the threshold. */ + const PANE_DRAG_OUT_MARGIN = 60; + const onToolbarPointerUp = useCallback( (e: ReactPointerEvent) => { const st = dragStartRef.current; @@ -275,12 +281,26 @@ export default function LeafPane({ leaf }: { leaf: LeafNode }) { (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); const wasDragging = st.dragging; dragStartRef.current = null; - if (wasDragging) { - document.body.style.cursor = ""; + if (!wasDragging) return; + document.body.style.cursor = ""; + + const releasedFarOutside = + e.clientX < -PANE_DRAG_OUT_MARGIN || + e.clientX > window.innerWidth + PANE_DRAG_OUT_MARGIN || + e.clientY < -PANE_DRAG_OUT_MARGIN || + e.clientY > window.innerHeight + PANE_DRAG_OUT_MARGIN; + + if (releasedFarOutside) { + // Cancel any in-flight swap state without committing, then pop + // this pane into a fresh window. moveToNewWindow handles the + // PTY-handoff + closeLeaf in the source. + orch.endHeaderDrag(false); + orch.moveToNewWindow(leaf.id); + } else { orch.endHeaderDrag(true); } }, - [orch.endHeaderDrag], + [orch.endHeaderDrag, orch.moveToNewWindow, leaf.id], ); const onToolbarPointerCancel = useCallback( diff --git a/src/lib/shortcuts.ts b/src/lib/shortcuts.ts index 0222f14..5b1b789 100644 --- a/src/lib/shortcuts.ts +++ b/src/lib/shortcuts.ts @@ -53,6 +53,11 @@ export const SHORTCUT_SECTIONS: ShortcutSection[] = [ description: "Pop the active pane into a fresh tiletopia window (PTY survives the move; scrollback ring replays)", }, + { + keys: "Drag pane toolbar past the window edge", + description: + "Same as the right-click action — release the drag well outside the window to detach into a new window", + }, ], }, { @@ -126,8 +131,8 @@ export const TIPS: TipSpec[] = [ body: "http and https URLs in terminal output get underlined and open in your default browser on click.", }, { - title: "Drag pane headers to swap", - body: "Grab a pane's title bar and drag it onto another pane to swap their tree positions. Useful for reorganizing without keyboard.", + title: "Drag pane headers to swap or detach", + body: "Grab a pane's title bar and drag onto another pane to swap their tree positions. Drag well outside the window edge (more than ~60px past) and release to detach the pane into a new window — same mechanism as the right-click 'Move to new window' action, PTY stays alive.", }, { title: "Workspace persistence",