Skip to main content
CHANGELOG

What shipped

Released versions only, generated at build time from the product Keep a Changelog file. Unreleased work is not listed here: if it is not in a version heading, it has not shipped.

Latest released v3.23.4 · · 61 releases

Currently downloadable v3.25.2-preview.1 · preview channel

Those are two different questions. This page lists versions that have been released, from the product changelog. The download page serves whatever the live preview channel points at, which moves ahead of the last released version with every build. Download the current build from /download. Channel and exact version always come from the live release manifest.

Releases 41–45 of 61

Page 9 of 13

v2.15.0

Hook-RPC flood fix, view-switch perf, install/updater hardening

Fixes the user-reported "freezing under load" and view-switch lag found via a dogfood-log RCA, finishes the remaining session-reliability hardening from the v2.14.0 RCA, makes the installer and auto-updater integrity-safe, and wires (inert) OSS code signing.

Fixed — hook-RPC timeout floods / UI freezes (Issue A1, A2)

  • hooks.signal no longer does a renderer workspace.list round-trip on every signal. A 2s-TTL coalescing cache collapses a tool-heavy turn's bursts into ~1 round-trip and serves the last-known list when the renderer is throttled — stopping the PostToolUse timeout floods that froze the UI and, at worst, blocked the daemon event loop into a forced respawn.
  • The Claude Code bridge retries transient connect-errors within its 2s budget (and never re-fires a request it already wrote), so a brief main-process restart window no longer drops hooks.

Fixed — session reliability (RCA A1/A9, A4, A6)

  • Partial-list reconcile now re-queries the daemon before clearing a live ptyId absent from a non-empty session list (2-strike guard), closing the last destructive-session-loss path the v2.14.0 RCA left open.
  • The daemon health probe tolerates a busy-but-responsive daemon — daemon.ping reports event-loop lag, thresholds raised to 5 strikes / 5s — instead of mistaking load for a hang and force-respawning.
  • DaemonClient.connect retries transient named-pipe errors (EPERM/ECONNRESET) with backoff; ENOENT still fails fast.
  • Session-pipe bind retries on EADDRINUSE, so a pane no longer dies when a prior pipe has not yet released its name.

Fixed — view-switch / multiview performance

  • WebGL terminal contexts are no longer disposed the instant a pane is hidden. A short grace period (cancelled on re-show) eliminates the GPU-context create/destroy thrash behind workspace-switch and multiview→single-view lag.

Added — auto-update integrity (fail-closed)

  • The updater downloads the Setup.exe and verifies a CI-published SHA-256 (update-manifest.json) before launching it; a tampered or unverifiable artifact is never run. Previously it opened an unverified URL.

Added — hook-RPC flood observability

  • A rolling 30s summary of slow/failed workspace.list resolutions is logged (escalating to a warning on a flood), so degradation is visible without hand-tallying bridge.log.

Changed — install funnel

  • install.ps1 now downloads the prebuilt, SHA-256-verified Setup.exe by default instead of always compiling from source. Build-from-source is opt-in (-FromSource / WMUX_FROM_SOURCE=1).

Changed — docs & security accuracy

  • Corrected the README "RunAsNode disabled" claim and reconciled SECURITY.md / PROTOCOL.md with the actual code (token entropy, icacls behavior, intentionally-disabled asar-integrity fuse). Removed the permanently-disabled EditorPanel "Save" affordance.

Added — code-signing pipeline (inert until configured)

  • release.yml is wired for SignPath Foundation (OSS) Authenticode signing of the installer, gated on a signing secret so it is a no-op until configured. Binaries remain unsigned (SmartScreen "unknown publisher") until the certificate is provisioned.

v2.14.0

Session-replacement fix + lifecycle observability + token ACL hardening

Fixes the reported instability where, while running several Claude Code windows, "the daemon resets and sessions get replaced by new empty windows." Root-caused via a multi-expert review (see plans/RCA-daemon-session-replacement-2026-05-29.md): the daemon process never actually dies (uptime is monotonic). The renderer's reconnect/reconcile path could not distinguish a transient failure from a permanent one and destructively cleared live ptyIds, making Terminal self-create empty sessions while the daemon still held the originals.

Fixed — live sessions replaced on reconnect (RCA A1/A2)

  • pty.reconnect now tags failures transient (pipe-not-writable / RPC threw during handler swap) vs permanent (session dead). useTerminal retries transient failures with short backoff instead of immediately clearing the surface — a live session no longer gets discarded on a momentary blip.
  • AppLayout reconcile preserves all ptyIds when the daemon returns an empty session list (almost always "not ready yet", not "all dead"). The late-reconnect (daemon:connected) path is now abort/timeout/catch guarded and never falls through to clearAllPtyState.
  • RECONCILE_TIMEOUT_MS is now derived from DAEMON_RPC_TIMEOUT_MS in shared/timeouts.ts (15s > 10s), removing the asymmetry that let a slow-but-successful pty.list trip the destructive startup fallback.

Added — daemon/main lifecycle observability (RCA A8)

  • Structured [lifecycle] logging on daemon attachSession/detachSession, main daemon:connected emit, DaemonClient.connect error codes (EPERM etc.), pty.list live-session count, and the renderer's destructive ptyId-clear decisions (mirrored into the main log). Reconnect/session-replacement events are now diagnosable post-hoc instead of invisible.

Security — token file ACL re-hardening (RCA A12)

  • secureWriteTokenFile only locked permissions when a token was freshly written; a token loaded from disk kept whatever (possibly broad, inherited) ACL it had. New reHardenTokenFileAcl() re-applies a restrictive ACL (Windows icacls) / chmod 0600 (POSIX) on the existing daemon-auth-token and ~/.wmux-auth-token at load time. Best-effort: never crashes a live daemon.

Fixed — session config merge + prefix mode

  • Merge session config against defaults on load and harden prefix mode handling.
  • Skip keybinding back-fill on key collision.

v2.13.0

OSC 133 EventBus tee + agent.awaiting_input lifecycle

Extends the agent.lifecycle event in wmux_events_poll with two new substrate signals so orchestrator SDKs and any MCP consumer can react to shell command lifecycle and agent approval prompts without polling terminal_read_events. Both signals are wired BOTH on the local-mode PTYBridge path AND on the daemon-mode DaemonNotificationRouter path (the default production path).

Minor version bump (v2.12.0 → v2.13.0) because the AgentLifecycleEvent payload gains a new source: 'osc133' enum value, a new kind: 'agent.awaiting_input' enum value, a nullable agent field (only null when source === 'osc133'), and an optional exitCode field. The AgentStatus union also gains 'awaiting_input'. All additive — existing v2.12.x consumers that switch on the previous enum values keep working unchanged.

Two new lifecycle signals (#76)

  • source: 'osc133' — every OSC 133 D shell-integration marker (e.g. from PowerShell, bash with VS Code shell integration, Ghostty, any CLI wrapped with prompt instrumentation) now tees onto the EventBus as kind: 'agent.stop' with the parsed exitCode. Latency-zero, shell-agnostic: orchestrators waiting on npm install / pytest / make / any CLI no longer need a heuristic detector. agent is set to the AgentDetector last-known slug when one is gated, otherwise null. OSC 133 events bypass the HookSignalRouter dedup ledger (always decision: 'emit') — they represent shell command lifecycle, not agent-turn boundaries.

  • kind: 'agent.awaiting_input' — AgentDetector now emits a distinct lifecycle kind when an agent surfaces a y/N or approval prompt mid-turn (Claude Code patterns Do you want to proceed? and Allow tool use for <Tool>). Distinct from agent.stop: orchestrators that auto-approve trusted operations can react to this kind to feed pre-approved answers without waiting for the turn to end. Routed through the same dedup ledger used for agent.stop.

Added

  • AgentLifecycleEvent.source enum — 'hook' | 'detector' | 'osc133'.
  • AgentLifecycleEvent.kind enum — 'agent.stop' | 'agent.subagent_stop' | 'agent.awaiting_input'.
  • AgentLifecycleEvent.exitCode?: number | null — present on source: 'osc133' events; absent on hook / detector sources.
  • AgentStatus = … | 'awaiting_input' — sidebar renders the new state as a yellow dot with the workspace.agentAwaitingInput label (en + ko translated; 21 other locales fall through Partial<TranslationMap> to en).
  • AgentSignalKind = … | 'agent.awaiting_input' — detector-only kind today; hook bridges are not expected to emit it but the union now admits it so dedup ledger entries share one shape.
  • scripts/osc133-awaiting-input-dynamic.mjs — end-to-end verification that spawns the packaged Electron app, exercises the daemon-mode path (the default production path), and asserts the new EventBus tee signals show up via wmux_events_poll. Result on this branch: 15/15 checks pass with a daemon--prefixed ptyId, confirming the daemon-path emit reaches the main process EventBus.

Fixed

  • Daemon-mode OSC 133 + awaiting_input mirror — the first cut wired the tee only on PTYBridge (the local-mode path). Daemon-backed PTYs — the default production path — parsed OSC 133 markers in DaemonPTYBridge and appended them to PromptEventLog but never forwarded them up to the main process, so consumers saw source: 'osc133' events in tests but never in real-world sessions. DaemonNotificationRouter now subscribes to a new session:prompt daemon broadcast and emits the EventBus tee from the production path. The awaiting_input lifecycle had the same gap; both are fixed together. Caught by Codex round-1 P1 review and verified end-to-end against the packaged build.
  • OSC 133 agent-attribution race — emitOsc133Lifecycle now snapshots the cached agent slug before awaiting workspace.list. The shell can emit OSC 133;D and then redraw the prompt in the same burst (firing a new session:agent event); without the pre-await snapshot, the OSC 133 emit would carry the next turn's agent slug. Matches the PTYBridge local-mode case 133 path, which reads agentDetector.getLastAgent() synchronously before any emit. Caught by Codex round-2 P2.
  • Approval-prompt regex tightened to whole-line anchors — Do you want to proceed? and Allow tool use for <Tool> are now anchored at both ends of the line, with only whitespace and Claude TUI box-drawing glyphs (│ ║ ┃ ═ ━ ─ ┄ ┅ ┆ ┇ ┈ ┉ ╭ ╮ ╯ ╰ ╔ ╗ ╝ ╚ ┌ ┐ ┘ └ ·) admitted as padding. Conversational mentions in agent output such as Answer Do you want to proceed? with caution or Please click Allow tool use for Bash no longer emit agent.awaiting_input — false positives are costly here because orchestrators may auto-feed approval responses. Codex rounds 1 → 5 progressively tightened this from an unanchored phrase match to a full-line anchor with canonical MCP tool-name grammar mcp__<server>__<tool> (two __ separators required, hyphens permitted, single-underscore identifiers rejected).

Changed

  • WmuxEventType is unchanged; agent.lifecycle was already present in v2.12.x. Only the payload shape grows.
  • wmux_events_poll MCP tool description updated to enumerate the three sources, new kind, and exitCode field so MCP-aware orchestrators discover the surface from introspection alone.
  • DaemonEvent.type gains a 'prompt.event' variant — the daemon-side broadcast carrying parsed OSC 133 PromptEvents to the main process.

Test

  • New DaemonNotificationRouter.lifecycle.test.ts (10 cases) covering detector awaiting_input emit, regression on waiting / complete, OSC 133 exitCode parsing, missing-suffix path, non-D ignore, agent slug cache, HookSignalRouter bypass for OSC 133, and session:died cache invalidation. Plus a race-fix test that mocks a deferred workspace.list and verifies the OSC 133 emit carries the pre-await snapshot, not the post-burst cache value.
  • New cases in PTYBridge.lifecycle.test.ts covering local-mode OSC 133 (exit code 0 / 1, no-suffix, A/B/C ignore, workspaceId gate, gated agent slug, dedup bypass), local-mode awaiting_input detection, regex false-positive immunity for mid-line Do you want to proceed? and Allow tool use for, regex true-positive on boxed prompts including corner glyphs (╮, ─), canonical MCP tool name matching (mcp__github__create_issue, mcp__context7__get-library-docs), and rejection of non-canonical single-underscore names.
  • Full suite: 2003/2004 (the one failure is StateWriter.test.ts:102 — the known cross-OS runner-load timeout flake first observed during v2.12.0 ship, independent of this PR; passes cleanly on rerun).
  • 5 rounds of Codex independent review: round 1 caught the two daemon-path P1 architectural gaps, rounds 2 – 5 progressively tightened detector regex correctness. All rounds passed the merge gate.

v2.12.0

MCP plugin permission enforcement + daemon lifecycle hardening

Lands the active enforcement layer for the Phase 2.1 MCP plugin substrate (PR #71) alongside a wave of lifecycle, identity, and UX hardening (PR #72/#74/#75). Plugins now have their declared capabilities verified on every RPC; the daemon self-shuts when idle and recovers from AV-blocked PID verification; a frozen WMUX_WORKSPACE_ID env can no longer leave in-pane MCP servers permanently stuck on a stale identity; xterm light themes are now WCAG-AA legible for true-color RGB white output; and keyboard pane/surface navigation finally moves DOM focus along with the visual marker.

Minor version bump (v2.11.0 → v2.12.0) because Phase 2.2 adds the RpcRejection discriminated union to RpcResponse's failure arm, the daemon.idleShutdownMinutes config, and the mcp.mode config flag. All additive; existing v2.11.x callers keep working.

Daemon lifecycle hardening (#72)

Closes four gaps in the wmux daemon lifecycle: an orphan daemon that survives forever in RAM after a forced wmux quit, a boot-block when anti-virus prevents PID verification, an opaque "daemon could not start" error after the respawn budget exhausts, and a transient first-ping race during cold-boot. Combined effect: the "1 wmux ≙ 1 daemon" invariant is now self-healing instead of relying on the next clean shutdown.

Added

  • Daemon idle self-shutdown — the daemon now terminates itself after 5 minutes with zero RPC clients and zero live PTY sessions (configurable via daemon.idleShutdownMinutes in ~/.wmux/config.json; set to 0 to keep the legacy "alive forever" behavior). Routes through the same shutdown() body used by SIGTERM / SIGINT / daemon.shutdown RPC, so the existing phase instrumentation and re-entry guard apply. Logs [shutdown.phase] idle.timeout idleMs=… cfgMs=….
  • DaemonPipeServer.getConnectionCount() / getLastDisconnectAt() — public accessors for the Watchdog idle predicate. The disconnect anchor is stamped only on the 0-edge (last socket closing), so a flapping reconnect cycle resets the idle deadline forward instead of accumulating stale idle time.
  • Watchdog idle-check hook — opt-in callbacks onIdleCheck / onIdleShutdown evaluated on every health tick. Decision logic exposed as evaluateIdle() so unit tests drive it without timers. Single state machine: idleMs = now − (lastDisconnectAt ?? startTime). Grace window and idle window are independently configurable.
  • scripts/daemon-idle-shutdown-dynamic.mjs — end-to-end verification that spawns the bundled daemon in an isolated tmp WMUX_DIR with WMUX_IDLE_SHUTDOWN_MS / WMUX_IDLE_GRACE_MS / WMUX_WATCHDOG_TICK_MS env overrides, connects, disconnects, and asserts the daemon exits cleanly with the idle.timeout breadcrumb. Runs in ~5s.

Fixed

  • Launcher ping retry — ensureDaemon now retries the first daemon.ping once with a 250ms delay before declaring the existing daemon unresponsive. Absorbs the cold-boot race where Defender realtime scan, ConPTY cold-init, or a large recovery loop makes the daemon miss the first 3-second ping window. Total worst case 6.25s, still well under the 15s spawn budget.
  • Unverified-live PID is now recoverable — when anti-virus blocks tasklist.exe / Get-CimInstance and the launcher cannot confirm what owns daemon.pid, it now prompts the user with an Electron dialog offering "Clean up and start fresh" instead of refusing to boot. Cancel re-throws the legacy error, now annotated with the exact elevated-PowerShell taskkill /F /PID … command for manual recovery.
  • Respawn-exhausted is no longer silent — DaemonRespawnController now captures the latest error message from the bootstrap or respawn loop and ships it on the respawn-exhausted event. main surfaces it via a native dialog.showErrorBox plus the existing renderer IPC channel, with concrete recovery steps. lastError is cleared on successful install so future exhaustions don't echo stale diagnostics.
  • SIGKILL-failure throw now embeds the recovery command — when the OS refuses to terminate a verified-stale daemon (typically EPERM under AV / different-user scenarios), the thrown error now includes the exact taskkill /F /PID … invocation the user needs in an elevated PowerShell. No silent taskkill fallback because process.kill('SIGKILL') already walks the same TerminateProcess path with the same user token; embedding the hint is more honest than retrying.

Changed

  • DaemonRespawnController.RespawnEvent — the respawn-exhausted variant now carries an optional lastError field. Additive change; existing consumers that ignore the field still type-check.
  • Suppression env var WMUX_NO_DIALOG=1 bypasses both the launcher recovery dialog and the respawn-exhausted dialog for automated runs.

Test

  • New idleShutdown.test.ts (source-level invariants for the daemon main wiring), new idle-flow test cases in Watchdog.test.ts, new getConnectionCount / getLastDisconnectAt lifecycle test in DaemonPipeServer.test.ts, new lastError propagation test in DaemonRespawnController.test.ts, and scripts/daemon-idle-shutdown-dynamic.mjs for the end-to-end path.

Workspace identity drift fix (#72)

Fixes a serious multi-agent bug: an in-pane MCP server (e.g. Claude Code) could get permanently stuck reporting a workspace id that no longer exists — a2a.whoami returning no workspace found for ws-… and terminal_send rejecting with not owned by workspace … (actual owner: …). Every identity-gated MCP call (A2A, terminal_*, browser routing) failed until the MCP server was restarted. Triggered when a workspace id is re-minted (daemon respawn / session restore) while the shell process — and its frozen WMUX_WORKSPACE_ID env — lives on.

Fixed

  • Workspace-identity is now anchored to the immutable ptyId, not a frozen workspace id. The on-disk PID map (~/.wmux/pid-map/<pid>) stores the ptyId; a2a.resolve.identity resolves the current owning workspace live from the renderer (input.findOwnerWorkspace) on every call. A re-minted workspace id can no longer produce a stale identity. The map is also re-anchored on pty.reconnect, so a surviving shell re-adopted after a respawn resolves correctly without a restart.
  • MCP resolvers (src/mcp, src/company/mcp) no longer permanently trust the env hint. WMUX_WORKSPACE_ID is demoted to a last-resort fallback behind the live PID-walk; an RPC that reports a stale identity (no workspace found / not owned by workspace) invalidates the in-process cache so the next call self-heals.

Changed

  • a2a.resolve.identity returns PID → current workspaceId (resolved live), legacy ws--prefixed pid-map entries pass through for one cycle, and ptyIds with no live owner are omitted (no phantom mappings).
  • docs/PROTOCOL.md §6.1 reordered: path B (live PID-walk) is now preferred over path A (stale-prone env hint); added the ptyId-anchor and self-heal notes.

Phase 2.2 MCP plugin permission enforcement (#71)

Lands the active enforcement layer on top of the Phase 2.1 record-only identity + grammar substrate (PR #48) and the spec-side default rules (PR #68). Plugins that declare a capability set via mcp.declarePermissions now have those declarations verified against every RPC they issue; mismatches return a structured RpcRejection describing the per-path failure, and unconfirmed declarations surface a user-approval prompt before the call can proceed.

Added

  • PermissionEnforcer substrate (src/main/mcp/PermissionEnforcer.ts) — pure-function permission gate. Given a method, params, request context, and trust record, returns allow, reject, or partial. Same function runs in both shadow and enforce modes; only the dispatcher's reaction changes.
  • Single declarative methodCapabilityMap — Record<RpcMethod, RequiredCapability> covering the full 96-method RPC surface. tsc --noEmit enforces totality so a new method without a gate entry fails the build. Identity bootstrap (mcp.identify, mcp.declarePermissions, system.identify, system.capabilities) is capability: null. Internal surfaces (daemon, company, surface, hooks) map to the reserved wmux.internal capability that no plugin can declare.
  • Structured RpcRejection discriminated union on RpcResponse's failure arm — capability-not-declared, path-not-allowed, paths-partially-allowed (with {allowed, rejected[]}), and identity-status (with optional pendingApproval.promptId). Additive on the existing {ok:false; error} arm; every switch (r.ok) site keeps narrowing.
  • ShadowRejectionLogger + JSONL audit log at ~/.wmux/shadow-rejections.log — discriminated entries (rejection / legacy-traffic). 1 MiB cap with single-generation rotation. Sync writes wrapped in try/catch — telemetry must never affect RPC throughput.
  • LegacyTrafficCounter — per-method milestones (1st / 10th / 100th / 1000th / 10000th call) for envelope-less RPCs, flushed to the shadow log. Replaces the previous process-once trust-DB write for accurate v3.1 surfacing data.
  • ApprovalQueue — (clientName, hash(declaredCapabilities)) dedupe key, synchronous promptId minting + async resolution. On approve/deny, writes through PluginTrustStore.setUserDecision. Multiple inflight RPCs from the same plugin during a prompt coalesce onto one modal.
  • PermissionApprovalDialog — risk-class-grouped capability list with asymmetric wording. Terminal-content (terminal.read, pane.search) and terminal-input (terminal.send) get critical-severity copy that names the concrete privilege ("can read what's on your screen, including secrets"); metadata / events / pane-lifecycle / workspace get neutral copy. Browser and A2A get caution.
  • mcp.mode config flag in ~/.wmux/config.json — shadow or enforce. Production wmux defaults to enforce; dev (electron-forge start / NODE_ENV=test) defaults to shadow for dogfood rollback safety.
  • PluginTrustStore.setUserDecision(name, 'trusted' | 'denied') — explicit user-decision write path. Seeds a fresh record when a prompt fires before mcp.identify lands.
  • Spec §4.4 "Enforcement contract" — documents the wire shape, retry idiom, mode flag, and worked glob example (meta.write:custom.foo ≠ custom.foo.bar without trailing * or **).
  • inventory.md Phase 2.2 capability map — per-method capability + path-source + risk-class column.

Changed

  • RpcRouter.dispatch now calls the enforcer before invoking the handler. In shadow mode, the would-be rejection is logged and the handler still runs (no behavior change for v2.x callers). In enforce mode, a non-allow outcome returns the RpcResponse failure WITHOUT calling the handler. legacy callers (no clientName envelope) and identity-bootstrap RPCs are always allowed.
  • ApprovalQueue.requestApproval returns { promptId, resolution } — the promptId is available synchronously so the dispatcher can thread it into the rejection without awaiting the user's decision.

Fixed

  • Keyboard pane/surface navigation now moves keyboard focus, not just the active border (src/renderer/hooks/useActivePaneFocus.ts). Switching panes with the tmux prefix arrows, Alt+Ctrl+Arrow, Ctrl+Tab, the RPC pane.focus bridge, or keyboard tab-switching moved the red active border (driven by ws.activePaneId) but left DOM focus on the previously focused pane's xterm — so keystrokes still landed in the old pane. xterm routes input from whichever textarea holds DOM focus, and no navigation path ever called terminal.focus(). A central useActivePaneFocus hook now pulls DOM focus onto the resolved active terminal whenever the target workspace/pane/surface changes, covering every state-only switch path in one place. Mouse clicks were unaffected (the click focuses the target xterm for free) and remain so.

Notes for plugin authors

  • Plugins SHOULD retry on rejection.pendingApproval.promptId with 1–5 s backoff. The substrate doesn't pin a socket waiting for the user (50-connection cap; OAuth authorization_pending precedent).
  • meta.write:custom.foo matches the EXACT path custom.foo. Declare meta.write:custom.foo.* or meta.write:custom.foo.** to cover the subtree.
  • events.poll is partial-mode multi-path: subscribing to mixed-allowed topics returns the allowed subset with a paths-partially-allowed rejection on the failure arm carrying both allowed and rejected lists. pane.setMetadata and pane.clearMetadata are all-or-nothing.

Light xterm theme contrast (#74)

Claude Code (and several other TUI apps) emit foreground text as true-color RGB white (#FFFFFF). Those escape sequences bypass our sandstone-light / paper-light xterm palettes, so the literal white rendered directly on hinomaru's cream background (#FAF8F5) and read as invisible — users could not see Claude Code's output at all on hinomaru/taegeuk.

Fixed

  • xterm minimumContrastRatio set to 4.5 (WCAG AA) on light themes. Detected via isLight(background) on the resolved palette; covers built-in light themes and any custom palette a user configures to a light tone. Dark themes keep the default ratio of 1 so intentionally subtle dimmed foregrounds (e.g. catppuccin-mocha's text-muted) remain unmodified.
  • Applied at both the initial new Terminal({...}) site and the runtime theme-switch effect, so toggling between themes inside a live session takes effect without remounting the terminal.

Keyboard pane navigation DOM focus (#75)

Switching panes with the keyboard moved the red active border but typing still landed in the previously focused pane. xterm routes keystrokes from whichever <textarea> currently holds DOM focus; navigation paths (focusPaneDirection, cyclePane, surface-tab switches, RPC pane.focus) only updated state, never called terminal.focus(). Mouse clicks were unaffected because the click focuses the target xterm DOM for free — so only keyboard paths were broken.

Fixed

  • useActivePaneFocus central hook (src/renderer/hooks/useActivePaneFocus.ts) — subscribes to the resolved active terminal (workspace + pane + surface) and pulls DOM focus onto that xterm whenever the target changes, closing every state-only switch path in one place rather than patching four call sites. Retries across a few animation frames so a freshly split pane's xterm still gets focus once useTerminal registers it. Declines non-terminal surfaces (browser/editor).

Test

  • New src/renderer/hooks/__tests__/useActivePaneFocus.test.ts — 11 cases on the pure resolution logic (resolveActivePanePtyId), including pane-switch and same-pane tab-switch coverage that directly pins this bug, plus browser/editor/empty-ptyId rejection. The DOM-focus application half (terminal.focus() + rAF retry) needs a browser harness the node-env vitest lacks and is verified by dogfood.

v2.11.0

Orchestrator substrate + Claude Code hook plugin

Lands the substrate piece that the new @wmux/orchestrator npm SDK consumes, plus the Claude Code hook plugin integration that delivers sub-200ms agent-completion signals (vs the heuristic regex detector). Minor version bump because the new agent.lifecycle event type is additive — no breaking changes vs v2.10.x clients.

Added

  • agent.lifecycle EventBus tee from hook + detector sources (#63). New WmuxEventType agent.lifecycle streams whenever a supported inner agent (Claude Code today; others via the integrations/<slug> bridge later) finishes a turn or subagent span. Tee sites:

    • hooks.rpc.ts — Claude Code Stop / SubagentStop hooks fire RPCs that emit the event with source: 'hook'. Sub-200ms, deterministic. Both emit and dedup decisions stream so observers can compare.
    • PTYBridge.ts AgentDetector — regex-based fallback for any agent, emits with source: 'detector' (~1-2s lag).
    • DaemonNotificationRouter.emitDetectorLifecycle — daemon-backed PTYs (the default production path) — sync recordDetector call before async workspace.list resolution so dedup timing matches local-mode. Carries ptyId, kind (agent.stop | agent.subagent_stop), source, agent slug, decision (emit | dedup). Polled via the existing wmux_events_poll MCP tool with the type filter extended.
  • Claude Code hook plugin Phase 1 integration backbone (#60). Adds the integrations/claude-code/hook-plugin/ directory that bridges Claude Code's hook events into wmux's signal pipeline. Foundation for the structured agent observability surface.

  • Phase 1.5 signal-health + Phase 2 usage-meter + env-first routing (#61). Per-pane signal-health plumbing (~140 LOC across substrate only — proxy metric layers like cumulative / percent / banner were dropped after the Codex review). 5-hour and 7-day usage windows. Env-first hook routing fix so WMUX_HOOK_TARGET overrides config-derived destinations.

Fixed

  • NOTICE files preserved for Apache 2.0 §4(d) compliance (#62). Bundled third-party NOTICE files now survive the electron-forge pack step, satisfying the Apache 2.0 attribution clause for the dependencies that ship one.

Documentation

  • README: SmartScreen install guidance for the unsigned installer (#66).
  • README: pointer to the new @wmux/orchestrator SDK in the MCP integration section (#67).

Compatibility

  • No breaking changes vs v2.10.x. Existing MCP clients keep working.
  • New agent.lifecycle event type is additive — clients that don't filter for it won't see it.
  • @wmux/orchestrator v0.1.x requires wmux ≥ 2.11.0 (the version this agent.lifecycle tee actually ships in — the SDK README mention of "≥ 2.10" was off by one).