Remote chats start in the machine's work folder; refused starts offer one-click actions

Merged
TE
5d ago
+802-108

Leo, 2026-10-31, after "Couldn't start Claude Code: Open a terminal in a folder on fs-firstsend-exercise first; the chat starts there.": "let's work on improving this a LOT".

What it does

B. A chat on a remote machine with no folder no longer dead-ends.

  • cmux-tui adds the verb agent-session-folders {surface} (capability agent-session-folders-v1). It answers {default, folders}. default is the machine's work folder, $HOME/work; on a Cloud VM it is created (0755) if missing, and on a Mac or SSH host nothing is created. folders is the work folder and its direct subfolders, repositories first, never hidden folders or symlinks. Callers are the same as agent-session-trust: a trusted local connection, on an agent tab of this store. A listed folder grants no trust; starting there still goes through agent-session-start's machine-kind gate and the folder trust question.
  • Swift stays thin. AgentTabStore.runRemoteStart falls back to that default when the chat has no chosen, seed or workspace folder. The lookup uses its own short connection, so the start's owner-checked connection is used for the start alone. An older daemon still answers app.folder_needed.
  • Choose folder on a remote chat lists that machine's own folders after its workspaces' folders, each once.
  • Copying the folder stays the 0-to-1 Cloud send's job (cloud.startChat), unchanged.

C. A refused start names the one-click way out.

  • The failed start card (SwitchNotice, "Couldn't start {agent}: …") reads the refusal's code (startRefusal.ts, keyed on the code, never the words). It offers New Cloud Machine for agent_session.team_vm_blocked and Choose Folder… for app.folder_needed, beside Retry. Any other code shows Retry only, as before.
  • The copy changes:
    • teamVM: "%@ is a teammate's machine, so its owner starts chats there. Pick your own machine, or create one with New Cloud Machine."
    • folderNeeded: "%@ has no work folder to start in. Use Choose folder to pick one there."
    • Other languages are marked needs_review.
  • Since #29107, a machine's creator is never refused on their own machine, so this card only reaches people starting on a teammate's machine. "Ask the owner" / "Share with me" come with the grant flow (cmux-platform chat grants), in its own PR.

Screenshots

Gallery entry agent-pane.switch-notice, rendered in Chromium at 520 px.

DarkLight
Teammate's machine
No work folder

Viewed: the action button sits beside Retry in the card's existing actions row, with a gap, and Retry takes the secondary style. Nothing repeats per row. The plain refusal (no action) still shows only Retry.

Prior art

#28111 / #29046 (cloud.startChat, which picks the machine and copies the folder; untouched), #28686 (composer Cloud row; untouched), #26323 (VM boundary). The start-in-Cloud lane (lane/start-chats-in-cloud) sends home: true with no cwd and relies on this fallback.

Tests

  • Rust (agent_session_folders.rs): no home or no work folder gives no default; a Cloud VM creates its missing work folder and a Mac doesn't; the list puts repositories first and skips files, hidden folders and symlinks; a work symlink is never the default.
  • Swift (AgentTabRemoteStartTests): a seed with no folder starts in /home/cmux/work, through the real attach client against a socket daemon. The folder-list merge keeps the workspaces' folders first, each once.
  • Web (startRefusal.test.ts): code-to-action mapping. Gallery agent-pane.switch-notice covers the team machine, no folder, and plain cases.
  • Fleet:
    • cmux-ci compile-check --app --recipe-version 2 at 820176a0: app passed (71 s), cmux-tui passed (38 s).
    • Rust fmt, clippy and tests: pass (130 core, 37 CLI).
    • Webview bun tests pass (4/4 new plus the harnessSwitch tests). The webview tsc error is in test/latency/probe.ts (interactionId), a file this PR doesn't touch.

Deploy: none. The app ships its own cmux-tui and pushes it to the VM.

Agent session: agent session

🤖 Generated with Claude Code