Tiling multi-terminal manager for WSL
Find a file
megaproxy e9015b2790 Restore workarounds; close via renderKey bump (kills all PTYs)
Agents investigated the close-button-stuck issue and recommended
removing all DOM-direct workarounds, hypothesising they were causing
the bug. Tested empirically: removing the workarounds breaks
EVERY interactive feature (no border, no broadcast, no resize,
no close). So Svelte 5 prop reactivity through the recursive Pane
chain is genuinely broken in this app — workarounds are required.

Restored workaround set:
- App.svelte polling loop: focus detection + active-class + broadcast
  class sync via DOM API every 250ms
- SplitNode.svelte drag: rAF-throttled direct .side flex update
- handleClose: bump renderKey to force full Pane remount. This kills
  every other pane's PTY (cost) but is the only reliable way to make
  the closed pane actually disappear AND avoid the "zombie split"
  click-intercept bug from the previous DOM-hide approach.

The underlying Svelte 5 issue remains an open question — the next
thing to try is the context+getter pattern (`setContext('active',
{ get id() { return activeLeafId } })` in App, `getContext('active')`
in LeafPane) which Agent 1's research found is the documented
escape hatch for deep reactive prop chains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:32:04 +01:00
scripts M5 ship infrastructure: icon, version, release script, README 2026-05-22 13:38:29 +01:00
src Restore workarounds; close via renderKey bump (kills all PTYs) 2026-05-22 17:32:04 +01:00
src-tauri Pick up Cargo.lock version bump from pnpm tauri build 2026-05-22 13:46:27 +01:00
.gitignore Remove accidentally-committed dev-session screenshots/scripts 2026-05-22 14:37:00 +01:00
CLAUDE.md Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00
index.html Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00
memory.md M5 ship infrastructure: icon, version, release script, README 2026-05-22 13:38:29 +01:00
package.json M5 ship infrastructure: icon, version, release script, README 2026-05-22 13:38:29 +01:00
pnpm-lock.yaml Add vitest + 43 unit tests for tree.ts 2026-05-22 13:28:02 +01:00
pnpm-workspace.yaml Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00
README.md Slim README for public visibility 2026-05-22 13:50:02 +01:00
svelte.config.js Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00
tsconfig.json Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00
tsconfig.node.json Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00
vite.config.ts Add vitest + 43 unit tests for tree.ts 2026-05-22 13:28:02 +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
  • 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

  • Split panes in the pane toolbar splits right, splits down. The new pane inherits the parent's distro + cwd.
  • 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).
  • Broadcast — toggle 📡 on two or more panes (orange border). Typing in any of them mirrors to all.
  • Preset layouts — titlebar buttons: 1 / 2H / 3H / 2V / 2×2. Confirms before replacing a multi-pane layout.
  • Active pane — click any pane → blue border + keyboard focus.
  • Jump to paneCtrl+K opens a fuzzy picker over label / distro / cwd. ↑/↓ to navigate, Enter to focus, Esc to close.
  • Idle toasts — top-right notification when a pane goes quiet for 5 s. Useful for "I started a long task; tell me when it's done."

Layout + per-pane settings auto-save to %APPDATA%\com.megaproxy.tiletopia\workspace.json (debounced 500 ms).

Stack

  • Tauri 2 (Rust backend, WebView2 frontend) — small bundle, native NSIS installer.
  • Svelte 5 + TypeScript + Vite + pnpm.
  • 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         # svelte-check

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, etc.) live in tree.ts; the rendering chain (Pane.svelteSplitNode.svelte / LeafPane.svelte) is thin.
  • Orchestration — broadcast routing, idle detection, palette, active-pane focus all live in App.svelte and reach the panes via a PaneOps bundle (src/lib/layout/ops.ts) drilled through the Pane chain.

License

No formal license yet. Public for inspection and personal use; if you want to redistribute, open an issue and ask.