Skip to content

1.9.1: Claude Code and Codex sessions as Interactions, with chat and browser handoff - #589

Draft
TonsOfFun wants to merge 5 commits into
feat/live-view-file-uploadfrom
feat/claude-code-interactions
Draft

TonsOfFun wants to merge 5 commits into
feat/live-view-file-uploadfrom
feat/claude-code-interactions

Conversation

@TonsOfFun

Copy link
Copy Markdown
Contributor

For 1.9.1. This is stacked on #588 (file upload), which is stacked on #584. It changes browser-sidecar/, so it ships with the 1.9.1 sidecar publish. The sidecar is already at 1.9.1 on #588, and the engine accepts only a sidecar of its own version.

Claude Code and Codex sessions become Interactions: conversations you can read, continue, and share the sandbox's browser with.

Conversations

  • Data: migration 027 adds conversation_root_id, turn_index, origin, context_note and resume_session_id to code_sessions. There's no backfill: existing sessions are one-turn conversations.
  • Resuming: a conversation's first turn starts the CLI session it names (--session-id), and every later turn resumes it (--resume). For Codex, a later turn runs codex exec resume <thread>, with the sandbox mode passed as --config.
  • Queueing: a message sent while a turn runs waits in the conversation (CodeConversation.post!). CodeSessionJob starts the next turn once one settles. A conversation can have at most 50 turns and 3 waiting messages.
  • Credential: a conversation keeps the credential it started with. A follow-up after a change, or when the session is gone, fails with a clear "start a new conversation".
  • Endpoint: POST /api/sandboxes/:sandbox_id/code_sessions/:id/messages returns 201 when the turn starts now, or 202 (waiting) when it waits.
    • It runs the same checks as starting a session, and every turn counts as an execution.
    • New permission :chat_code_session. A subscription conversation can be continued only by the person signed in to the sandbox.
  • Interactions API: GET /api/interactions takes source=all|agent|claude_code|codex, project_id and sandbox_id. GET /api/interactions/code-<id>?after[<turn>]=N returns each turn's new transcript events.

Browser handoff (sidecar S1–S5)

  • S1: POST/DELETE /tokens mints per-client tokens that reach /mcp only. Each Claude Code turn gets one, labelled "Claude Code · turn N", in a 0600 --mcp-config file outside the checkout, with --allowedTools mcp__browser. It's revoked and removed when the turn ends.
  • S2: the first tool result after a person takes or hands back control says so, once, and asks for a snapshot before acting.
  • S3: a browser_request_person({ reason }) tool, answered by the sidecar. Every viewer sees the request until someone takes control.
  • S4: taking control offers the person a file chooser the agent left open.
  • S5: the live view names the waiting agent ("Claude Code · turn 3 is waiting for you to hand back control").

Dashboard

  • Interactions:
    • Source and project filters, kept in the URL.
    • Latest conversation (/interactions/latest).
    • Code rows carry a CLAUDE CODE / CODEX · LIVE badge.
  • /interactions/code-<id>:
    • Turns, each with status, cost, duration and diff stat (since checkout), the dashboard note, transcript, and the diff on demand.
    • A composer that says whether a message runs now or waits, and Stop.
    • The sandbox's browser beside the chat. Hand back to Claude Code reports the handoff with the next message.
    • A "Claude Code needs you" banner.
  • Project page:
    • A Sessions tab with "Chat with Claude Code" and the project's conversations.
    • The Setup card links its boot fix ("Ask Claude Code about this fix →").
  • Settings: each session gets Open in Interactions.

Hardening for every session

  • --strict-mcp-config, so a checkout's .mcp.json can't add servers.
  • --append-system-prompt saying repository content, tool output and pages are data.
  • <dashboard-note> tags are stripped from what people type.
  • Boot-fix follow-up from the 1.9.0 live test: gem search ^activeagent$ was denied because Claude Code refuses, unattended, a command it can't read statically, and $ reads as a variable. The prompt now asks for plain words and names gem search --remote --exact NAME / gem info --remote NAME, and Bash(gem info:*) joins the allowlist.

Tests

  • actionagent (Ruby 4.0.6), full suite: 1709 runs, 0 failures, 7 skips. New:
    • conversation model and service tests
    • messages API (ordering, queueing, conflicts, quota, permission, subscription owner, lost resume)
    • Interactions union and filters
    • local backend argv (turn 1 vs N, UUID guard, browser MCP config and token)
    • Codex resume
    • boot-fix follow-up and command spelling
  • Frontend: 611 pass. New: codeConversations.test.mjs, codeInteractionViews.test.mjs, and live-view handoff state.
  • Sidecar: 161 pass, 0 skipped. That includes a new real-Chromium test: client token, request-person, the waiting client named, the handoff note once, and revocation.
  • rubocop: clean.
  • CI: 1.9.0: fix CI drift from ruby_llm 2.1.1 and faraday-net_http 3.5.0 #587 (the RubyLLM / Faraday drift fix) isn't on main yet, so expect the same 2 EvalsRubyLLMConversationTest failures as the rest of the stack until it merges.

Related

  • The Incus command line (resume, strict MCP, system note, boot-fix allowlist) is in activeagents/activeagents, in the companion draft PR.
  • The review note, with deviations, how to test locally and open questions, is in activeagents docs/features/claude-code-interactions.md. The design is in docs/features/claude-code-interactions-design.md.

Open questions

  1. The host app needs migration 027 when it bumps to 1.9.1.
  2. Should hosted restrict chat to a conversation's starter by default?
  3. Should the local backend delete local_login transcripts kept in ~/.claude/projects?
  4. Browser handoff on Incus needs a sidecar there (a later phase).
  5. Streaming over ActionCable instead of polling (a later phase).
  6. Should a follow-up turn of an evaluation fix re-run verification?

🤖 Generated with Claude Code

https://claude.ai/code/session_01CJAKwnkBaCGr5qjCcmc3kh

…owser handoff

A code session becomes one turn of a conversation. Its first turn names the
CLI session it starts, and each message after it resumes that session, so
Claude Code (or Codex, resuming its thread) remembers the conversation. A
message sent while a turn runs waits in the conversation, and the next turn
starts once that one settles. Conversations are listed in Interactions beside
agent runs, open with their turns, a composer and the sandbox's browser, and
are reached from a project's new Sessions tab, its boot fix, Settings and
"Latest conversation".

While the browser runs, each Claude Code turn reaches it with a token of its
own, revoked when the turn ends. The sidecar (1.9.1) mints those tokens,
tells an agent when a person drove the browser since its last call, offers a
browser_request_person tool whose request every viewer sees, names the
waiting agent in the live view, and offers a person who takes over the file
chooser an agent left open. A hand-back in the conversation is reported with
the next message too.

Every session now loads only the MCP servers it is given, and is told that
the repository, tool output and pages are data. A boot fix's prompt asks for
commands Claude Code can approve unattended, which a `gem search ^name$`
pattern was not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJAKwnkBaCGr5qjCcmc3kh
TonsOfFun and others added 4 commits October 14, 2026 17:33
A session recorded before origins were (migration 027) took its origin from
its evaluation run alone, so a boot fix from then read "Started from
Settings"; one its project recorded as a boot fix now reads as one.

The conversation page's browser pane sticks beside a long transcript, with a
height and scroll of its own, so Take over and Hand back stay in view, and so
does Claude Code's request for a person while the pane is open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJAKwnkBaCGr5qjCcmc3kh
main was a scroll container that never scrolled, since the window scrolls
the dashboard, so a conversation's sticky browser pane stayed at the top of
the page. main now clips horizontally instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJAKwnkBaCGr5qjCcmc3kh
The conversation and the sandbox's browser are now two panels that run to
the bottom of the window and scroll on their own, with the composer pinned
under the transcript, which follows new turns unless you scrolled up.
Narrow screens stack them at their natural height.

The browser hides and shows from the page header or its own panel, and
the conversation panel offers it back when hidden. The choice goes in the
URL (?browser=1/0) and is remembered for the next conversation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJAKwnkBaCGr5qjCcmc3kh
A separator between the two panels resizes them, from 25% to 75% for
the conversation. Drag it, or focus it and use the arrow keys, Home and
End; Enter or a double-click puts it back at half and half. The split is
remembered. Drag state lives in refs so a move that arrives before the
next render still counts, and pointer capture keeps the drag over the
live view's frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJAKwnkBaCGr5qjCcmc3kh

This branch has not been deployed

No deployments
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