Skip to content

Keep agent- and script-opened workspaces and panes in the background - #15281

Merged
teamleaderleo merged 4 commits into
mainfrom
fix/agent-opened-ui-no-focus
Sep 28, 2026
Merged

teamleaderleo merged 4 commits into
mainfrom
fix/agent-opened-ui-no-focus

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Agent-opened workspaces and panes stole focus mid-typing. cmux cloud workspace new <vm> had no way to stay in the background and selected the new workspace immediately. cmux's socket policy (skills/cmux-socket-policy) already says CLI commands must not steal focus without explicit intent. The vm/cloud/surface family broke that policy in three ways:

  • Lost policy frame. These handlers run in v2VmCall's detached Task, where the thread-local focus allowance is empty. SurfacePaneFactory treats an empty allowance as allowed.
  • Focus by default. surface.project, surface.new_terminal, vm.terminal_open, vm.terminal_new, vm.workspace_new and vm.workspace_open all defaulted focus to true.
  • Selection ignored the flag. SurfacePaneFactory.createLocalWorkspace always selected the new workspace and switched the active window, even with focus: false. That is the path behind vm workspace open and vm workspace new --reuse.

The rule now

A UI-opening CLI command takes focus only when a person ran it interactively. A coding agent or script gets a background open plus an unread dot.

  • Explicit flags win. --focus, --focus true|false, --focus=<bool> and --no-focus parse the same way on every command, including cmux agent <kind> and cmux ssh. In cmux agent, a value after --focus counts only if it is exactly true/false/1/0/yes/no.
  • Env override. Without a flag, CMUX_FOCUS_NEW=1|0 decides.
  • Otherwise the caller decides. The command focuses only when stdin and stdout are both TTYs and the environment is not a coding agent. Agents are detected with the existing isCodingAgentEnvironment: CLAUDECODE, CODEX_THREAD_ID, OPENCODE, CMUX_AGENT_LAUNCH_KIND, and so on, plus AI_AGENT, which is new.
  • Explicit focus param. These CLI paths always send one. Server defaults for the six handlers above are now false for any other socket client.
  • Selection honors focus. createLocalWorkspace / addWorkspaceInPreferredMainWindow take select:, so a background workspace no longer switches window or selection.
  • Unread dot. A pane opened in the background gets the restored-unread indicator. It clears when the person selects the workspace, clicks the pane or types, and it is skipped when the person is already looking at that pane.
  • Clicks in the app still focus. App-launched CLI runs (Machines panel actions, Base, ssh:// links) set CMUX_FOCUS_NEW=1, so those clicks focus as before.
  • vm agent lands beside the caller. It now passes the caller's workspace and surface, so its terminal splits next to the agent instead of into whatever workspace is selected. If that context is stale (the tab moved), it retries once against the selected workspace.

Commands covered

The audit covered every CLI path that opens UI:

Command Before Now
vm/cloud workspace new, workspace open always focused, no flag helper default, --focus/--no-focus
vm new, base open, base reset, fork, restore, shell/attach, ssh, tui, open, dev, layout apply --open focused by default helper default, flags
vm agent (without --no-open) focused split in the selected workspace helper default, splits in the caller's workspace
surface open, surface new-terminal focused by server default helper default, flags
`cmux open <file url>` focused by default
cmux ssh, ssh-tmux selected unless --no-focus helper default
new-workspace, new-split, new-pane, new-surface, markdown open, browser open, vm desktop, port opens already background by default unchanged
tmux split-window focus unless -d (tmux semantics) unchanged, comment fixed

Left as is on purpose: project open and ssh-session-attach, since both are explicit user flows.

Follow-ups (design, for the team)

Where agent-opened panes land is a feature call: a right-side background split, the right-sidebar dock, or a new workspace. It is not part of this fix. Today a background open keeps each command's current placement and adds the unread dot.

Related, not touched here: #15160 (pane drag), #14894 (focus pulse), #15229 (sidebar text weight). Companion sidebar model fix: #15260.

Tests

  • docs/cli-contract.md help probes are updated for the new flags, and cmux ssh --help has a new probe.

  • CLICoderouterCommandTests covers cmux agent --focus parsing.

  • SurfaceSocketCommandTests: the default is background, explicit true is honored, and a background open marks the pane unread.

  • tests/test_cli_vm_open_device_target.py: the background default, --focus, --no-focus and CMUX_FOCUS_NEW.

  • CLI tests that relied on the old focus default now pass --focus or expect background.

Fleet dogfood evidence follows in a comment.

🤖 Generated with Claude Code

`cmux cloud workspace new <vm>` selected the new workspace while the person
was typing elsewhere. The vm/cloud and surface open commands defaulted focus
to true on both sides: the CLI never sent a focus value, and the socket
handlers read a missing `focus` as true while running in `v2VmCall`'s detached
task, where no socket focus policy frame applies.

CLI: a new `defaultFocusForUserOpen()` decides the default for every open
command. It focuses only for an interactive run (a terminal on stdin and
stdout, no coding agent environment; `AI_AGENT` joins the agent key list).
`CMUX_FOCUS_NEW=1|0` overrides it, and `--focus` / `--no-focus` always win.
vm/cloud new, base open/reset, fork, restore, shell/attach, ssh, tui, open,
workspace new/open and agent, `surface open`/`new-terminal`, `cmux open` and
`cmux ssh`/`ssh-tmux` take the flags and always send an explicit focus.
`vm agent` now opens its pane beside the caller instead of in whichever
workspace is selected. The app sets `CMUX_FOCUS_NEW=1` when it launches the
CLI for a click (Machines panel, Base, ssh:// links), so those still focus.

Server: vm.* and surface.* open handlers default `focus` to false. Creating a
local workspace for a group honors focus, so a background open no longer
switches the window or workspace. A pane opened without focus gets the
unread dot, which clears when the person selects the workspace, clicks the
pane or types in it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 2 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 3c1f3007-c000-4844-87d7-3ffcfb75c3a4

📥 Commits

Reviewing files that changed from the base of the PR and between b36339a and 59edd39.

📒 Files selected for processing (33)
  • CLI/CMUXCLI+SSHHelp.swift
  • CLI/CMUXCLI+SessionMove.swift
  • CLI/CMUXCLI+VMDev.swift
  • CLI/CMUXCLI+VMLayoutEnv.swift
  • CLI/CMUXCLI+VMTransfer.swift
  • CLI/CMUXCLI+VMTui.swift
  • CLI/cmux.swift
  • CLI/cmux_open.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate+CmuxSSHURL.swift
  • Sources/AppDelegate.swift
  • Sources/CloudVMActionLauncher.swift
  • Sources/Surfaces/CmuxTuiRemoteRouting.swift
  • Sources/Surfaces/LocalSurfaceProvider.swift
  • Sources/Surfaces/SurfaceCatalog+Groups.swift
  • Sources/Surfaces/SurfaceCatalog+WindowWorkspaceHost.swift
  • Sources/Surfaces/SurfacePaneFactory.swift
  • Sources/Surfaces/SurfaceSocketCommands.swift
  • cmuxTests/CLICoderouterCommandTests.swift
  • cmuxTests/CloudSidebarConsistencyTests.swift
  • cmuxTests/CloudWorkspaceTargetingTests.swift
  • cmuxTests/SurfaceCatalogTests.swift
  • cmuxTests/SurfaceSocketCommandTests.swift
  • cmuxTests/VMDefaultCloudCommandTests.swift
  • cmuxTests/VMSSHCommandTests.swift
  • cmuxTests/VMSSHTransportFallbackTests.swift
  • cmuxTests/WorkspaceRemoteConnectionTests.swift
  • docs/cli-contract.md
  • skills/cmux-cloud-vm/references/agent-workflows.md
  • skills/cmux-cloud-vm/references/commands.md
  • skills/cmux-cloud-vm/references/sidebar-parity.md
  • skills/cmux-socket-policy/SKILL.md
  • tests/test_cli_vm_open_device_target.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Dogfood build of 59edd395e6fa5a1652de80f905d827ce468eda3d

cmux DEV pr-15281-59edd395.app

The link opens this exact commit in the cmux dev menu bar app. The build starts on each push and the page waits until it is ready; a newer push replaces it. It signs in against production, so Cloud or backend changes still need a tagged build with a development backend.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

CI failure attribution

CI passes on 59edd395e6 (run 36427054014 attempt 2).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's.

teamleaderleo and others added 3 commits September 28, 2026 05:45
- docs/cli-contract.md: vm agent and vm open help probes and table rows match
  the new focus flags; add a Focus on open row and an ssh help probe.
- vm dev and vm layout apply --open take --focus/--no-focus and send
  focus explicitly (default: interactive only).
- One focus flag grammar (--focus, --focus true|false, --focus=<bool>,
  --no-focus) for vm workspace new/open, vm agent, cmux ssh and ssh-tmux.
  The agent alias router reads a value after --focus only when it is
  exactly true/false/1/0/yes/no.
- vm agent retries once without the caller workspace when that stale
  context is rejected; the server validates it before creating anything.
- amp, grok, hermes-agent (and the other launch kinds) count as coding
  agent environments for the focus default.
- Update cmux-cloud-vm skill references and add router focus tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The warning budget flagged `case "--no-focus", "--focus", _ where ...`:
the `where` binds only the last pattern, which was intended but reads as
if it guarded all three.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…request's

Dogfooding `CLAUDECODE=1 cmux surface new-terminal --machine local` on a
PR build showed focus still jumping to the new terminal. The local
provider first makes the pane with focus: true in the selected workspace
and only then projects it to its destination with the requested focus, so
the staging step stole focus before the request's focus: false applied.
Also update the top-level `cmux surface` usage lines to the new flags.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Dogfood round 1 (fleet mini). I compared release 0.64.19 with this PR's CI-built app at e6c9914, driving both over the socket. Each case runs cmux open https://example.com in a fresh workspace and reads identify before and after.

Caller 0.64.19 focus after PR focus after
Agent (CLAUDECODE=1, no TTY) browser (stolen) terminal (kept)
Person (ssh -t, TTY, no agent env) browser browser (still focuses)
Before (agent run) After (agent run)
before after

Found and fixed while dogfooding: CLAUDECODE=1 cmux surface new-terminal --machine local still moved focus. The cause: LocalSurfaceProvider.createTerminal stages the pane with focus: true before projecting it with the request's focus. Fixed in 59edd39. Round 2 re-checks that path on the new CI build.

The unread dot covers panes created through the vm/surface handlers. A cmux open split lands in the workspace you are already looking at, so it gets no dot.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Dogfood round 2, on a fleet mini running the CI build of 59edd39 (the fix that makes the local surface new-terminal staging pane stop taking focus):

Caller Command Focus before Focus after
Agent (CLAUDECODE=1, piped) cmux surface new-terminal --machine local 1B0754C2 1B0754C2, unchanged; new terminal opens in the background with an unread badge
Person (TTY) cmux surface new-terminal --machine local 5C1A2512 148511F6, the new terminal

agent new-terminal keeps focus

The focus ring stays on the original pane. "Terminal 2" opened next to it, and the sidebar row shows the unread badge.

teamleaderleo added a commit that referenced this pull request Sep 28, 2026
@teamleaderleo
teamleaderleo merged commit 7b0a15f into main Sep 28, 2026
99 of 104 checks passed
@teamleaderleo
teamleaderleo deleted the fix/agent-opened-ui-no-focus branch September 28, 2026 14:51
@github-actions

Copy link
Copy Markdown
Contributor

Merge receipt for 59edd395e6: every check was green at merge (19 verified; 14 skipped by policy). Full suite runs on main after merge.

rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 28, 2026
1028a08 test: isolate background workspace git probe fixture (manaflow-ai#15388)
41ad40d fix: keep the terminal area when the window is too narrow for the side panels (manaflow-ai#15369)
2890f0b Roll the Base create back when the owner network resolve fails (manaflow-ai#15358)
7b0a15f Keep agent- and script-opened workspaces and panes in the background (manaflow-ai#15281)
4f14fa3 ci: move CLI regressions to CLI product tests and rebalance the seven app-host shards (manaflow-ai#15177)
906926a ci: dogfood builds are opt-in with the dev-build label (manaflow-ai#15380)
2f6716c PR media: classify app changes by CI's build inputs; a reuse error is no refusal (manaflow-ai#15386)
bc28bc4 Release the Base generation when a create is refused for credits (manaflow-ai#15343)
6760c93 iOS: Add Computer never disturbs the active Mac (manaflow-ai#15102)
0f2d3d3 Show Claude sessions that stop on an API error instead of leaving them Running (manaflow-ai#15232)
20ef7c9 Keep the main window floor on the animating setFrame path (manaflow-ai#15368)
b4f5dc5 ci: move UI runs pinned to Blacksmith macOS 26 onto owned Macs (manaflow-ai#15383)
e02c385 PR media: compile once when CI's build cannot load, and say why a tour skipped (manaflow-ai#15378)
ebd1f4f fix(iroh-v2): commit delivery accounting only after the frame is sent (manaflow-ai#15344)

# Conflicts:
#	.github/workflows/ci-guards.yml
#	.github/workflows/ci-macos.yml
#	.github/workflows/ci.yml
#	.github/workflows/pr-media.yml
#	.github/workflows/test-e2e.yml
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant