* Fix manual unread clear race on focused tab
* Add mark-as-read tab action and show ring for manual unread
* Flash then clear manual unread on tab focus
* Add tmux rename-window workspace compatibility
Implement workspace.rename in the v2 API and wire CLI commands rename-workspace/rename-window with help text.
Add a regression test that validates API and CLI rename parity plus error handling.
Refs: https://github.com/manaflow-ai/cmux/issues/153
* Add full tmux compatibility command matrix and regression coverage
When the server returns a plain-text error (e.g., "ERROR: Access denied
...") before the JSON protocol starts, sendV2() would pass it through
JSONSerialization which throws a confusing NSCocoaErrorDomain 3840 error.
Now sendV2() checks for "ERROR:" prefix and surfaces the real message.
Also includes the raw response in the fallback error for easier debugging.
Fixes https://github.com/manaflow-ai/cmux/issues/188
Previously, --help/-h was only checked at the top level before command
dispatch. Running e.g. `cmux new-workspace --help` would execute the
command instead of printing help.
Add per-subcommand help text for all commands that take arguments/flags.
The help check runs before socket connect so it works even when cmux is
not running. Commands without dedicated help (ping, help, list-windows,
etc.) fall through to normal behavior.
* CLI: add --command flag to new-workspace
Allows running an initial command in the new workspace's terminal:
cmux new-workspace --command "cd /path && claude"
After creating the workspace, waits 500ms for the shell to initialize,
then sends the command text via surface.send_text.
Closes#120
* CLI: error on unknown flags for new-workspace
Typos like `--comand` were silently ignored. Now reports the
unknown flag and lists known flags.
* CLI: skip --command send when workspace creation fails
If new_workspace returns an error instead of "OK <uuid>",
don't attempt to send the command (which would hit the wrong workspace).
* Remove index-based CLI APIs and make commands workspace-relative
Migrate 14 CLI commands from v1 text protocol to v2 JSON-RPC, making them
workspace-relative via CMUX_WORKSPACE_ID env var fallback. Update 7 more
commands to use normalize functions instead of legacy resolvers. Remove dead
code (5 structs, 5 parsers, 3 functions). Add regression tests.
Commands migrated v1→v2: send, send-key, send-panel, send-key-panel,
new-split, new-pane, new-surface, close-surface, list-panes,
list-pane-surfaces, list-panels, surface-health, focus-pane, focus-panel.
Commands updated: move-workspace-to-window, list-workspaces,
close-workspace, select-workspace, trigger-flash, resolveWorkspaceId,
resolveSurfaceId.
* Fix CMUX_SURFACE_ID env fallback when --workspace is overridden
When --workspace is explicitly passed, don't fall back to CMUX_SURFACE_ID
from the caller's environment. The caller's surface belongs to a different
workspace, causing "surface not found" errors. Only use the env var fallback
when the workspace is implicit (from CMUX_WORKSPACE_ID or server default).
Affects: send, send-key, new-split, close-surface, trigger-flash, identify,
notify, claude-hook.
* Validate surface before close and respect -- option terminator in send
P1: close-surface now validates the surface handle exists before sending
surface.close, preventing silent fallback to focused surface when a stale
or mistyped ref is provided.
P2: parseOption now respects -- as an option terminator. send, send-key,
send-panel, send-key-panel strip the -- marker from payload args. This
prevents payload tokens like --workspace from being consumed as routing
flags (e.g. `cmux send -- echo --workspace foo` sends all text correctly).
* Fix test_send_workspace_relative: add to main() and send valid text
The test was never called from main() and sent empty string which would
raise immediately via _run_cli. Send a space character instead and add
the test to the main() call list.
* Respect --id-format in text output and resolve workspace refs across windows
Text-mode list commands (list-workspaces, list-panes, list-panels,
list-pane-surfaces, surface-health) now honor --id-format for plain output,
not just --json. Added textHandle() helper that picks ref/id/both based on
the selected format.
resolveWorkspaceId now enumerates all windows when resolving a workspace
ref, so notify/claude-hook work correctly in multi-window sessions where
the target workspace may be in a non-active window.
* Preserve escape-sequence semantics for send text
The v1 server unescaped \n, \r, \t in send payloads before injecting into
the terminal. The v2 surface.send_text handler sends text verbatim. Add
CLI-side unescapeSendText() to restore the same behavior: \n and \r map
to carriage return (Enter key), \t maps to tab. Applied to send and
send-panel commands.
* Reject malformed handles instead of passing through to server
All four normalize functions (window, workspace, pane, surface) now throw
a clear error for unrecognized handle formats instead of passing them
through. Previously, a typo like `--panel foo` would forward `foo` to the
server which would silently fall back to the focused surface.
* Allow cross-workspace surface refs in close-surface and strip plural ID arrays
Remove workspace-scoped pre-validation from close-surface so explicit
surface refs/UUIDs from other workspaces work without requiring
--workspace. The server resolves workspace from surface_id directly.
Malformed handles are already caught by normalizeSurfaceHandle.
Extend formatIDs to strip plural _ids/_refs array pairs (e.g.
surface_ids/surface_refs in pane.list output) based on --id-format,
matching the existing singular _id/_ref stripping behavior.
* Honor explicit --window over CMUX_WORKSPACE_ID env fallback
When --window is passed globally, skip the CMUX_WORKSPACE_ID env var
fallback so commands operate on the targeted window's selected workspace
instead of the caller's workspace from a different window. Affects all
migrated commands that use workspaceFromArgsOrEnv or inline env fallback.
* Socket access control: process ancestry check + file permissions
Redesign socket control modes from (off, notifications, full) to
(off, cmuxOnly, allowAll):
- cmuxOnly (default): uses LOCAL_PEERPID + sysctl process tree walk to
verify the connecting process is a descendant of cmux. External
processes (SSH, other terminals) are rejected.
- allowAll: hidden mode accessible only via CMUX_SOCKET_MODE=allowAll
env var, skips ancestry check. Legacy "full"/"notifications" env
values map here for backward compat.
- off: disables socket entirely.
Security hardening:
- Server: chmod 0600 on socket after bind (owner-only access)
- CLI: stat() ownership check before connect (reject fake sockets)
Removes per-command allow-list (isCommandAllowed) — once a process
passes the ancestry check, all commands are available.
Includes migration for persisted UserDefaults values and env var
aliases (cmux_only, cmux-only, allow_all, allow-all).
* Add /sync-branch skill for submodule + main sync
- Sidebar now shows git branch, listening ports, log entries, progress bars, and status pills with expand/collapse
- Fix localhost/127.0.0.1 URL parsing by checking before generic URL(string:) which misinterprets the scheme
- Remove custom Keychain distinct ID in favor of PostHog SDK's built-in anonymous ID
- browser open now defaults to caller's workspace via CMUX_WORKSPACE_ID env var
- Improve CLI help text for environment variables
Instead of creating a merged config directory and injecting
CLAUDE_CONFIG_DIR on every terminal spawn, place a thin wrapper
script at Resources/bin/claude that intercepts claude invocations
to inject --session-id and --settings flags. This eliminates
blocking I/O on terminal creation and removes config management
complexity.
- Add Resources/bin/claude wrapper script with hook injection
- Add shell integration PATH fix (re-prepend after .zshrc/.bashrc)
- Add transcript reading for richer stop notifications
- Add set_status/clear_status to notifications socket allowlist
- Add Settings toggle to disable Claude Code integration
- Update docs to reflect automatic integration approach
- Unset CLAUDECODE env var to avoid nested session detection
* Fix blank terminal after split operations and add visual tests
## Blank Terminal Fix
- Add `needsRefreshAfterWindowChange` flag in GhosttyTerminalView
- Force terminal refresh when view is added to window, even if size unchanged
- Add `ghostty_surface_refresh()` call in attachToView for same-view reattachment
- Add debug logging for surface attachment lifecycle (DEBUG builds only)
## Bonsplit Migration
- Add bonsplit as local Swift package (vendor/bonsplit submodule)
- Replace custom SplitTree with BonsplitController
- Add Panel protocol with TerminalPanel and BrowserPanel implementations
- Add SidebarTab as main tab container with BonsplitController
- Remove old Splits/ directory (SplitTree, SplitView, TerminalSplitTreeView)
## Visual Screenshot Tests
- Add test_visual_screenshots.py for automated visual regression testing
- Uses in-app screenshot API (CGWindowListCreateImage) - no screen recording needed
- Generates HTML report with before/after comparisons
- Tests: splits, browser panels, focus switching, close operations, rapid cycles
- Includes annotation fields for easy feedback
## Browser Shortcut (⌘⇧B)
- Add keyboard shortcut to open browser panel in current pane
- Add openBrowser() method to TabManager
- Add shortcut configuration in KeyboardShortcutSettings
## Screenshot Command
- Add 'screenshot' command to TerminalController for in-app window capture
- Returns OK with screenshot ID and path
## Other
- Add tests/visual_output/ and tests/visual_report.html to .gitignore
* Add browser title subscription and set tab height to 30px
- Subscribe to BrowserPanel.$pageTitle changes to update bonsplit tabs
- Update tab titles in real-time as page navigation occurs
- Clean up subscriptions when panels are removed
- Set bonsplit tab bar and tab height to 30px (in submodule)
* Fix socket API regressions in list_surfaces, list_bonsplit_tabs, focus_pane
- list_surfaces: Remove [terminal]/[browser] suffix to keep UUID-only format
that clients and tests expect for parsing
- list_bonsplit_tabs --pane: Properly look up pane by UUID instead of
creating a new PaneID (requires bonsplit PaneID.id to be public)
- focus_pane: Accept both UUID strings and integer indices as documented
* Fix browser panel stability and keyboard shortcuts
- Prevent WKWebView focus lifecycle crashes during split/view reshuffles
- Match bracket shortcuts via keyCode (Cmd+Shift+[ / ], Cmd+Ctrl+[ / ])
- Support Ghostty config goto_split:* keybinds when WebView is focused
- Add focus_webview/is_webview_focused socket commands and regression tests
- Rename SidebarTab to Workspace and update docs
* Make ctrl+enter keybind test skippable
Skip when the Ghostty keybind isn't configured or when osascript can't send keystrokes (no Accessibility permission), so VM runs stay green.
* Auto-focus browser omnibar when blank
When a browser surface is focused but no URL is loaded yet, focus the address bar instead of the WKWebView.
* Stabilize socket surface indexing
* Focus browser omnibar escape; add webview keybind UI tests
- Escape in omnibar now returns focus to WKWebView\n- Add UI tests for Cmd+Ctrl+H pane navigation with WebKit focused (including Ghostty config)\n- Avoid flaky element screenshots in UpdatePillUITests on the UTM VM
* Fix browser drag-to-split blanks and socket parsing
* Fix webview-focused shortcuts and stabilize browser splits
- Match ctrl/shift shortcuts by keyCode where needed (Ctrl+H, bracket keys)
- Load Ghostty goto_split triggers reliably and refresh on config load
- Add debug socket helpers: set_shortcut + simulate_shortcut for tests
- Convert browser goto_split/keybind tests to socket-based injection (no osascript)
- Bump bonsplit for drag-to-split fixes
* Fix split layout collapse and harden socket pane APIs
* Stabilize OSC 99 notification test timing
* Fix terminal focus routing after split reparent
* Support simulate_shortcut enter for focus routing test
* Stabilize terminal focus routing test
* Fix frozen new terminal tabs after many splits
* Fix frozen new terminal tabs after splits
* Fix terminal freeze on launch/new tabs
* Update ghostty submodule
* Fix terminal focus/render stalls after split churn
* Fix nested split collapsing existing pane
* Fix nested split collapse + stabilize new-surface focus
* Update bonsplit submodule
* Fix SIGINT test flake
* Remove bonsplit tab-switch crossfade
* Remove PROJECTS.md
* Remove bonsplit tab selection animation
* Ignore generated test reports
* Middle click closes tab
* Revert unintended .gitignore change
* Fix build after main merge
* Revert "Fix build after main merge"
This reverts commit 16bf9816d0856b5385d52f886aa5eb50f3c9d9a4.
* Revert "Merge remote-tracking branch 'origin/main' into fix/blank-terminal-and-visual-tests"
This reverts commit 7c20fb53fd71fea7a19a3673f2dd73e5f0c783c4, reversing
changes made to 0aff107d787bc9d8bbc28220090b4ca7af72e040.
* Remove tab close fade animation
* Use terminal.fill icon
* Make terminal tab icon smaller
* Match browser globe tab icon size
* Bonsplit: tab min width 48 and tighter close button
* Bonsplit: smaller tab title font
* Show unread notification badge in bonsplit tabs and improve UI polish
Sync unread notification state to bonsplit tab badges (blue dot).
Improve EmptyPanelView with Terminal/Browser buttons and shortcut hints.
Add tooltips to close tab button and search overlay buttons.
* Fix reload.sh single-instance safety check on macOS
Replace GNU-only `ps -o etimes=` with portable `ps -o etime=` and
parse the dd-hh:mm:ss format manually for macOS compatibility.
* Centralize keyboard shortcut definitions into Action enum
Replace per-shortcut boilerplate with a single Action enum that holds
the label, defaults key, and default binding for each shortcut. All
call sites now use shortcut(for:). Settings UI is data-driven via
ForEach(Action.allCases). Titlebar tooltips update dynamically when
shortcuts are changed. Remove duplicate .keyboardShortcut() modifiers
from menu items that are already handled by the event monitor.
* Fix WKWebView consuming app menu shortcuts and close panel confirmation
Add CmuxWebView subclass that routes key equivalents through the main
menu before WebKit, so Cmd+N/Cmd+W/tab switching work when a browser
pane is focused. Fix Cmd+W close-panel path: bypass Bonsplit delegate
gating after the user confirms the running-process dialog by tracking
forceCloseTabIds. Add unit tests (CmuxWebViewKeyEquivalentTests) and
UI test scaffolding (MenuKeyEquivalentRoutingUITests) with a new
cmux-unit Xcode scheme.
* Update CLAUDE.md and PROJECTS.md with recent changes
CLAUDE.md: enforce --tag for reload commands, add cleanup safety rules.
PROJECTS.md: log notification badge, reload.sh fix, Cmd+W fix, WebView
key equiv fix, and centralized shortcuts work.
* Keep selection index stable on close
* Add concepts page documenting terminology hierarchy
New docs page explaining Window > Workspace > Pane > Surface > Panel
hierarchy with aligned ASCII diagram. Updated tabs.mdx and splits.mdx
to use consistent terminology (workspace instead of tab, surface
instead of panel) and corrected outdated CLI command references.
* Update bonsplit submodule
* WIP: improve split close stability and UI regressions
* Close terminal panel on child exit; hide terminal dirty dot
* Fix split close/focus regressions and stabilize UI tests
* Add unread Dock/Cmd+Tab badge with settings toggle
* Fix browser-surface shortcuts and Cmd+L browser opening
* Snapshot current workspace state before regression fixes
* Update bonsplit submodule snapshot
* Stabilize split-close regression capture and sidebar resize assertions
* Change default Show Notifications shortcut from Cmd+Shift+I to Cmd+I
* Fix update check readiness race, enable release update logging, and improve checking spinner
* Restore terminal file drop, fix browser omnibar click focus, and add panel workspace ID mutation for surface moves
* Add Cmd+digit workspace hints, titlebar shortcut pills, sidebar drag-reorder, and workspace placement settings
* Add v2 browser automation API, surface move/reorder commands, and short-handle ref system to TerminalController
* Add CLI browser command surface, --id-format flag, and move/reorder commands
* Extend test clients with move/reorder APIs, ref-handle support, and increased timeouts
* Harden test runner scripts with deterministic builds, retry logic, and robust socket readiness
* Stabilize existing test suites with focus-wait helpers, increased timeouts, and API shape updates
* Add terminal file drop e2e regression test
* Add v2 browser API, CLI ref resolution, and surface move/reorder test suites
* Add unit tests for shortcut hints, workspace reorder, drop planner, and update UI test stabilization
* Add cmux-debug-windows skill with snapshot script and agent config
* Update project docs: mark browser parity and move/reorder phases complete, add parallel agent workflow guidelines
* Update bonsplit submodule: re-entrant setPosition guard, tab shortcut hints, and moveTab/reorderTab API
* Add browser agent UX improvements: snapshot refs, placement reuse, diagnostics, and skill docs
- Upgrade browser.snapshot to emit accessibility tree text with element refs (eN)
- Add right-sibling pane reuse policy for browser.open_split placement
- Add rich not_found diagnostics with retry logic for selector actions
- Support --snapshot-after for post-action verification on mutating commands
- Allow browser fill with empty text for clearing inputs
- Default CLI --id-format to refs-first (UUIDs opt-in via --id-format uuids|both)
- Format legacy new-pane/new-surface output with short surface refs
- Add skills/cmuxterm-browser/ and skills/cmuxterm/ end-user skill docs
- Add regression tests for placement policy, snapshot refs, diagnostics, and ID defaults
* Update bonsplit submodule: keep raster favicons in color when inactive
* Fix zsh ZDOTDIR wrapper + log parsing with -- messages
* Fix CI race condition: serialize self-hosted builds with concurrency group
Two workflows racing on the same self-hosted runner caused DerivedData
corruption (release's rm -rf nuked DerivedData while CI was building).
Add shared concurrency group and scope DerivedData cleanup to project.
* Add --panel flag to new-split command
Allows splitting a specific panel without changing focus first.
Usage: cmuxterm new-split <direction> [--panel <id|index>]
Example: cmuxterm new-split down --panel 1
* Return new panel ID from new-split command
new-split now returns the UUID of the newly created panel, enabling
reliable chaining of split operations without index drift issues.
Before: OK
After: OK F2675177-3838-49AF-A1A0-1744C0048E99
Example workflow to create left + 2x2 grid on right:
RIGHT=$(cmuxterm new-split right | awk '{print $2}')
BOTTOM=$(cmuxterm new-split down --panel $RIGHT | awk '{print $2}')
cmuxterm new-split right --panel $RIGHT
cmuxterm new-split right --panel $BOTTOM