What the user saw: dragging a gutter filled the affected panes with many overlapping bash prompts, some corrupted mid-print (megaproxy@DESKTOP-megaproxy@DESKTOP-SSAQG5 etc). Root cause: every resizePane() call sends SIGWINCH to the shell, which makes bash redraw its prompt. The previous fix coalesced the local xterm fit() into one per rAF, but still fired resizePane on every rAF — 60+ SIGWINCHes per second during a drag, faster than bash can finish one prompt redraw before the next interrupts it. Fix: separate the two concerns. fit() + term.refresh() still run every rAF (the visual must stay smooth). But resizePane() is debounced to fire 150 ms after the LAST rAF — i.e. only when you stop dragging — so bash gets one clean SIGWINCH at the final size and produces a single tidy prompt redraw. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| 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
- 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. The titlebar📡 all off/📡 all on/📡 N/Mbutton flips the whole group at once. - 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 pane —
Ctrl+Kopens a fuzzy picker over label / distro / cwd. ↑/↓ to navigate, Enter to focus, Esc to close.
Keyboard shortcuts
| Key | Action |
|---|---|
Ctrl+K |
open the jump-to-pane palette |
Ctrl+Shift+E |
split active pane to the right |
Ctrl+Shift+O |
split active pane downward |
Ctrl+Shift+W |
close active pane |
Ctrl+Shift+B |
toggle broadcast on active pane |
Ctrl+Shift+Alt+B |
toggle broadcast on ALL panes (titlebar 📡) |
Ctrl+Shift+←/→/↑/↓ |
focus neighbour pane in that direction |
Shortcuts work while a terminal is focused (we capture before xterm.js sees the key). They DON'T fire while you're typing into a label edit or the palette input, so those still work normally.
- 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.
- 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.