Reverts in one combined commit: - |
||
|---|---|---|
| scripts | ||
| src | ||
| src-tauri | ||
| .gitignore | ||
| CLAUDE.md | ||
| index.html | ||
| memory.md | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vite.config.ts | ||
tiletopia
A Windows desktop app for running and arranging many WSL terminals at once. Built primarily for managing multiple claude sessions across projects in parallel; works for any multi-shell workflow.
- Tiling layout — recursive splits, draggable dividers, preset layouts (single / 2-col / 3-col / 2-row / 2×2)
- Per-pane distro + cwd + label, persisted across restarts
- Broadcast input to a group of panes (per-pane 📡 chip, or global toggle in the titlebar)
- Idle-detection toasts when a pane goes quiet
- Ctrl+K palette to fuzzy-jump between panes
Install
- Download the latest
tiletopia_<version>_x64-setup.exefrom the releases page. - Run it. Windows SmartScreen will warn "unrecognized publisher" — it's not code-signed. More info → Run anyway.
- Launch tiletopia from the Start menu. A window opens with one terminal pane bound to your default WSL distro.
Requirements
- Windows 10/11 with WebView2 Runtime (preinstalled on Win11).
- At least one WSL distro registered (
wsl -l -v).
Using it
Shortcuts and tips
Keyboard shortcuts
Layout
| Key | Action |
|---|---|
Ctrl+Shift+E |
Split active pane to the right |
Ctrl+Shift+O |
Split active pane downward |
Ctrl+Shift+W |
Close active pane |
Ctrl+Shift+P |
Promote active pane out one level (turns a nested pane into a full row/column; self-inverse) |
Navigation
| Key | Action |
|---|---|
Ctrl+K |
Open jump-to-pane palette |
Ctrl+Shift+← / → / ↑ / ↓ |
Focus neighbour pane in that direction |
Broadcast
| Key | Action |
|---|---|
Ctrl+Shift+B |
Toggle broadcast on active pane |
Ctrl+Shift+Alt+B |
Toggle broadcast on ALL panes (same as titlebar 📡) |
Font size
| Key | Action |
|---|---|
Ctrl+= / Ctrl+- / Ctrl+0 |
Zoom active pane in / out / reset |
Ctrl+Shift+= / Ctrl+Shift+- / Ctrl+Shift+0 |
Same, applied to every pane |
Terminal
| Key | Action |
|---|---|
Ctrl+Shift+C / Ctrl+Shift+V |
Copy selection / paste in terminal |
Help
| Key | Action |
|---|---|
F1 |
Show this help overlay |
Tips
- Per-pane shell picker — Click the distro chip in any pane's toolbar to switch between WSL distros, PowerShell, or a saved SSH host. The pane respawns with the new shell.
- 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.
- 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.
- 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.
Shortcuts work while a terminal is focused — we capture the key before xterm.js sees it. They don't fire while you're typing into a label edit or the palette input, so those still work normally. Ctrl and ⌘ (Cmd) are interchangeable.
Font size persists per pane in workspace.json, so a zoomed pane stays zoomed across restarts.
The shortcut tables and tips above are generated from
src/lib/shortcuts.ts(the single source of truth shared with the in-app help overlay). To change them, edit that file and runpnpm gen:readme.
Mouse + toolbar
- Split panes —
⇥in the pane toolbar splits right,⇣splits down. The new pane inherits the parent's distro; the cwd defaults to~in the WSL distro. - Close pane —
×. The sibling expands to fill. - Rename pane — click the label in the toolbar, type,
Enter(Escto cancel). - Change distro — click the small
Ubuntu ▾chip; pick a distro from the popover. The pane respawns (old shell is killed). - Swap panes — click-and-drag a pane's toolbar onto another pane. The two leaves trade tree slots; both shells stay alive, both scrollbacks intact.
- Active pane — click any pane → blue border + keyboard focus.
- Resize — drag the gutter between two panes. A 180 px minimum is enforced on both sides.
Broadcast, idle, presets
- Broadcast — toggle
📡on two or more panes (orange border). Typing in any of them mirrors to the rest. The titlebar📡 all off/📡 all on/📡 N/Mbutton flips the whole group at once. - Idle indicator — when a pane goes quiet for 5 s, its border turns red and its "alive" toolbar tag swaps to red "idle". The titlebar also shows an
N idlecount. Clears the moment new output arrives. Active + broadcasting borders take precedence so the focus indicator isn't masked. - Preset layouts — titlebar buttons
1/2H/3H/2V/2×2. Existing panes are spliced into the new shape in order (ids, shells, scrollback preserved); extra slots spawn fresh shells. Only prompts if the preset has fewer slots than you currently have panes (those overflow shells get killed).
Layout + per-pane settings auto-save to %APPDATA%\com.megaproxy.tiletopia\workspace.json (debounced 500 ms).
MCP server (Claude can drive the workspace)
The titlebar 🤖 button opens a small panel that starts an MCP (Model Context Protocol) server. A Claude session — running anywhere reachable from the host, including inside one of tiletopia's own panes — can connect to it, read scrollback, wait for commands to settle, and inspect the layout. v1 is read-only: no spawning, no keystroke injection, no host editing.
- Off by default. Click the button, hit Server: ON to start. The panel shows the URL + bearer token and a ready-to-paste Claude config snippet. Both port and token persist across restarts (saved to
%APPDATA%\com.megaproxy.tiletopia\mcp.json); use Regenerate in the panel if the token leaks. - Default-deny per pane. Toggle the 🤖 chip in any pane's toolbar to allow MCP to see it. Panes without the chip on are invisible to the server.
- Saved SSH passwords are never exposed through the MCP surface.
- Bound to all interfaces (
0.0.0.0). The bearer token is the only auth — don't enable the server on an untrusted network.
Claude Desktop setup (one-click via .mcpb bundle — recommended)
The MCP panel has a Download .mcpb button that fetches a packaged Claude Desktop extension (an .mcpb file). Drag it into Claude Desktop's Settings → Extensions pane and Claude will auto-discover tiletopia — no config editing, no copy-pasting tokens.
The bundle ships a tiny wrapper that reads your per-install bearer token straight from %APPDATA%\com.megaproxy.tiletopia\mcp.json at launch, so:
- It carries no secrets — the same file works for every tiletopia install.
- Token regeneration in the panel keeps working transparently; the next time Claude Desktop launches the extension, it'll pick up the new token.
- Requires
npx(Node 18+) on PATH because the wrapper still talks to tiletopia throughmcp-remote(same reason as the manual recipe below).
You can also rebuild the bundle from source:
pnpm run build:mcpb # writes dist-mcpb/tiletopia.mcpb
Claude Code setup (via mcp-remote stdio shim — fallback / manual recipe)
Claude Code (the terminal CLI) doesn't accept .mcpb bundles yet, and its HTTP-MCP client currently tries OAuth discovery and ignores static headers auth (Anthropic #17152, #46879). The mcp-remote stdio shim transparently proxies the HTTP endpoint with the bearer header attached, sidestepping the OAuth flow.
The panel's config snippet uses this shim by default — paste it into your project's .mcp.json:
{
"mcpServers": {
"tiletopia": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"http://127.0.0.1:47821/mcp",
"--allow-http",
"--header", "Authorization: Bearer <token-from-panel>"
]
}
}
}
Requires npx (Node 18+) on the client side. Other MCP clients that handle static bearer auth correctly can skip the shim and connect directly to the URL + token shown in the panel.
WSL connectivity
When Claude runs inside WSL, swap 127.0.0.1 for the WSL gateway IP (ip route show default | awk '{print $3}' inside WSL — note that this changes after each WSL restart) or enable mirrored networking (networkingMode=mirrored in %UserProfile%\.wslconfig then wsl --shutdown; Win 11 22H2+). Allow the port through Windows Defender Firewall once — elevated PowerShell:
New-NetFirewallRule -DisplayName "tiletopia MCP" -Direction Inbound `
-Action Allow -Protocol TCP -LocalPort 47821 -Profile Any
Stack
- Tauri 2 (Rust backend, WebView2 frontend) — small bundle, native NSIS installer.
- React 18 + TypeScript + Vite + pnpm. (The v0.1.0 release was Svelte 5; v0.2.0+ is React after a ground-up rewrite of the frontend. Same data model, same backend, more reliable reactivity through the recursive Pane chain. The Svelte version is preserved on the
svelte-archivebranch.) - xterm.js +
@xterm/addon-fitfor terminal rendering. portable-pty(Rust) spawningwsl.exe -d <distro>PTYs.
Build from source
This targets Windows; the Rust toolchain runs on the Windows host. Prereqs per Tauri docs: MSVC ("C++ build tools" workload), Rust, Node 20+, pnpm (corepack use pnpm@latest), at least one WSL distro.
git clone https://git.rdx4.com/megaproxy/tiletopia.git
cd tiletopia
pnpm install
pnpm tauri dev # iterate
pnpm tauri build # NSIS installer at src-tauri\target\release\bundle\nsis\
Keep the source on a Windows-native drive (e.g. C:\ or D:\). Running pnpm against a \\wsl.localhost\... UNC path crashes pnpm 11.x inside isDriveExFat (with a misleading error from the crashing hint formatter).
Run the tests
pnpm test # vitest, 43 cases on the layout tree
pnpm test:watch
pnpm check # tsc --noEmit (strict TypeScript pass)
pnpm build # tsc -b && vite build — full production frontend bundle
The test suite covers the pure helpers in src/lib/layout/tree.ts. UI behavior, broadcast routing, and Tauri integration are manually tested.
Architecture
- Backend (
src-tauri/src/pty.rs):PtyManagerholdingMutex<HashMap<PaneId, PaneHandle>>ofportable-ptychildren. Each spawned pane gets a background reader thread that emitspane://{id}/dataevents (base64 byte chunks). Counterparts:write_to_pane/resize_pane/kill_pane. Workspace persistence viasave_workspace/load_workspacewrites toapp.path().app_config_dir()with atomic tmp + rename. - Layout (
src/lib/layout/tree.ts): binary tree of splits.HSplit | VSplitinternal nodes with a ratio,Leafat the bottom — same model as i3 / tmux / Zellij. Adaptive resize falls out of mutating one parent ratio. Pure helpers (splitLeaf,closeLeaf,changeDistro,setAllBroadcast, etc.) live intree.tswith 43 vitest cases; the rendering chain (Pane.tsx→SplitNode.tsx/LeafPane.tsx) is thin. - Orchestration — broadcast routing, idle detection, palette, active-pane focus all live in
App.tsx. Shared state and operations reach descendants through a React Context (src/lib/layout/orchestration.tsx), so each LeafPane readsactiveLeafId,distros, and the tree-mutation methods directly viauseOrchestration()— no prop drilling through the recursive Pane chain.
License
No formal license yet. Public for inspection and personal use; if you want to redistribute, open an issue and ask.