Commit graph

138 commits

Author SHA1 Message Date
00a1e24ecf Shelve the per-pane context indicator (keep narrow-toolbar fix)
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>
2026-05-28 23:47:06 +01:00
15c2842ce1 Context bar: loosen recency gate to 3h (10min hid idle-but-live sessions)
The match works (cwd resolves, backend has the data), but a live claude
you're actively using can sit idle far longer than 10min, so the gate was
hiding it. Loosen to 3h — suppresses only genuinely dormant directories.
Can't distinguish 'claude live here' from 'shell in a recent claude dir'
without a WSL foreground-process probe (deferred).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:38:13 +01:00
a1d7919537 Context bar: defer OSC7 cwd update out of render phase (was dropped)
The OSC 7 handler runs synchronously inside term.write() as PTY data is
processed, which can coincide with React's render phase — calling the
parent setState there warned 'cannot update while rendering' and the
liveCwd update was dropped, so claude panes never registered their cwd
and the bar never showed (backend data + match were fine). Defer the
onCwd call via queueMicrotask. Plus a TEMP per-pane match-decision log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:19:25 +01:00
bbe827af22 TEMP: log context distros/sessions + OSC7-reported cwd for diagnosis 2026-05-28 23:14:03 +01:00
50766c3fdd memory: log OSC 7 confirmed + recency gate + absolute-token fix 2026-05-28 23:09:44 +01:00
c01a4decbf Context bar: show absolute tokens, assume 1M window (% wasn't reliable)
The transcript doesn't record the 200k-vs-1M window (model id is bare,
e.g. claude-opus-4-7; the [1m] in /context is display-only), so the
<200k→200k guess overstated the % for 1M users (a 42k session read 21%
instead of 4%). Fix: the indicator label now shows the absolute token
count (accurate regardless of window), and the fill bar assumes 1M (the
common case here; a 200k-only user would just see the bar read low while
the token number stays correct).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:09:15 +01:00
0358128b24 Context bar: only show for actively-written sessions (gate on recent mtime)
A bash pane sitting in a directory that once had a claude session was
lighting up with that session's stale context. Gate the indicator on the
matched session having been written within the last 10 min, so it tracks
a live claude rather than any dormant transcript in the same dir.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:06:30 +01:00
02d97d1520 memory: log OSC 7 live-cwd fix for the context bar (pending re-test) 2026-05-28 22:58:10 +01:00
d776f962da Context bar: match panes by live cwd via OSC 7 (was keyed on unset leaf.cwd)
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>
2026-05-28 22:57:53 +01:00
24ab7f067f memory: narrow-toolbar verified; context bar blocked on leaf.cwd never being set 2026-05-28 22:53:45 +01:00
20b60661cb memory: backlog 'reattach window to existing window' idea
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:44:30 +01:00
5f8e9f92c5 memory: log pivot from usage panel to per-pane context-fill indicator
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:43:24 +01:00
d951c360ae Replace token-usage panel with per-pane context-fill indicator
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>
2026-05-28 22:43:06 +01:00
b23f3d1ecb memory: log usage-panel scope/metric refinements + double-count investigation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:26:33 +01:00
ebbf8db407 Usage panel: scope to open panes, lead with tokens, label $ as API estimate
Addresses feedback on the usage panel:
- It was summing every recent session on the distro (all projects, mounted
  + home dirs), not the open panes' work — which read as inflated/double-
  counted. (Verified there's no literal double count: every transcript is
  read once and no two project dirs share a cwd, since claude resolves
  symlinks/mounts to the real path before mangling.) Now the panel + the
  titlebar chip default to sessions whose cwd matches an open pane, with an
  'open panes / all recent' toggle to see the full per-distro list.
- Token volume is now the headline figure; the API-cost estimate is shown
  as a clearly-labeled '~$' secondary, with a footer note that it's n/a on
  a Pro/Max subscription and can't reflect /usage quota. Kept visible (not
  hidden) so it can be validated against real API billing.

Frontend-only; backend still returns the full recent set for the toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:26:15 +01:00
e3c3810ba0 memory: log per-session token tracking (done, pending Windows verify)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:16:20 +01:00
e30ac461af Commit pnpm-lock for the three new xterm addons
Pins @xterm/addon-canvas 0.7.0, addon-search 0.15.0, addon-unicode11
0.8.0 (installed on the Windows host) so the deps are reproducible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 22:16:03 +01:00
1df8c3181b Add per-session claude token/cost usage panel (WSL, v1)
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>
2026-05-28 22:15:51 +01:00
a6d3f8a9f9 memory: mark cursor fix + 3 xterm features verified on Windows
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 21:57:32 +01:00
1bbc6a5783 memory: mark find-in-scrollback, unicode11, pane-nav implemented (pending Windows verify)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 21:52:07 +01:00
baa00dfc5c Add find-in-scrollback, unicode11, and keyboard pane navigation
Three xterm.js features, implemented together because they share the
XtermPane mount + the single attachCustomKeyEventHandler:

- Unicode 11: load @xterm/addon-unicode11, set activeVersion='11' after
  the canvas renderer so emoji/CJK/box-drawing widths stop drifting.
- Find in scrollback: @xterm/addon-search + a new per-pane SearchBar
  overlay (Ctrl+Shift+F to open, Enter/Shift+Enter next/prev, regex +
  case toggles, Esc to close & refocus). Overlay is an absolutely-
  positioned sibling in a position:relative wrapper so fit() is unaffected.
- Pane navigation: Ctrl+Alt+Arrow / Ctrl+Alt+HJKL (spatial neighbour via
  findNeighborInDirection) and Alt+1..9 (Nth leaf in walkLeaves order).
  XtermPane emits a NavigateIntent; App resolves the target leaf and sets
  it active, reusing the existing isActive->focusTrigger refocus chain.

All chords live in one attachCustomKeyEventHandler (xterm replaces the
handler on each call). Shortcuts added to shortcuts.ts (SoT for README +
Help), including the Alt+digit shell-conflict caveat. tsc clean apart
from the three not-yet-installed addon modules.

Needs pnpm install on the Windows host + runtime verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 21:51:29 +01:00
8bb080345e memory: log fan-out feature research backlog (5 prioritized + parked)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 21:39:23 +01:00
b5db68da8b memory: log in-app code-markup/editor-pane idea
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 21:32:05 +01:00
07bba99eb5 Use canvas renderer to fix stuck/ghost cursor in panes
The DOM renderer draws the cursor as a separate layered element; under
the Claude TUI's rapid cursor hide/show plus cursorBlink it leaves a
stale white block frozen where the cursor used to be. Load
@xterm/addon-canvas (composites the cursor into the text surface) with a
try/catch that falls back to the DOM renderer on init failure. Canvas
over WebGL because tiletopia runs many panes and WebView2 caps live
WebGL contexts (~16).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 21:31:59 +01:00
df159056a1 memory: log v0.4.0 release wrap-up
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:44:58 +01:00
5ef35e3a74 README: add tabs + multi-window to feature highlights
The at-a-glance highlights list omitted the two headline 0.4.0 features
(tabs and multi-window pane transfer); body sections already covered them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:43:21 +01:00
2a1f1d41ad Bump version to 0.4.0
Tabs + multi-window pane transfer feature release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 20:36:22 +01:00
309b6024d4 Fix XtermPane IPC listener leak on unmount-during-spawn/adopt
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>
2026-05-28 20:34:36 +01:00
e6d0040021 Fix workspace accumulation, tab-close popover, scrollbars, drag ghost
- 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>
2026-05-28 20:24:09 +01:00
bea6cf2977 Fix detached-window IPC scoping and pane-transfer session loss
- 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>
2026-05-28 19:46:30 +01:00
681d15fdc3 memory.md: fix Phase 2 verify command (cargo from src-tauri/, not root)
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>
2026-05-28 19:05:29 +01:00
597f9ac9b7 Session log: tabs + multi-window pane transfer (3 phases)
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>
2026-05-28 19:01:26 +01:00
6faf7e5e19 Phase 3: drag pane past window edge to detach
Extends the existing header-drag gesture (which swaps panes inside
the window) with an "outside the window" case: release the drag more
than 60px past any viewport edge and the pane detaches into a new
window via the same moveToNewWindow path the right-click menu uses.

The 60px slop avoids triggering on accidental release over the OS
titlebar / window chrome — without it any drag that ended above
clientY=0 would fire as a detach, which is wrong because that area is
still inside the user's window.

No backend changes — Phase 2's transfer mechanism already handles
everything; this just wires a second entry point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:59:48 +01:00
8ad51787fc Phase 2: drag-/right-click-a-pane-to-new-window
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>
2026-05-28 18:57:31 +01:00
1a035ad0a6 Phase 1: tabbed workspaces
Each tab is an independent tile tree; PTYs in non-active tabs keep
running (render-all-panes with visibility:hidden on inactive layers
so xterm.js's fit() still sees valid dimensions and the existing
per-pane resize dedupe absorbs no-op SIGWINCHes).

workspace.json shape goes from a bare TreeNode to
`{ version: 2, workspaces: [{ id, name, tree }] }` with a legacy v1
auto-wrap migration (the old single tree becomes one tab named
"Default").

App.tsx wraps the old single-tree state in workspace-aware state
but keeps `tree` / `setTree` / `activeLeafId` / `setActiveLeafId` as
identity-stable derived wrappers (reading currentWorkspaceId from a
ref), so the bulk of App.tsx stays unchanged.

XtermPane's initial term.focus() now checks `visibility !== "hidden"`
on the container so a pane mounting inside a hidden tab on app boot
doesn't yank focus away from the active tab. The focus poller is
scoped to the active workspace layer for the same reason.

Shortcuts: Ctrl+T new tab, Ctrl+Shift+T close current (window.confirm
when there are live panes), Ctrl+PageDown/PageUp navigate, Ctrl+1..9
switch to tab N. README + help overlay auto-generated from
shortcuts.ts.

79/79 vitest pass (7 new envelope-migration cases). tsc -b clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 18:43:32 +01:00
c92847413b Session log: v0.3.0 shipped + release-time gotchas for next time
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>
2026-05-26 19:32:22 +01:00
1db8b26109 release.sh: call node directly for build:mcpb (skip pnpm install hang)
`pnpm run build:mcpb` triggers an implicit `pnpm install` first to
verify node_modules against the lockfile. From WSL against the
/mnt/d/ Windows filesystem that node_modules walk hangs for minutes.
The build-mcpb.mjs script is pure Node + fs (no deps) so we can just
invoke it directly. Saves the pnpm wrapping overhead on every release.

Caught during the v0.3.0 release run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:29:53 +01:00
99b97c0c9b Cargo.lock: 0.2.3 → 0.3.0 2026-05-26 19:14:47 +01:00
7e285b27df pnpm check: use tsc -b so it catches what pnpm build catches
`tsc --noEmit` and `tsc -b` apply slightly different narrowing rules
on project-reference codebases — the prior check missed the spawn_pane
hostId narrowing bug (commit e1ceaab) that pnpm build immediately
flagged. Both tsconfig.app.json and tsconfig.node.json already set
`noEmit: true`, so `tsc -b` does no emission — the only difference
is build-mode dependency tracking + slightly stricter type checks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:11:34 +01:00
e1ceaabbff Fix tsc -b error: bind narrowed SSH spawn spec to a local before closure use
buildConfirmInfo's spawn_pane case used `a.spec!.hostId` inside a
hosts.find() callback, which compiles under tsc --noEmit (what
pnpm check runs) but fails under tsc -b (what pnpm build runs):
the non-null assertion drops the kind==="ssh" narrowing the parent
ternary had established, so .hostId can't be resolved against the
WSL/PowerShell variants of the union.

Fix: bind a.spec to a local const inside the narrowed if-block so
the closure carries the SSH variant through.

Caught by pnpm tauri build during v0.3.0 release prep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:06:19 +01:00
420438b494 Bump version to 0.3.0 2026-05-26 19:03:44 +01:00
3d4e0fabe5 Clear cargo warnings: drop v2.1 classifier scaffold, annotate tool_router
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>
2026-05-26 18:59:04 +01:00
139730259a README: refresh for today's shipped features (MCP v2, SSH, hard-deny)
Several sections were stale:

- Top feature list missing SSH host support, PowerShell, MCP, drag-to-swap.
- MCP section still claimed "v1 is read-only" — actually shipped 10 write
  tools (PR-1 through PR-4 today) plus three-tier policy engine, audit log,
  SSH safeguards, extraArgs sanitiser, and 14 compiled-in hard-deny patterns
  (with the rule-set rework that fixed the 9-of-10-rules-don't-actually-fire
  bug). Added a per-tool table.
- Test counts were 43 vitest cases; actual is 72 (frontend) + 138 (Rust).
  Added the `cargo test --lib` recipe and pointer to scripts/pr4-verify.mjs.
- Architecture section now covers hosts.rs / creds.rs / mcp.rs / mcp_policy.rs
  alongside pty.rs and the layout tree.

Marker block (auto-generated from shortcuts.ts) untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:42:49 +01:00
35194cd60c release.sh: build + attach .mcpb bundle alongside the installer
The McpPanel's "Download .mcpb" button opens the Forgejo release page,
so the asset has to actually be there. release.sh now runs
`pnpm run build:mcpb` and attaches `dist-mcpb/tiletopia.mcpb` as a
second --asset to `tea releases create`.

Closes B's mcpb-into-release follow-up. Next release tag will have a
working one-click Claude Desktop install path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:37:57 +01:00
50fbd0e531 Revert idle "claude foreground" filter — back to legacy 5s notify
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>
2026-05-26 18:33:11 +01:00
9931a92c5f Idle probe: inline pane_id + watch list into bash script (drop positional args)
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>
2026-05-26 18:25:55 +01:00
6772b8db37 Idle filter: pivot per-distro → per-pane via TILETOPIA_PANE_ID env marker
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>
2026-05-26 17:58:51 +01:00
d3474d33b0 README: regenerate marker block to pick up new MCP tip from shortcuts.ts
Auto-merge of the .mcpb commit captured the new tip body in shortcuts.ts
but left the old text inside the README's <!-- SHORTCUTS:START --> block.
Running pnpm gen:readme syncs them — proves the new workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:37:11 +01:00
b29233a012 Add .mcpb Claude Desktop bundle with zero-config token handling
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).
2026-05-26 17:36:29 +01:00
25aac634ab README: generate shortcuts table from shortcuts.ts (single source of truth)
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.
2026-05-26 17:34:54 +01:00