Tiling multi-terminal manager for WSL
Find a file
megaproxy 4bf55782da CLAUDE.md: React 18, not Svelte 5
Stack line was stale since the React migration in commit 774b863 (0.2.0).
Also updates the `pnpm check` parenthetical from svelte-check to
tsc --noEmit, which is what the script actually runs now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:13:32 +01:00
scripts M5 ship infrastructure: icon, version, release script, README 2026-05-22 13:38:29 +01:00
src MCP v2 PR-4: add_host + delete_host + extraArgs sanitiser + third SSH safeguard 2026-05-26 16:04:14 +01:00
src-tauri Fix hard-deny enforcement gaps surfaced by PR-4 test re-enable 2026-05-26 16:05:31 +01:00
.gitignore gitignore: cargo-test.log 2026-05-26 16:06:08 +01:00
CLAUDE.md CLAUDE.md: React 18, not Svelte 5 2026-05-26 16:13:32 +01:00
index.html Migrate frontend from Svelte 5 to React 18 2026-05-22 18:05:05 +01:00
memory.md Fix hard-deny enforcement gaps surfaced by PR-4 test re-enable 2026-05-26 16:05:31 +01:00
package.json Make URLs in terminal output clickable via xterm web-links + tauri-plugin-opener 2026-05-25 19:13:08 +01:00
pnpm-lock.yaml Make URLs in terminal output clickable via xterm web-links + tauri-plugin-opener 2026-05-25 19:13:08 +01:00
pnpm-workspace.yaml Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00
README.md MCP: persistent port/token + mcp-remote shim recipe for Claude Code 2026-05-26 11:05:13 +01:00
tsconfig.app.json Migrate frontend from Svelte 5 to React 18 2026-05-22 18:05:05 +01:00
tsconfig.json Migrate frontend from Svelte 5 to React 18 2026-05-22 18:05:05 +01:00
tsconfig.node.json Migrate frontend from Svelte 5 to React 18 2026-05-22 18:05:05 +01:00
vite.config.ts Migrate frontend from Svelte 5 to React 18 2026-05-22 18:05:05 +01:00

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

  1. Download the latest tiletopia_<version>_x64-setup.exe from the releases page.
  2. Run it. Windows SmartScreen will warn "unrecognized publisher" — it's not code-signed. More info → Run anyway.
  3. 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

Keyboard shortcuts

Key Action
Ctrl+K open the jump-to-pane palette (fuzzy match over label / distro / cwd; / to move, Enter to focus, Esc to close)
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 (e.g. nested-right c becomes a full-width bottom row). Self-inverse.
Ctrl+Shift+B toggle broadcast on active pane
Ctrl+Shift+Alt+B toggle broadcast on ALL panes (same as the titlebar 📡 button)
Ctrl+Shift+← / / / focus neighbour pane in that direction
Ctrl+= / Ctrl+- / Ctrl+0 zoom the active pane in / out / back to default
Ctrl+Shift+= / Ctrl+Shift+- / Ctrl+Shift+0 same, applied to every pane (shift = "to all")

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.

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 (Esc to 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/M button 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 idle count. 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 Code setup (via mcp-remote stdio shim)

Claude Code's 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-archive branch.)
  • xterm.js + @xterm/addon-fit for terminal rendering.
  • portable-pty (Rust) spawning wsl.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): PtyManager holding Mutex<HashMap<PaneId, PaneHandle>> of portable-pty children. Each spawned pane gets a background reader thread that emits pane://{id}/data events (base64 byte chunks). Counterparts: write_to_pane / resize_pane / kill_pane. Workspace persistence via save_workspace / load_workspace writes to app.path().app_config_dir() with atomic tmp + rename.
  • Layout (src/lib/layout/tree.ts): binary tree of splits. HSplit | VSplit internal nodes with a ratio, Leaf at 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 in tree.ts with 43 vitest cases; the rendering chain (Pane.tsxSplitNode.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 reads activeLeafId, distros, and the tree-mutation methods directly via useOrchestration() — 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.