tiletopia/README.md
megaproxy b352f8f049 Initial scaffold from M1 spike (tiletopia)
Tauri 2 + Svelte 5 + xterm.js + portable-pty. Single full-window
WSL terminal pane with clickable distro picker. M1 verified manually
on Windows: window opens, xterm.js renders, claude TUI works,
resize reflows cleanly.

Graduated from ~/claude/ideas/wsl-mux/ per the approved plan at
~/.claude/plans/imperative-coalescing-feigenbaum.md. See memory.md
for decisions, open TODOs, and the M2-M5 roadmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:31:29 +01:00

4.2 KiB

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.