Backend:
- save_workspace / load_workspace Tauri commands writing to
%APPDATA%\com.megaproxy.tiletopia\workspace.json with atomic
tmp+rename. Path from app.path().app_config_dir() (no dirs crate).
Layout helpers:
- tree.ts: changeDistro (with id swap to force XtermPane remount via
{#key}), changeLabel, presetSingle / TwoColumns / ThreeColumns /
TwoRows / TwoByTwo.
- New ops.ts with PaneOps interface bundling split / close /
setDistro / setLabel / distros, drilled through Pane chain
instead of individual callbacks.
UI:
- LeafPane: in-toolbar editable label (click to rename, Enter
saves, Esc cancels) and distro chip popover. Picking a different
distro respawns the pane.
- App.svelte: migrated from localStorage to APPDATA via the new
Tauri commands, debounced 500ms. One-time localStorage migration
on boot. Split inherits parent's distro+cwd. Titlebar preset
buttons with confirm when replacing >1 pane.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
9 KiB
Markdown
72 lines
9 KiB
Markdown
# memory — tiletopia
|
||
|
||
Durable memory for this project. Read at session start, update before session end. Date format: `YYYY-MM-DD`.
|
||
|
||
## Decisions & rationale
|
||
|
||
- **Stack: Tauri 2 + Svelte 5 + TypeScript + Vite + pnpm + xterm.js + `portable-pty`.** Mirrors `claude-usage-widget` so we reuse a known-good Windows-targeting toolchain (MSVC + WebView2 + NSIS installer). No new technology bets stacked on top of the new product bet.
|
||
- **Layout model: binary tree of splits, NOT free-form rectangles.** Same as i3 / tmux / Zellij. Each internal node is HSplit/VSplit + ratio; each leaf is a terminal. Dragging a gutter mutates one parent ratio; both sibling subtrees reflow; descendants get `resize`. Adaptive resize falls out automatically with no constraint solver. Preset layouts ("3 columns", "2×2") are pre-built trees.
|
||
- **PTY backend: `portable-pty` (same crate WezTerm uses).** Spawns `wsl.exe -d <distro> --cd <path>` on Windows. Manager is a `Mutex<HashMap<PaneId, PaneHandle>>` in Rust; each pane has a background reader thread that emits `pane://{id}/data` events.
|
||
- **Wire format: base64-encoded byte chunks via Tauri events.** xterm.js's `onData` emits strings; we UTF-8 encode then base64. Slower than a typed-array payload but trivially correct. Revisit if throughput matters.
|
||
- **Source on Windows-native disk (`D:\dev\tiletopia\`), symlinked into WSL.** Same pattern as `rimlike` (`D:\godot\rimlike`) and `tavernkeep`. Forced by pnpm 11.x's `isDriveExFat` crashing on `\\wsl.localhost\...` UNC paths.
|
||
- **Don't commit `node_modules`, `src-tauri/target`, or `.pnpm-store`. DO commit `Cargo.lock`** (binary project, reproducible builds).
|
||
- **Session awareness without an in-pane agent.** Plan: poll `/proc/<pid>/cwd` of the shell's child + foreground process every ~2s. Sufficient to detect `cd` and whether `claude` is running.
|
||
- **State propagation in the layout tree: hybrid mutable + replace.** The root tree is `$state(...)` at App level. Direct mutation (e.g. `node.ratio = X` during gutter drag) is reactive via Svelte 5's deep proxy. Structural changes (split/close) go through pure helpers in `tree.ts` that return a new root, which App reassigns. Drag stays fast (no tree walk); structural changes stay simple. `{#key leaf.id}` around `LeafPane` ensures swapping a leaf in/out cleanly unmounts XtermPane (which kills the PTY on destroy).
|
||
- **Layout persistence: `%APPDATA%/com.megaproxy.tiletopia/workspace.json`** via two Tauri commands (`save_workspace`, `load_workspace`). Atomic write (tmp + rename) so a crash mid-save can't leave a partial file. Path comes from Tauri's `app.path().app_config_dir()` — no separate `dirs` crate needed. M2's localStorage path is checked once at boot as a one-time migration source, then cleared.
|
||
- **Auto-save is debounced 500ms.** Every tree mutation kicks the `$effect`; it resets a timeout and only writes after 500ms of quiet. Cheap enough to never need throttling on UI mutations; matters because each gutter-drag step would otherwise hit disk dozens of times per second.
|
||
- **Pane operations bundled into a `PaneOps` interface** in `lib/layout/ops.ts`. Pane and SplitNode just pass `ops` through; LeafPane consumes it. Replaces M2's per-callback prop drilling (would have been split + close + setDistro + setLabel + distros = 5 separate props). Easier to grow as M4 adds broadcast / palette ops.
|
||
- **Per-pane distro change forces a remount via id swap.** `changeDistro` in `tree.ts` assigns a new id to the leaf; `Pane.svelte`'s `{#key leaf.id}` unmounts XtermPane (which kills the old PTY) and mounts a fresh one with the new distro. Same mechanism we already use for split/close.
|
||
- **Split inherits parent's distro AND cwd** (not label — label is a per-pane name, not a hierarchy thing). So "split right" while in a project keeps both panes in that project.
|
||
|
||
## Open questions / TODOs
|
||
|
||
- [x] ~~**M2 — splits-tree layout component.** Two panes side by side, draggable divider, both panes alive. Save/restore layout as JSON.~~ Done 2026-05-22.
|
||
- [x] ~~**M3 — workspace persistence + preset layouts + per-pane distro + pane labels.**~~ Done 2026-05-22.
|
||
- [x] ~~**Auto-save debouncing.**~~ 500ms timer in `App.svelte` `$effect`.
|
||
- [x] ~~**HMR distro picker reset.**~~ No longer an issue — per-pane distro selection via in-toolbar popover; titlebar `default:` only seeds new splits.
|
||
- [ ] **Multi-workspace tabs.** Several independent layouts the user can switch between. Saved as `workspaces.json` with `{ current: id, list: [{ id, name, tree }] }`. Not on the M0–M5 critical path; either bolt on after M5 ship or fold into a "tabs" minor milestone.
|
||
- [ ] **M4 — orchestration.** Broadcast input groups (write same bytes to N PTYs flagged into a group), idle/finish notifications (detect when a `claude` pane stops emitting output for >Ns → toast), Ctrl+K fuzzy palette over `label / distro / cwd`. Will extend `PaneOps` with a couple more methods + add a "broadcast group" concept to the leaf type.
|
||
- [ ] **M5 — Ship.** Replace placeholder icons, NSIS installer, Forgejo release. Copy `claude-usage-widget`'s release scripts.
|
||
- [ ] **Native Windows shells (cmd / pwsh)?** `portable-pty` supports them for free; keep the option open. Decide whether to expose in UI at M3.
|
||
- [ ] **Persistent scrollback across app restarts.** Would need an out-of-process mux daemon. Big scope creep; explicitly deferred past v1.
|
||
- [ ] **Keybinding philosophy.** Copy tmux, copy WezTerm, or invent? Decide at M3.
|
||
|
||
## Session log
|
||
|
||
### 2026-05-22 — M3 persistence + presets + per-pane distro/label
|
||
|
||
- Backend: added `save_workspace(json)` and `load_workspace()` Tauri commands. Atomic write via tmp + rename. Path resolved from `app.path().app_config_dir()`.
|
||
- Frontend ipc: `saveWorkspace` / `loadWorkspace` wrappers.
|
||
- `tree.ts`: added `changeDistro` (with id swap to force XtermPane remount), `changeLabel`, and 5 preset trees (single, 2H, 3H, 2V, 2×2).
|
||
- New `lib/layout/ops.ts` with `PaneOps` interface; refactored `Pane.svelte` / `SplitNode.svelte` / `LeafPane.svelte` to take `ops` instead of individual callbacks.
|
||
- `LeafPane.svelte`: in-toolbar pane-label editor (click to rename, Enter saves, Esc cancels) and distro chip with click-popover. Picking a different distro in the popover respawns the pane.
|
||
- `App.svelte`: migrated to APPDATA persistence with 500ms debounce. One-time localStorage→APPDATA migration on boot. Split inherits parent's distro+cwd via `findLeaf`. Titlebar preset buttons (1 / 2H / 3H / 2V / 2×2) with a confirm prompt when replacing >1 pane.
|
||
- `pnpm check` clean (109 files, 0 errors, 0 warnings).
|
||
- Manual verification on Windows: (to fill in)
|
||
|
||
### 2026-05-22 — M2 splits-tree layout
|
||
|
||
- Added `src/lib/layout/`: `tree.ts` (pure helpers: types, newLeaf, splitLeaf, closeLeaf, replaceById, serialize/deserialize with shape-checking), `SplitNode.svelte` (flex container + draggable gutter with pointer-capture), `LeafPane.svelte` (toolbar with split-right/split-down/close + XtermPane underneath), `Pane.svelte` (recursive dispatcher).
|
||
- Rewrote `App.svelte` to hold the tree as `$state` and wire split/close callbacks through. Auto-saves to localStorage on every `$effect` tick.
|
||
- Distro UX: titlebar shows clickable distro buttons that set the **default** for new panes. Existing panes keep their distro. Per-pane override is M3.
|
||
- Passes `pnpm check` cleanly (108 files, 0 errors, 0 warnings).
|
||
- Validated manually on Windows: splits-right and splits-down work, both panes stay alive, gutter drag reflows both xterm sides cleanly, close-pane collapses to the sibling, layout restores from localStorage across window restarts.
|
||
|
||
### 2026-05-22
|
||
|
||
- Graduated from `ideas/wsl-mux/` to project. Renamed working name `wsl-mux` → final name `tiletopia` across Cargo/package/Tauri configs and source.
|
||
- Promoted spike contents from `D:\dev\wsl-mux\spike\` to `D:\dev\tiletopia\` (no more spike subdir; the project IS what was the spike).
|
||
- Initialized git, created private Forgejo repo `tiletopia`, pushed initial scaffold.
|
||
- M1 verified manually on the Windows host: window opens, xterm.js renders, `claude` TUI works inside the pane, resize reflows cleanly, `htop` renders. Distro auto-pick chose `docker-desktop` (Docker Desktop's BusyBox helper distro) on first try — added explicit clickable distro buttons in the titlebar as both a diagnostic and a manual override. Clicking `Ubuntu` works end-to-end.
|
||
- Old idea folder archived to `~/claude/archive/ideas/wsl-mux/` (preserves full brainstorm + session log).
|
||
|
||
## External references
|
||
|
||
- **Approved plan / roadmap:** `~/.claude/plans/imperative-coalescing-feigenbaum.md` (M0–M5 milestones with verification criteria for each)
|
||
- **Stack precedent:** `~/claude/projects/claude-usage-widget/` — same Tauri + Svelte + WebView2 toolchain, already ships a Windows installer via Forgejo releases. WSL distro-probing logic copied/adapted into `src-tauri/src/pty.rs`.
|
||
- **Archived idea history:** `~/claude/archive/ideas/wsl-mux/plan.md`
|
||
- **Forgejo repo:** https://git.rdx4.com/megaproxy/tiletopia
|
||
- **xterm.js docs:** https://xtermjs.org/
|
||
- **portable-pty crate:** https://crates.io/crates/portable-pty
|
||
- **Tauri 2 docs:** https://v2.tauri.app/
|
||
- **Prior art for splits-tree layout:** i3, tmux, Zellij, WezTerm
|