Add cmux left-sidebar toggle|show|hide|focus|mode

AL
2d ago
+2001-151

Summary

cmux has no command for the left (workspace) sidebar. To hide it from a script, or check whether it's showing, you have to press View → Toggle Left Sidebar through macOS Accessibility and read the window's accessibility tree. That grants cmux, and every shell and agent running in it, control of the whole Mac. It also broke on 0.64, when the menu item was renamed.

This adds cmux left-sidebar toggle|show|hide|focus|mode, the left-side sibling of cmux right-sidebar, with the same --window <id|ref|index> and --workspace <id|ref|index> targeting. Without a target it acts on the window ⌘B would toggle.

  • toggle, show and hide change that window's sidebar directly and never make a window key or activate the app, so a background script can hide the sidebar without taking over the screen. This is stricter than right-sidebar on purpose: its toggle and show may move focus.
  • focus brings the window forward, shows the sidebar if needed and focuses the workspace list through the existing Focus Sidebar path. It's the only left_sidebar command the socket focus policy lets move focus.
  • mode prints {"visible":bool}. The issue asked for state printing shown|hidden. I matched right-sidebar mode instead, without its mode field since the left sidebar has none, so scripts read both the same way. I can add a state alias if you'd prefer.
  • A sidebar the window collapsed for lack of room reports visible: false, and show and toggle bring it back the way ⌘B does. hide also clears that auto-collapse, so the sidebar doesn't come back when the window widens.

set, the per-mode aliases and --no-focus only make sense for the right sidebar, so they're rejected. Over the socket this is the V1 left_sidebar line, routed through CmuxControlSocket like right_sidebar. There's no v2 method, and the cmux ssh relay never forwards V1 lines, so no relay allowlist change is needed.

Fixes #15196

Commits

The first three are behavior-preserving refactors, so the feature can reuse right-sidebar's code instead of copying it:

  1. Rename the right-sidebar targeting helpers to neutral names (SidebarRemoteTarget, sidebarRemoteContext, resolveSidebarWindowId, resolveSidebarWorkspaceId, resolvedSidebarHandleID).
  2. Move right-sidebar's CLI flag parsing and forwarding, and the app's --workspace/--tab/--window reading, into helpers that take each sidebar's own messages. The app-side helpers live in Sources/SidebarRemoteArguments.swift. Right-sidebar replies are unchanged.
  3. Pull ⌘B's window choice out of toggleSidebarInActiveMainWindow into sidebarToggleTarget(), so the CLI picks the same window without also making it key.
  4. The feature.
  5. The tests.

After review, with each regression test committed before its fix: a blank --window or --workspace value now fails instead of falling back to the default window, for right-sidebar too. The global --window no longer focuses a left-sidebar target before the command runs. The parser and focus-policy tests call the real code instead of DEBUG-only wrappers.

If you'd rather keep this smaller, I can drop commit 2 and give left-sidebar its own parsing instead.

Testing

On macOS 26.6.2 with Xcode 26.6, at b2d226640 on top of main 5da8121f9:

  • CMUX_DEV_BACKEND_MODE=local ./scripts/reload.sh --tag left-sidebar --build-only built the app.
  • tests/test_cli_left_sidebar.py (new, 5 tests) and tests/test_cli_right_sidebar_aliases.py pass against that build's CLI and a fake socket that records requests. They cover what each action sends, mode output, target forwarding, and rejected input (no subcommand, state, set files, files, extra words, --no-focus, missing or blank --window/--workspace values) never reaching the socket, and the global --window sending no window.focus. The 7 cases added after review failed on dc92b7798 and pass now. The right-sidebar test passing is the check that commit 2 kept its CLI behavior.
  • swift test --package-path Packages/macOS/CmuxControlSocket: 627 tests pass, including 3 new left_sidebar coordinator tests and left_sidebar in the main-actor policy test.
  • ./scripts/test-unit.sh build-for-testing compiles the 7 new app tests in cmuxTests/LeftSidebarRemoteCommandTests.swift: parsing, the focus policy, state changes, window and workspace targeting, the ⌘B default target, hide clearing auto-collapse, and the no-window error. I haven't run them locally, so they first run in CI.
  • python3 scripts/verify-local.py (17/17) and git diff --check pass.
  • Manual run in a tagged build of an earlier commit (81ac3c1a7, before the parsing refactor): visibility and mode, agreement with ⌘B and View → Toggle Left Sidebar, hide/show from a background terminal leaving focus and window order alone, window and workspace targeting, narrow-window auto-collapse, rejected input, and right-sidebar all behaved as described. The final commit hasn't been run in an app; for the refactor, the CLI tests above are the evidence.

Localization: 16 new keys (leftSidebar.remote.error.* and cli.leftSidebar.*, including the usage text) in all 9 locales, added with ./scripts/localize-changes. python3 scripts/localization_catalog.py check reports 0 parity errors. Like their neighbors, the V1 help line, the top-level synopsis line and a DEBUG-only test message aren't localized.

Not fixed here

Manual testing turned these up. I left them out to keep this PR to one feature:

  • With both sidebars open, focus leaves keyboard focus in the right sidebar. The Focus Sidebar shortcut does the same, and this PR doesn't change its code, so it looks like focusSidebarForKeyboard (Sources/AppDelegate+SidebarKeyboardFocus.swift): it focuses the list once and doesn't take focus back.
  • focus on a hidden sidebar shows it, but the list doesn't keep keyboard focus, probably the same function focusing before the sidebar finishes appearing. I haven't checked whether the shortcut fails this way.

I can open an issue for the first one if it's worth fixing separately.

Changelog

Added: cmux left-sidebar toggle|show|hide|focus|mode shows, hides, focuses and reads the left sidebar from scripts, without Accessibility

Demo

Rejected input stops before the CLI connects, with exit status 1:

$ cmux left-sidebar state
Error: Unknown left-sidebar command 'state'
$ cmux left-sidebar show --no-focus
Error: left-sidebar: unknown flag '--no-focus'
$ cmux left-sidebar toggle extra
Error: left-sidebar toggle received unexpected arguments
$ cmux left-sidebar hide --window
Error: left-sidebar: --window requires an id

No screenshots, since nothing on screen changes beyond what ⌘B already does.

Checklist

  • Behavior changes have added or updated tests, or Testing says why not
  • UI, settings, menu, schema, help-text or user-facing docs change: localization audited, and the result is stated above
  • User-facing docs updated if needed (docs/cli-contract.md)
  • Reviewed with a subagent before merge (cmux-review), and all bot and human review comments resolved

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added CLI and remote-control support for the left sidebar, including toggle, show, hide, focus, and visibility checks.
    • Added optional workspace and window targeting for left-sidebar commands.
    • Visibility checks return JSON state; focus brings the relevant window forward and focuses the workspace list.
  • Bug Fixes
    • Improved validation and error reporting for invalid sidebar commands, options, and targets.
  • Documentation
    • Documented left-sidebar commands, targeting, and behavior in CLI help and the command reference.