Tiling multi-terminal manager for WSL
Find a file
megaproxy 547b47ded4 Fix M4 reactivity bugs: active border, Ctrl+K, diagnostics
- Drill activeLeafId as a separate prop through Pane -> SplitNode ->
  LeafPane instead of bundling it into the \$derived ops object.
  Passing activeLeafId via ops caused subsequent focus changes to
  not propagate to children (LeafPane's active = \$derived(...) wasn't
  re-evaluating when ops's identity changed). Drilling sidesteps any
  prop-as-derived-object reactivity quirks.
- Ctrl+K listener now uses capture phase so it wins over xterm.js's
  keydown handler inside the focused terminal.
- Bump active/broadcasting borders to 2px and brighter colors so the
  visual change is unmissable.
- Add a 🔔 test-toast button in the titlebar to verify the
  notification pipeline independently of idle detection.
- Sprinkle console.log diagnostics through the active/broadcast/
  idle/notify flows so we can pinpoint any remaining issues from
  devtools next time something looks off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:20:11 +01:00
src Fix M4 reactivity bugs: active border, Ctrl+K, diagnostics 2026-05-22 13:20:11 +01:00
src-tauri Add M3: APPDATA persistence + presets + per-pane distro/label 2026-05-22 12:55:46 +01:00
.gitignore Add src-tauri/gen/ to gitignore 2026-05-22 12:32:35 +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 Add M4 orchestration: broadcast, idle notifications, palette 2026-05-22 13:08:40 +01:00
package.json Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00
pnpm-lock.yaml Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00
pnpm-workspace.yaml Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00
README.md Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +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 Initial scaffold from M1 spike (tiletopia) 2026-05-22 12:31:29 +01:00

tiletopia

A Windows desktop app for running and arranging many WSL terminals at once. Built primarily to manage multiple claude sessions across projects in parallel, but works for any multi-shell workflow.

Status: early — single-pane M1 works. Tiling layout (M2), workspace persistence (M3), and cross-pane orchestration (M4) are the next milestones. See memory.md.

Stack

  • Tauri 2 (Rust backend, WebView2 frontend) — small bundle, native Windows installer via NSIS.
  • Svelte 5 + TypeScript + Vite + pnpm.
  • xterm.js + @xterm/addon-fit for terminal rendering.
  • portable-pty (Rust crate) spawning wsl.exe -d <distro> PTYs.

Run

This project targets Windows. Dev requires:

  • Windows 10/11 + WebView2 Runtime (preinstalled on Win11).
  • MSVC toolchain (VS Build Tools, "C++ build tools" workload).
  • Rust on the Windows host.
  • Node 20+ and pnpm (corepack use pnpm@11.2.2).
  • WSL with at least one distro installed.

Location matters. The source must live on a Windows-native drive (here: D:\dev\tiletopia\). Don't run pnpm against the \\wsl.localhost\... UNC path — pnpm 11.x crashes inside isDriveExFat and the actual error gets swallowed by the crashing error-hint formatter.

From a Windows shell:

cd D:\dev\tiletopia
pnpm install
pnpm tauri dev          # iterate
pnpm tauri build        # NSIS installer at src-tauri\target\release\bundle\nsis\

The WSL-side symlink at ~/claude/projects/tiletopia points here for in-WSL editing.

How it works (current state)

  • Backend (src-tauri/src/pty.rs): a PtyManager holding a Mutex<HashMap<PaneId, PaneHandle>> of portable-pty children. Each spawned pane gets a background reader thread that emits pane://{id}/data events to the frontend (base64-encoded byte chunks). Counterparts: write_to_pane, resize_pane, kill_pane. Distro enumeration parses wsl.exe -l -q (UTF-16LE).
  • Frontend (src/components/XtermPane.svelte): xterm.js + FitAddon mounted into a div. On mount, calls spawn_pane, subscribes to the pane's event stream, wires term.onDatawrite_to_pane, and uses a ResizeObserver to forward dimension changes to the PTY.
  • App (src/App.svelte): titlebar with clickable distro buttons (auto-picks first non-docker-desktop distro; user can override). One XtermPane wrapped in {#key selected} so changing distro destroys + respawns the pane.

Layout

tiletopia/
├── CLAUDE.md, memory.md, README.md
├── .gitignore, pnpm-workspace.yaml
├── package.json, vite.config.ts, svelte.config.js, tsconfig.json, tsconfig.node.json
├── index.html
├── src/
│   ├── main.ts          # mounts App, imports xterm.css
│   ├── App.svelte       # titlebar + one XtermPane (M1)
│   ├── styles.css
│   ├── ipc.ts           # typed Tauri command wrappers
│   └── components/
│       └── XtermPane.svelte
└── src-tauri/
    ├── Cargo.toml, build.rs, tauri.conf.json
    ├── capabilities/default.json
    ├── icons/                # placeholder, copied from claude-usage-widget
    └── src/
        ├── main.rs
        ├── lib.rs       # tauri builder, registers commands, manages PtyManager
        ├── pty.rs       # PtyManager + list_wsl_distros
        └── commands.rs  # #[tauri::command] surface

Known gotchas (today)

  • Don't pnpm install from a UNC path (\\wsl.localhost\...). pnpm 11.x crashes in its isDriveExFat probe; the underlying error gets swallowed.
  • Console flash on wsl.exe -l -q: suppressed via the CREATE_NO_WINDOW flag in pty.rs. The PTY itself doesn't allocate a console (portable-pty uses ConPTY directly).
  • base64 wire format: xterm.js emits string from onData; we UTF-8 encode then base64. Not the fastest; switch to typed-array event payloads later if throughput is an issue.
  • No icons of our own: copied from claude-usage-widget. Replace before any release.
  • Cargo build only works on Windows host — Rust toolchain isn't installed in WSL. pnpm check runs in WSL and validates the Svelte/TS side.