tiletopia/memory.md
megaproxy 64b90ebddb Add M3: APPDATA persistence + presets + per-pane distro/label
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>
2026-05-22 12:55:46 +01:00

9 KiB
Raw Blame History

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

  • M2 — splits-tree layout component. Two panes side by side, draggable divider, both panes alive. Save/restore layout as JSON. Done 2026-05-22.
  • M3 — workspace persistence + preset layouts + per-pane distro + pane labels. Done 2026-05-22.
  • Auto-save debouncing. 500ms timer in App.svelte $effect.
  • 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 M0M5 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 (M0M5 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