cmux NEXT: Share Chat with Guest as one op (palette, tab menu, CLI, MCP) (carries #26861)

Merged
TE
6d ago
+14838-9348

Carries #26861 onto current feat-cmux-next. Its prerequisite #26825 (the daemon's owner_token on agent-session-share-create) has landed, and #26861's owner session is gone, so its one commit (f77d3f117d9, same author) is cherry-picked here alone, without the #26825 commits its branch carried. The only conflict was ActionCatalog.xcstrings; its 10 new keys are added unchanged and no other entry changes. The diff is exactly the original commit's (+3,954, 16 files).

Review fix (615630d50cd, BLOCK at 2dc76d993001): tab lookup spans every machine, so Share with Guest could name a chat on an SSH machine, mint the owner's install token and send it to that machine's daemon, and share list with a remote target used its surface number against this Mac's shares. Both now refuse a chat that is not on this Mac's daemon ("Only a chat on this Mac can be shared.", all 21 locales) before any token is minted. Test: AgentShareTests.aChatOnAnotherMachineIsNotThisMacs.

Review fix (996619d4ced, BLOCK at 615630d50cd7): a surface or tab target that names nothing now answers not_found (ActionRegistry.refuseNotFound), not unavailable. Test: AgentShareTests.aMissingChatAnswersNotFound.

Base merged at dba4621ed84. Review fix (af23163dd68, Sol BLOCK at 996619d4cedb): a pane, workspace or other non-tab target, a malformed --surface or --uses, and --surface beside a tab target are refused instead of falling back to another chat. Create now carries the session the app named, and the daemon answers share.chat_moved when the surface shows another chat by the time the request arrives (cmux-tui-core agent_session_shares.rs, with a test). share revoke|kick positional ids may start with - (only the commands' own flags count as flags), and an id given both positionally and as its flag is refused. Tests: AgentShareTests.aSelectorThatCannotNameOneChatIsRefused, share_cli tests, agent_session_shares_tests.

More review fixes: 3a81ebfb8f9 (box BLOCK at f1a37a5d0846): positional ids now pass as --share-id=ID / --guest-id=ID, so an id that starts with -- arrives whole (a test runs the CLI's own action parser). 5edb45317ad (Sol P2 at af23163dd681): a share verb that times out reports mayHaveApplied, so a caller does not blindly retry a create the daemon may still finish, and the person is told to check cmux share list first. Fleet: Rust share_cli 6 and cmux-tui-core shares 35 passed with clippy clean at 3a81ebfb8f94; AgentShareTests passed at f1a37a5d0846.


Size: most of the +3,954 lines are generated localization (3,150 lines in two string catalogs); the share-operation logic is under about 800 lines.

Summary

Leo (2026-10-28), on Theo's T3 Code "Share with guest": "we want to be having, be able to do this kind of flow for CMux multiplayer ... we can work on the agent chat multiplayer." Plan and inventory: manaflow-ai/cmuxterm-hq#4766; spec: leo-workspace design/chat-multiplayer.md sections 1 and 6.

This is the owner's entry point for the link share, on every surface, as one op:

  • agent.share (Share Chat with Guest… in the palette; Share with Guest… in an agent chat tab's right-click menu, under More; no default shortcut). It names a started agent chat by --target tab:<id>, --surface N, the chat whose agent ran the CLI, else the focused chat, and calls the daemon's agent-session-share-create.
  • CLI: cmux share chat [--surface N] [--ttl 24h] [--uses 1] [--json] prints the guest URL alone (so | pbcopy works); --json gives {share_id, url, expires_at, uses_left, session, surface}. cmux share list, cmux share revoke ID, cmux share kick ID GUEST are the family (agent.share.list|revoke|kick): CLI and MCP only; the share sheet (next PR) offers them in place.
  • MCP: each of the four is a cmux mcp serve tool (MCP follows the CLI decision).
  • A person's palette or menu run copies the link and shows a toast ("Guest link copied. It works once and expires in 24 hours."); the share sheet replaces that in the follow-up PR.
  • Errors keep the daemon's code (share.needs_sign_in, share.room_unreachable, share.room_refused, share.not_a_chat, share.limit, …) with a sentence a person can act on.

Owner token. The app mints a fresh cmux install token for each create with MacInstallIdentity.installToken() (CloudService.installIdentity, the ES256 install principal from /v1/auth/token, the same environment as the rest of cmux Cloud) and hands it to the daemon in that one request; nothing stores it. Signed out, the request goes without one and a room-bridged daemon answers share.needs_sign_in. The CLI never sees the token: cmux share chat runs the app's action, which adds it.

Layout. The logic lives in CmuxNextDaemonService (AgentShareActions, behind an AgentShareHost protocol); the App adds only AppServices+AgentShareHost.swift (one-line forwards) and one bind line. Typed requests are CmuxNextDaemon/Requests/Agents/AgentSessionShares.swift. The CLI's positional share revoke ID / share kick ID GUEST map to the actions' --share-id / --guest-id in cli/share_cli.rs.

The generated action exports (action-surfaces.json, actions.md) come from the PR autofix's Mac regeneration.

Testing

  • CmuxNextAppTests/AgentShareTests: every surface is declared on the one op (palette, CLI share chat, MCP, the tab menu, no shortcut; the family is CLI and MCP only); a started chat's tab menu offers it and a new chat's or a terminal's does not; --surface / tab target resolution and refusals; --ttl parsing (1 min to 7 d); daemon refusals keep their code; the answer carries the URL.
  • CmuxNextDaemonTests/AgentSessionShareRequestTests: wire shape of create/list/revoke/kick (snake_case, absent options left out, which the daemon's deny_unknown_fields needs) and decoding of create and list answers.
  • share_cli Rust unit tests: positional ids, other words untouched (cmux host share stays the tailnet share), share chat prints only the URL.
  • Ran: prepush-gate.py (no new violations), localization_catalog.py check (no new errors; all 21 locales filled). CI results to follow on this head.

Observed / Not run

Not run yet: the Swift suites and the Rust tests on CI/fleet at this head (results posted as a comment), and a live create against a room (needs #26825 and the room PR).

Localization audit: 10 new action/argument titles (ActionCatalog.xcstrings) and 14 refusal/notice strings (new CmuxNextDaemonService AgentShare.xcstrings), each with all 21 locales; no bare English in the changed Swift.

Risk: normal. New op and verbs only; the token goes only to the owner's own local daemon over its same-user unix socket (the daemon refuses share verbs on any other connection).

Captures

Tab right-click > More on a started agent chat, 2x, cropped to the window. Before is the merge base 463abe2a1de5a2a8e77c53c229a51cc9bf0a58e4; after is 3e369eaa5c22805ef241e0fca84708c109472b9a.

BeforeAfter
Lightbefore lightafter light
Darkbefore darkafter dark

Viewed after image: yes
Visual verdict: SHIP: "Share with Guest..." adds a single row with its own divider between Rename Tab and Options in the More submenu, with no repeated control, and the menu still fits on screen in light and dark, so it sits fine next to ChatGPT or Claude.

Changelog

Added: Share Chat with Guest in the command palette, an agent chat tab's menu, cmux share chat|list|revoke|kick and MCP

Agent session: agent session

🤖 Generated with Claude Code