cmux NEXT: view a Claude Code or Codex session without taking it over

Merged
TE
17d ago
+2693-69

Leo wants to see a Claude Code or Codex session open in cmux NEXT next to the app that runs it, to compare the UI and move over gradually, without cmux taking the session away. This adds a View Only open: cmux NEXT shows the chat and follows the app as it writes, and it never resumes, writes to or locks the original. Take Over is a separate, explicit step. Viewing never blocks starting a session: new chats, harness picks and folders work as before, and the viewer always offers a new chat of its own.

Behavior

  • acpmux watch mode. session/new with adopt.watch: true (Claude Code or Codex) looks up the transcript the way adoption does: ~/.claude/projects/**/<id>.jsonl, or a Codex rollout-…-<id>.jsonl. It does not start the harness. The chat replays the transcript into acpmux history, then reads only new complete lines every 400 ms and shows each prompt and answer as it lands. It stores the file and the byte offset (SessionMeta.watching), so a daemon restart picks up where it stopped. A transcript that is replaced or truncated is read again from the start.
  • Nothing touches the original. The transcript file is opened read-only, and no agent process starts, so there is no --resume, session/load or lock. A prompt is refused with watch.read_only, and web origins cannot use _acpmux/watch_take_over (watch.origin_refused).
  • Take Over. _acpmux/watch_take_over ends the watch. The next prompt resumes the conversation as an ordinary adopted chat. If the transcript was written in the last few minutes, Take Over first answers with the same adopt.live details an adopt does. The page then asks whether to fork the chat (Claude Code only) or open it anyway.
  • Continue in New Chat. The View Only banner offers a way on beside Take Over. For Claude Code, Continue in New Chat forks the conversation into a new chat (claude --resume <id> --fork-session), so the original is only read. For Codex, which cannot be forked while another app runs it, New Chat Here starts a Codex chat in the same folder. The viewed chat stays as it is. An adopt with ifLive: fork now always makes a new chat; before, it could return the chat that already adopted or viewed the conversation. This only adds a button and changes no existing flow, so it has no Debug toggle.
  • Agent pane. Session summaries carry watching: "claude" | "codex". The pane shows "Viewing Claude Code session" with a Take Over button, and the composer is off with "Take over this session to send messages".
  • Entry points, one path. The All chats row menu's View Only (chats.watch), the palette's View Chat Only (agentChat.view) and cmux agent view <chat> all call ChatsOpenCoordinator.watch. That coordinator is the Open Chat path with adopt.watch set. <chat> is an index key (claude-code:<id>, codex:<id>), a transcript path, or a bare session id; a bare id is Claude Code's when its transcript is under ~/.claude/projects, otherwise Codex's. cmux agent view show|ensure|… keep their sidebar-view meaning.

Supporting changes:

  • hub/lifecycle.rs was at the 1000-line limit. Its route check moves to route_switch.rs::check_new_route.
  • cli.rs is at 993 lines. The positional chat for agent view lives in cli/agent_view.rs.
  • server/requests.rs stays under the line limit: adopt and Take Over share one IfLive::parse.
  • CmuxNextApp does not grow (its size cap): ChatOpenSubject moved to CmuxNextAgentPane, and the View Chat Only refusal moved to RefusalStrings.
  • Merged feat-cmux-next at 59d0076. The base this branch started from failed to compile cmux-sdk's home_conversation_live test, and that is fixed upstream.

Validation

Hosted CI only; nothing was built on a Mac.

  • acpmux, tests/hub_integration/watch.rs:
    • a watched Codex chat shows the rollout and what is appended to it;
    • a snapshot of every file under the Codex home is byte-identical before and after;
    • prompts are refused, and no initialize or session/load is recorded;
    • a watched Claude chat shows prompts and answers and settles ended turns;
    • Take Over lets the next prompt run and settles a turn the other app left open.
    • The pane's warm-up and a mode change start no agent on a viewed chat.
    • A Claude turn ends on its text line, not the thinking line before it.
    • Viewing never blocks starting a session: a new chat in the viewed chat's folder runs, and a fork is its own chat while the view goes on (viewing_a_chat_never_blocks_starting_a_new_one).
    • A unit test covers complete-line reading and offsets.
    • Focused run test_filter=watch: 39204810572, in progress.
  • CLI, cli/agent_view.rs: key, path and bare-id mapping, and sidebar view verbs are left alone. Focused run test_filter=agent_view: 39204813047, in progress.
  • Swift AgentChatViewTests: View Only sends watch: true in the adopt handshake, and an ordinary open sends none. chats.watch decodes, and the action takes only Claude Code or Codex keys. These run in this PR's cmux-next checks.
  • Webview, bun:
    • watch-banner.test.tsx: the banner names the app and offers Take Over plus Continue in New Chat (Claude Code) or New Chat Here (Codex);
    • all-chats-view-only.test.tsx: the row menu offers View Only, off for chats acpmux cannot watch, and it calls back with the row's key.
    • tsc --noEmit is clean.
  • The action exports (plans/cmux-next/action-surfaces.json) are left to the autofix bot, which regenerates them on a Mac runner.

Not verified: a live run against a real Claude Code or Codex session in a built app, and the side-by-side capture against the Claude app. That needs a dogfood build (dev-build label) on a machine with those apps signed in.

Limits

  • Claude Code and Codex only; other families are refused.
  • Viewing polls the file (400 ms) instead of using file events.
  • A watched chat shows the transcript's prompts and answers; tool calls and thinking are not mapped yet.
  • A transcript replaced by a longer file is not detected (no inode check), and an old Codex rollout with only unannotated items shows nothing while being followed.
  • The model and mode pickers stay drawn on a viewed chat. acpmux refuses those changes until Take Over, but the controls are not greyed out.
  • A Codex New Chat Here starts without the viewed conversation's context.

Changelog

  • Added: View Only for Claude Code and Codex chats in cmux NEXT (All chats menu, palette, cmux agent view): the chat follows the app that runs it without taking it over, until Take Over.

Context: manaflow-ai/cmuxterm-hq#2503. Related: #21253 (read-only observe of an acpmux session, which complements this one).

No capture: View Only needs a Claude Code or Codex transcript written outside cmux, and a capture slot has none. The banner and the All chats menu row are webview UI, covered by watch-banner.test.tsx and all-chats-view-only.test.tsx.

🤖 Generated with Claude Code

Agent session: cosmic-gray