Skip to content

Agent conversation protocol P1: Go normalizer in cmuxd-remote + TanStack chat surface - #5736

Closed
lawrencecchen wants to merge 29 commits into
mainfrom
feat-agent-normalized-layer
Closed

lawrencecchen wants to merge 29 commits into
mainfrom
feat-agent-normalized-layer

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

A normalized layer over coding-agent session output, plus a structured GUI that can open on any session, including ones already running in a terminal pane that cmux did not spawn. Design doc: docs/agent-conversation-protocol.md.

The interface is adapted from t3code's canonical runtime events (items with a started/updated/completed lifecycle, tool results folded into their tool item, reserved names for streaming/permission events). The implementation is deliberately different: instead of spawning agents through SDKs (which only sees sessions the app created), the producer reads the transcripts the agents already write to disk, so any session is observable.

Pieces

Contract. webviews/src/agent-chat/protocol.ts (source of truth) mirrored by daemon/remote/agentconv/protocol.go. Golden fixtures under daemon/remote/agentconv/testdata/ are the wire-form contract both sides test against.

Producer (Go, in cmuxd-remote). daemon/remote/agentconv parses Claude Code project transcripts (~/.claude/projects/<enc-cwd>/<uuid>.jsonl) and Codex rollouts (~/.codex/sessions/.../rollout-*.jsonl) into canonical items: messages, reasoning, command_execution / file_change / mcp_tool_call / web_search tools with results paired by id, context compactions. A subscription replays a snapshot then tails the file (bounded poll, truncation re-snapshots). New RPC verbs agent.sessions.list / agent.session.open / agent.session.close next to the existing pty.* verbs, capability agent.conversation, working over plain serve --stdio. One implementation runs everywhere transcripts live: headless Linux cmux servers and macOS.

GUI (webviews TanStack). New /agent-chat surface: pure event reducer with seq guards, chat timeline (markdown messages, collapsed reasoning, expandable tool rows with status, turn separators, derived auto-follow with jump-to-latest, no useEffect-based scroll logic). Without a WebKit host it falls back to a fixture-replaying mock bridge, so the surface runs in plain vite and bun tests.

Host wiring (Swift). Window → View Chat resolves the focused panel to its transcript via RestorableAgentSessionIndex (honoring CLAUDE_CONFIG_DIR, ~/.codex-accounts/claude/*, CODEX_HOME, same roots as the resume path), spawns the cached cmuxd-remote binary as a local stdio child, and relays canonical events into the webview over the agentChat bridge. Binary resolution: the CMUX_REMOTE_DAEMON_BINARY dev override (gated by CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1), then the existing checksum-verified remote-daemons cache (exact app version, then newest cached). No network: cache miss reports the daemon unavailable in the UI.

Verification

  • daemon/remote: go test ./... green (golden parsers, folding, tail append/truncation, discovery, plus an end-to-end stdio RPC test: open → snapshot frame → file append → item frame). Parsers sanity-checked against real ~/.claude and ~/.codex transcripts (hundreds of items, zero in_progress leaks on finished sessions).
  • webviews: typecheck, lint:ci, verify:tanstack-router, bun test (reducer suite), build-webviews-app.sh --check, react-doctor (no findings in new files) all green.
  • Swift: build-only xcodebuild ... -derivedDataPath /tmp/cmux-<tag> build.

Localization audit: new Swift-facing strings (menu.window.viewChat, agentChat.windowTitle, agentChat.noSession.*, agentChat.error.noTranscript) have en + ja entries in Resources/Localizable.xcstrings. The webview surface's strings are English-only, matching the existing agent-session surface.

Known limits (by design, P1)

  • Read-only: the composer (write path) is P2. The design doc pins the rule: live-TUI sessions get PTY injection into the existing pane, only detached sessions get a daemon-spawned claude --resume.
  • No content.delta token streaming (transcripts append whole items; names reserved).
  • The chat window does not yet receive terminal theme tokens (CSS falls back to dark/light system scheme).
  • Locator trusts executables already in the remote-daemons cache (the cache is normally filled by the checksum-verified cmux ssh bootstrap).

Supersedes #5576 (Swift-native parsers + SwiftUI chat view): same goal, but the parser now lives in the Go daemon so headless Linux hosts and future iOS consumers share one implementation, and the GUI lives in the TanStack webviews app.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Note

Low Risk
Read-only UI and string catalog changes; main caveat is rendering agent markdown via HTML (local transcript content only).

Overview
Adds shipped webview assets and macOS-facing copy for the Agent Chat window opened from Window → View Chat.

Localization (Localizable.xcstrings): New en/ja strings for the menu item (menu.window.viewChat), window title, “no agent conversation” guidance (focus a Claude Code/Codex terminal), OK, and missing-transcript errors.

Agent Chat web surface: New agent-chat.html entry (data-cmux-webview-kind="agent-chat") that boots the bundled TanStack/React chunk. The surface talks to the host via the agentChat WebKit bridge (chat.init / chat.subscribe), applies a reducer over canonical snapshot/stream events (seq-guarded item updates, daemon status), and renders a read-only timeline—markdown assistant replies, user bubbles, collapsible reasoning/tools, turn separators, live/unavailable daemon UI, and scroll auto-follow with Jump to latest.

Reviewed by Cursor Bugbot for commit a971c96. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Adds a normalized agent conversation layer and a read-only Agent Chat pane split next to the focused panel, powered by a Go normalizer in cmuxd-remote and a TanStack UI. Streams transcript tail + live hook events; live retention is bounded with auto-resnapshot, resolution falls back to panel resume data, a capability-gated daemon with numeric version sorting picks a compatible cached binary, and empty snapshots are handled with click-to-focus.

  • Bug Fixes

    • WebView reliability: recovers after WebContent termination and guarantees module boot under file:// so the surface mounts.
    • Stability: bounds transcript tail reads and removes a WK handler retain cycle.
    • UX: drops Claude “No response requested.” placeholders, renders error/stopped/working states, and shows a clear subscribe-failure state.
    • Tests: guard imports so test targets compile in both Debug (cmux_DEV) and Release (cmux) builds.
  • Migration

    • Ensure a cached cmuxd-remote exists or set CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1 with CMUX_REMOTE_DAEMON_BINARY; the app gates on the agent.conversation capability.
    • Open via Window > View Chat or the Command Palette “Open Agent Chat.” P1 is read-only (no composer, no token-level streaming).

Written for commit 5ef315d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • "View Chat…" menu opens a standalone Agent Chat window with provider sessions, timeline of messages, reasoning, tool calls, and outputs
    • Real-time streaming with transcript replay, session discovery across providers, auto-follow and a “Jump to latest” control
    • UI states for connecting, daemon availability, empty/no-session messaging, and localized chat strings
  • Documentation

    • Added agent conversation protocol documentation

lawrencecchen and others added 4 commits June 9, 2026 15:41
…Go mirror, design doc)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ne UI, mock bridge

Read-only chat timeline over the canonical agent conversation protocol.
Native transport is the agentChat WebKit handler; without a host the
surface falls back to a fixture-replaying mock bridge so it runs in
plain vite/bun. Auto-follow is derived state (no effects): a bottom
anchor keyed by lastSeq re-pins the viewport while following.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt.* RPC verbs

agentconv parses Claude Code project transcripts and Codex rollouts into
the canonical conversation protocol: items with a lifecycle, tool results
folded by tool_use_id, snapshot replay + poll tail with truncation
re-snapshot, session discovery with cwd filtering. Golden fixtures under
agentconv/testdata are the cross-language contract shared with the TS
types in webviews. Verified against real ~/.claude and ~/.codex
transcripts on a dev machine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hat webview window

Window > View Chat resolves the focused panel to its transcript via
RestorableAgentSessionIndex (CLAUDE_CONFIG_DIR, ~/.codex-accounts/claude/*,
and CODEX_HOME honored, same roots as the resume path), spawns the cached
cmuxd-remote binary as a local stdio child, and relays canonical
conversation events into the webviews /agent-chat surface over the
agentChat bridge. Binary resolution uses the existing remote-daemons
cache plus the gated CMUX_REMOTE_DAEMON_BINARY dev override; a cache
miss reports the daemon unavailable instead of downloading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 12, 2026 10:25pm
cmux-staging Building Building Preview, Comment Jun 12, 2026 10:25pm

@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an end-to-end agent chat feature: canonical event protocol, Claude/Codex transcript parsers and tailing subscriptions, cmuxd RPC for sessions, Swift host integration (presenter/window/webview bridge/daemon client), and a React webview surface for live conversation timelines.

Changes

Agent Chat Feature

Layer / File(s) Summary
All changes (single checkpoint)
daemon/remote/agentconv/*, daemon/remote/cmd/*, Sources/AgentChat/*, Resources/markdown-viewer/webviews-app/*, webviews/src/agent-chat/*, webviews/src/*, cmux.xcodeproj/*, Resources/Localizable.xcstrings, scripts/build-webviews-app.sh, daemon/remote/agentconv/testdata/*, daemon/remote/agentconv/*_test.go, daemon/remote/cmd/*_test.go
Complete implementation of agent conversation protocol and feature: protocol contracts and docs, Claude/Codex JSONL parsers, conversation state and subscription tailing, cmuxd RPC handlers and event pumping, Swift transcript resolution, daemon binary discovery and stdio client, macOS window/presenter/webview controller bridging chat surface, generated webview HTML and routing, TypeScript bridge/mock, reducer and React timeline UI with styles, tests and fixtures.
  • Sequence Diagram(s): (none generated; change is multi-component but already well described in text)
  • Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Possibly related PRs:
    • manaflow-ai/cmux#5613: Both PRs touch dynamic webview mounting and route dispatch; #5613 refactored dynamic imports used here.

"A rabbit scurries to the wire,
transcripts hum like morning choir,
Claude and Codex hand in paw,
chat arrives with tidy law. 🐇"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-agent-normalized-layer

Comment thread Sources/AgentChat/AgentChatWebViewController.swift Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e240161160

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +177 to +185
func EncodeClaudeProjectDir(cwd string) string {
var builder strings.Builder
builder.Grow(len(cwd))
for _, r := range cwd {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
builder.WriteRune(r)
} else {
builder.WriteByte('-')
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match Claude project dirs using the same encoding

For Claude sessions whose cwd contains characters other than / or . (for example spaces or punctuation), this Go resolver now looks under a different project directory than the Swift launch/resume path (RestorableAgentSessionIndex.encodeClaudeProjectDir only replaces / and ., and tests document that rule). The app passes session_id+cwd to the daemon for list/open flows, so those sessions are missed even though their transcript exists; make EncodeClaudeProjectDir mirror the established Swift rule.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4225328 (failing test) + a3309b8: project-dir matching now uses Claude Code's real encoding rule (every non-alphanumeric becomes '-'), with an encoded-cwd regression fixture.

@greptile-apps

greptile-apps Bot commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a normalized agent-conversation layer (agentconv) in the Go daemon that parses Claude Code and Codex on-disk transcripts into a canonical event stream, and wires a read-only /agent-chat TanStack webview surface reachable via Window → View Chat. The macOS host resolves the focused panel's transcript, spawns cmuxd-remote serve --stdio as a local stdio child, and relays canonical events into the webview over the agentChat WebKit bridge.

  • Go normalizer (daemon/remote/agentconv): snapshot + bounded-poll tail, truncation re-snapshot, tool-result folding by tool_use_id, golden-fixture contract tests, and new agent.sessions.list / agent.session.open / agent.session.close RPC verbs.
  • React surface (webviews/src/agent-chat): pure reducer with seq-regression guard, ref-callback auto-follow (no useEffect scroll logic), jump-to-latest pill, mock bridge for standalone dev.
  • Swift host wiring (Sources/AgentChat): AgentChatPresenter runs transcript resolution off-main via Task.detached; AgentDaemonClient is an NSLock-guarded NDJSON RPC client; new localized strings added in both en and ja.

Confidence Score: 4/5

Safe to merge; the one issue that needs a fix before shipping is a cosmetic but policy-violating user-visible exit-code leak in the termination handler.

The Go normalizer, reducer, and Swift host wiring are all well-structured. The only current defect on the changed path is in AgentChatWebViewController.onTermination: it passes a raw process exit code directly into the DaemonBanner UI, exposing an implementation detail to users. This is the same class of problem caught by the previous thread on AgentDaemonBinaryLocator, and this new site was not addressed in that fix. The scanTranscriptHead scaling concern in discover.go is limited to the agent.sessions.list verb that the P1 UI does not yet call, so it does not affect shipped behavior today.

Sources/AgentChat/AgentChatWebViewController.swift — the onTermination closure at line 174 needs the raw exit code replaced with a user-friendly message before the feature ships.

Important Files Changed

Filename Overview
Sources/AgentChat/AgentChatWebViewController.swift Hosts the /agent-chat webview, bridges JS↔Swift, spawns and tears down the daemon client. The onTermination closure exposes raw process exit codes in the user-visible DaemonBanner detail string.
Sources/AgentChat/AgentDaemonBinaryLocator.swift Locates the cached cmuxd-remote binary via env-var override or the remote-daemons cache; previously flagged for exposing env var names and file paths in user-visible error strings.
Sources/AgentChat/AgentDaemonClient.swift NDJSON RPC client over a spawned stdio child; uses NSLock to guard mutable state, CheckedContinuations for request/reply, and readabilityHandler for async reads. Logic is sound.
Sources/AgentChat/AgentChatPresenter.swift Correctly resolves the focused panel off-main with Task.detached before presenting the window; all strings use String(localized:defaultValue:) with catalog entries.
Sources/AgentChat/AgentChatTranscriptResolver.swift Resolves Claude/Codex transcript paths from the restorable-session index; mirrors the resume-path config-root ordering precisely. Synchronous FS calls are always made off-main via Task.detached in the presenter.
daemon/remote/agentconv/subscription.go Snapshot + bounded-poll tail implementation; handles truncation by re-snapshotting, ErrNotExist gracefully, stop/done channel contract, and seq numbering correctly.
daemon/remote/agentconv/discover.go Session discovery via filesystem glob/scan; scanTranscriptHead is called for every transcript before the limit is applied, which could be expensive for users with many sessions when agent.sessions.list is used.
daemon/remote/cmd/cmuxd-remote/agent_conversation.go RPC verb handlers for agent.sessions.list / agent.session.open / agent.session.close; subscription lifecycle and pumpAgentEvents goroutine teardown look correct.
webviews/src/agent-chat/conversationStore.ts Pure reducer over snapshot/item.*/session.meta/error events with seq-regression guard; correctly resets lastSeq on snapshots and upserts items by id.
webviews/src/agent-chat/react/AgentChatApp.tsx React surface; ref-callback-based auto-follow avoids useEffect scroll logic; jump-to-latest pill derived cleanly from unfollowedAtSeq state.

Sequence Diagram

sequenceDiagram
    participant User
    participant Swift as macOS Host (Swift)
    participant WKWebView
    participant Daemon as cmuxd-remote (Go)
    participant FS as Filesystem (~/.claude / ~/.codex)

    User->>Swift: Window → View Chat
    Swift->>Swift: AgentChatPresenter.presentForFocusedPanel()
    Swift->>Swift: Task.detached: RestorableAgentSessionIndex.load() + resolve()
    Swift-->>Swift: Resolution(provider, sessionId, transcriptURL)
    Swift->>WKWebView: loadFileURL(agent-chat.html)
    WKWebView->>Swift: chat.init (JS→Swift)
    Swift->>Swift: AgentDaemonBinaryLocator.locate()
    Swift-->>WKWebView: "{daemon_status, session}"
    WKWebView->>Swift: chat.subscribe (JS→Swift)
    Swift->>Daemon: spawn cmuxd-remote serve --stdio
    Swift->>Daemon: hello RPC
    Daemon-->>Swift: "{name, capabilities}"
    Swift->>Daemon: "agent.session.open {provider, transcript_path}"
    Daemon->>FS: Open transcript JSONL
    Daemon->>Daemon: Parse snapshot
    Daemon-->>Swift: "{subscription_id, session}"
    Swift-->>WKWebView: ok (subscribe reply)
    loop Tail poll every 300ms
        Daemon->>FS: readNewLines()
        FS-->>Daemon: new lines
        Daemon->>Daemon: parse items / emit events
        Daemon-->>Swift: "agent.session.event {type: item.started/updated/completed}"
        Swift->>WKWebView: cmuxAgentChatBridge.receive(agent.event)
        WKWebView->>WKWebView: reduceConversation(state, action)
    end
    User->>Swift: Close window
    Swift->>Daemon: terminate()
    Daemon->>Daemon: close subscriptions
Loading

Reviews (3): Last reviewed commit: "Address review feedback: discovery cwd r..." | Re-trigger Greptile

Comment on lines +112 to +125
private func initResultPayload() -> [String: Any] {
var payload: [String: Any] = [:]
switch AgentDaemonBinaryLocator().locate() {
case .found:
payload["daemon_status"] = "ready"
case .unavailable(let detail):
payload["daemon_status"] = "unavailable"
payload["daemon_detail"] = detail
}
if let session = sessionRefPayload() {
payload["session"] = session
}
return payload
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Synchronous filesystem ops on the main actor

AgentDaemonBinaryLocator().locate() is called synchronously on the main actor in both initResultPayload() (here) and at the start of subscribe() (line 151), before any await. locate() calls fileManager.contentsOfDirectory(atPath:) (directory scan in newestCachedBinary) and fileManager.fileExists / isExecutableFile (multiple stat syscalls). If the remote-daemons cache is on a slow or NFS-mounted volume these block the main run loop, stalling all UI until they return.

Both sites should hop off the main actor before calling locate(). For initResultPayload() the whole chat.init handler could be made async and wrapped in a Task; for subscribe() a Task.detached or await withCheckedThrowingContinuation boundary before the locate() call would suffice — exactly the pattern already used in AgentChatPresenter.presentForFocusedPanel for RestorableAgentSessionIndex.load().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c14e701: the daemon binary locate runs off the main actor; the controller awaits it.

Comment on lines +38 to +53
}
return .unavailable(detail: "CMUX_REMOTE_DAEMON_BINARY points to a missing or non-executable file: \(override.path)")
}
let (goOS, goArch) = hostPlatform()
let version = appVersionString()
if let exact = try? Workspace.remoteDaemonCachedBinaryURL(
version: version, goOS: goOS, goArch: goArch, fileManager: fileManager
), isExecutableFile(exact) {
return .found(exact)
}
if let fallback = newestCachedBinary(goOS: goOS, goArch: goArch, excludingVersion: version) {
return .found(fallback)
}
return .unavailable(
detail: "No cached cmuxd-remote binary for \(goOS)-\(goArch); connect a remote host once (cmux ssh) or set CMUX_REMOTE_DAEMON_BINARY with CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1."
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Environment variable names in user-facing error copy

Both unavailable outcomes contain implementation-internal details that land directly in the webview UI. The first string (line 39) includes the raw file path of the override binary and the env var name CMUX_REMOTE_DAEMON_BINARY. The second string (line 52) includes CMUX_REMOTE_DAEMON_BINARY, CMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1, and references to the internal "remote-daemons cache". These strings flow through initResultPayload → daemon_detail, and through the subscribe() error path → userMessage, and are rendered verbatim in DaemonBanner and EmptyState in the webview.

The detail for the env-var override path can be omitted entirely (it's a dev path the user intentionally configured). The cache-miss message should be rephrased in cmux terms — something like "The agent conversation feature requires a remote daemon that hasn't been cached yet. Connect to a remote host with cmux ssh to enable it." — reserving the env-var guidance for a debug log entry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c14e701: error copy is localized human text (en+ja); env var names no longer appear in user-facing strings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@daemon/remote/agentconv/codex.go`:
- Around line 323-344: The code currently only detects failure via
wrapper.Metadata after parsing text as JSON string; if the original raw JSON was
an object with metadata.exit_code it gets ignored. When you unmarshal raw into
object (the map[string]any block), also inspect object["metadata"] (a map) for
an "exit_code" numeric value (handle float64 or json.Number), set a failed bool
if exit_code != 0, and when you extract object["output"] or object["content"]
assign text and remember that failed flag; finally return &ToolOutput{Text:
text, IsError: failed} (and ensure the later wrapper parsing still
overrides/fine-tunes failed when applicable).

In `@daemon/remote/agentconv/discover.go`:
- Around line 204-223: ResolveTranscriptPath's ProviderCodex branch ignores the
cwd narrowing: when cwd is provided, first try to locate a candidate scoped to
that cwd (e.g., build a glob or explicit path under roots.CodexDir "sessions"
that includes the encoded project dir for cwd or the expected cwd-containing
segment) and return it if found; if not found, fall back to the existing broad
glob. Modify the ProviderCodex handling in ResolveTranscriptPath to (1) if cwd
!= "" construct a narrowed glob/path that includes the encoded cwd directory
segment (or filter matches for paths containing the encoded cwd) and check
os.Stat or matches for existence, returning that candidate when present, and (2)
otherwise or if no narrowed match, keep the current broad glob lookup
(matches[len(matches)-1]) as the fallback.

In `@daemon/remote/agentconv/parser_test.go`:
- Around line 23-29: The scanner loop in the test uses bufio.NewScanner and
parser.consumeLine but does not check scanner.Err(), so scanning failures are
ignored; after the for scanner.Scan() loop (before returning parser.conv())
check scanner.Err() and fail the test or return an error when non-nil (e.g.,
call t.Fatalf or propagate the error) so fixture read/scan errors surface;
locate the scanner setup (bufio.NewScanner,
scanner.Buffer(maxTranscriptLineBytes)) and add the post-loop error check
referencing scanner.Err() and the test harness (t) or the surrounding function
to report the failure.

In `@daemon/remote/agentconv/subscription.go`:
- Around line 138-141: When truncation causes you to re-parse and you call
reader.readNewLines(), don’t silently continue on error: catch the returned err
from reader.readNewLines() and emit the same recoverable error event you use in
the earlier error path (the recoverable error event emitted at the previous
handling block), including the error and context about the truncation/snapshot
attempt, then continue; in short, replace the silent continue after
reader.readNewLines() with a call to the subscriber’s recoverable-error emission
(the same event type/handler used at the earlier block) so the client receives a
snapshot/failure notification.

In `@daemon/remote/cmd/cmuxd-remote/agent_conversation.go`:
- Around line 85-95: In handleAgentSessionClose, detect a missing or empty
subscription_id right after extracting subscriptionID via
stringParam(req.Params, "subscription_id") and before looking up s.agentSubs; if
subscriptionID == "" return rpcResponse with OK:false and
Error.Code="invalid_request" and a clear message (e.g., "missing
subscription_id") instead of falling through to the not_found path so malformed
input is distinguished from unknown subscriptions.

In `@Resources/Localizable.xcstrings`:
- Around line 188910-188925: The localization entry for
"agentChat.error.noTranscript" is missing the "extractionState": "manual" field
present on other new keys; update each localization block (e.g., the "en" and
"ja" stringUnit objects) under "agentChat.error.noTranscript" to include
"extractionState": "manual" so the structure matches other added entries (ensure
you add it alongside "state" and "value" inside the relevant stringUnit
objects).

In `@Sources/AgentChat/AgentChatWindowController.swift`:
- Line 24: The auxiliary-window identifier set cmuxAuxiliaryWindowIdentifiers is
missing the new window identifier "cmux.agentChat" (set via window.identifier =
NSUserInterfaceItemIdentifier("cmux.agentChat") in AgentChatWindowController);
update the declaration of the private let cmuxAuxiliaryWindowIdentifiers to
include the string "cmux.agentChat" in the set so the auxiliary-window
close-shortcut lint passes.

In `@Sources/AgentChat/AgentDaemonClient.swift`:
- Around line 44-49: The startup flips the started flag before the daemon
successfully launches, allowing allocateRequest/queueing to proceed when the
process may have failed; in start() move setting started = true to after
process.run() returns successfully (or set started = false inside the catch to
roll back), and ensure any request allocation paths (the methods referenced
around lines 66-67, 71-76, 83-106 — e.g., allocateRequest or whatever enqueues
continuations) check that the process is actually running (or that started is
only true when process.isRunning) before queuing/waiting; update error handling
to release/rollback the started state on failure and gate request allocation on
a confirmed running process.

In `@webviews/src/agent-chat/bridge.ts`:
- Around line 115-117: The fallback error message for the AgentChatBridgeError
is hard-coded ("Native bridge request failed."); update the construction of
AgentChatBridgeError (where reply.error?.userMessage is used) to pull a
localized string from the webview i18n source (e.g., next-intl/messages or the
existing webview locale module) instead of the hard-coded English fallback, and
ensure you add the new key and translations across all locale files so every
locale has coverage for the fallback message.
- Around line 127-139: The current resolveAgentChatBridge silently falls back to
createMockAgentChatBridge() when nativeAgentChatHandler() is missing; change it
to fail closed in non-dev runtimes by only using the mock in dev/test and
otherwise surface an explicit error (or return a bridge whose methods throw) so
no synthetic data appears in production. Update resolveAgentChatBridge to check
the runtime (eg. NODE_ENV or an existing isDev/isTest helper) after calling
nativeAgentChatHandler(), and if handler is null: if dev/test return
createMockAgentChatBridge(), else throw a clear Error ("Agent chat bridge
unavailable: native handler missing") or return a bridge implementation whose
init/subscribe/dispose throw that same error; keep
ensureAgentChatBridgeInstalled() and nativeAgentChatHandler() usage unchanged
and adjust the single return site currently returning
createMockAgentChatBridge().

In `@webviews/src/agent-chat/conversationStore.ts`:
- Line 109: The current update path uses upsertItem(state.items, event.item)
which calls findIndex on every streamed item event, causing O(n²) behavior for
long transcripts; replace this with an id->index lookup cache stored on the
state (e.g., state.indexById) that is rebuilt from state.items when processing a
snapshot and kept in sync on inserts/updates/removes so streamed events can
update items by direct index rather than scanning. Update code paths that call
upsertItem (including the handler that returns { ...state, lastSeq: event.seq,
items: upsertItem(...) } and the related 121-128 logic) to use the lookup for
O(1) updates, and ensure the cache is updated whenever items are mutated or a
snapshot is applied.

In `@webviews/src/agent-chat/react/AgentChatApp.tsx`:
- Around line 56-57: Replace all hardcoded user-visible strings in AgentChatApp
(e.g., the error detail "Agent bridge request failed."), and the similar
literals in display.ts and rows.tsx, with calls to the repository's web locale
API (use the existing web locale source/getString/i18n helper used elsewhere)
and wire message IDs instead of inline English. Update or add entries for each
new message ID to every supported message catalog so the error text, status
text, empty states and CTAs are localized across locales; ensure the component
uses the localized string (and passes through error.message when available) and
import the same locale helper consistently in AgentChatApp, display.ts and
rows.tsx.

In `@webviews/src/agent-chat/styles.css`:
- Around line 334-341: The .agent-chat-visually-hidden rule uses the deprecated
clip property; remove clip and replace it with a modern equivalent using
clip-path (e.g., clip-path: inset(50%)) so the element remains visually hidden
but accessible; update the .agent-chat-visually-hidden CSS by deleting the clip
declaration and adding clip-path: inset(50%) (keeping position, width, height,
overflow, and white-space as-is) to satisfy stylelint and preserve the utility's
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 798cd55f-d064-4ed8-98d1-7abd3acc43cc

📥 Commits

Reviewing files that changed from the base of the PR and between daeca99 and e240161.

📒 Files selected for processing (46)
  • Resources/Localizable.xcstrings
  • Resources/markdown-viewer/webviews-app/agent-chat.html
  • Resources/markdown-viewer/webviews-app/chunks/agentChatSurface.mjs
  • Resources/markdown-viewer/webviews-app/chunks/agentSessionSurface.mjs
  • Resources/markdown-viewer/webviews-app/chunks/diffSurface.mjs
  • Resources/markdown-viewer/webviews-app/chunks/installWebviewStyles.mjs
  • Resources/markdown-viewer/webviews-app/chunks/markdown.mjs
  • Resources/markdown-viewer/webviews-app/main.mjs
  • Sources/AgentChat/AgentChatPresenter.swift
  • Sources/AgentChat/AgentChatTranscriptResolver.swift
  • Sources/AgentChat/AgentChatWebViewController.swift
  • Sources/AgentChat/AgentChatWindowController.swift
  • Sources/AgentChat/AgentDaemonBinaryLocator.swift
  • Sources/AgentChat/AgentDaemonClient.swift
  • Sources/cmuxApp.swift
  • cmux.xcodeproj/project.pbxproj
  • daemon/remote/README.md
  • daemon/remote/agentconv/claude.go
  • daemon/remote/agentconv/codex.go
  • daemon/remote/agentconv/conversation.go
  • daemon/remote/agentconv/discover.go
  • daemon/remote/agentconv/parser_test.go
  • daemon/remote/agentconv/protocol.go
  • daemon/remote/agentconv/subscription.go
  • daemon/remote/agentconv/subscription_test.go
  • daemon/remote/agentconv/testdata/claude/basic.jsonl
  • daemon/remote/agentconv/testdata/claude/expected.json
  • daemon/remote/agentconv/testdata/codex/basic.jsonl
  • daemon/remote/agentconv/testdata/codex/expected.json
  • daemon/remote/cmd/cmuxd-remote/agent_conversation.go
  • daemon/remote/cmd/cmuxd-remote/agent_conversation_test.go
  • daemon/remote/cmd/cmuxd-remote/main.go
  • docs/agent-conversation-protocol.md
  • scripts/build-webviews-app.sh
  • webviews/src/agent-chat/bridge.ts
  • webviews/src/agent-chat/conversationStore.test.ts
  • webviews/src/agent-chat/conversationStore.ts
  • webviews/src/agent-chat/mockBridge.ts
  • webviews/src/agent-chat/protocol.ts
  • webviews/src/agent-chat/react/AgentChatApp.tsx
  • webviews/src/agent-chat/react/display.ts
  • webviews/src/agent-chat/react/rows.tsx
  • webviews/src/agent-chat/styles.css
  • webviews/src/main.tsx
  • webviews/src/router.tsx
  • webviews/src/surfaces/agentChatSurface.tsx

Comment thread daemon/remote/agentconv/codex.go
Comment thread daemon/remote/agentconv/discover.go
Comment thread daemon/remote/agentconv/parser_test.go
Comment thread daemon/remote/agentconv/subscription.go
Comment thread daemon/remote/cmd/cmuxd-remote/agent_conversation.go
Comment thread webviews/src/agent-chat/bridge.ts
Comment thread webviews/src/agent-chat/bridge.ts
Comment thread webviews/src/agent-chat/conversationStore.ts Outdated
Comment thread webviews/src/agent-chat/react/AgentChatApp.tsx Outdated
Comment thread webviews/src/agent-chat/styles.css
…es the chat window)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Resources/markdown-viewer/webviews-app/chunks/agentChatSurface.mjs Outdated
…`},created_at:"2026-06-09T16:59:35Z"},{id:"tool-3",type:"file_change",status:"completed",title:"webviews/src/router.tsx",tool_name:"Edit",tool_use_id:"toolu_mock_03",input:{file_path:"webviews/src/router.tsx",old_string:'path: "/agent-session"',new_string:'path: "/agent-chat"'},output:{text:"Edited webviews/src/router.tsx"},created_at:"2026-06-09T16:59:42Z"},{id:"tool-4",type:"web_search",status:"completed",title:"tanstack router hash history nested routes",tool_name:"WebSearch",tool_use_id:"toolu_mock_04",input:{query:"tanstack router hash history nested routes"},output:{text:"TanStack Router docs: createHashHistory() keeps route state in the URL fragment…"},created_at:"2026-06-09T17:00:01Z"},{id:"tool-5",type:"mcp_tool_call",status:"failed",title:"browser_screenshot",tool_name:"mcp__browser__screenshot",tool_use_id:"toolu_mock_05",input:{url:"http://localhost:5173/#/agent-chat"},output:{text:"Error: no browser session connected",is_error:!0},created_at:"2026-06-09T17:00:18Z"},{id:"compaction-1",type:"context_compaction",status:"completed",text:"Context compacted (142k → 38k tokens)",created_at:"2026-06-09T17:00:40Z"},{id:"error-1",type:"error",status:"completed",text:"Provider stream interrupted; retrying transcript tail.",created_at:"2026-06-09T17:00:41Z"},{id:"unknown-1",type:"unknown",status:"completed",text:"queue-operation",created_at:"2026-06-09T17:00:42Z"},{id:"msg-4",type:"user_message",status:"completed",text:"Now run the verify gates and show me the summary.",created_at:"2026-06-09T17:02:10Z"},{id:"tool-6",type:"command_execution",status:"in_progress",title:"bun run typecheck && bun run lint:ci",tool_name:"Bash",tool_use_id:"toolu_mock_06",input:{command:"bun run typecheck && bun run lint:ci"},created_at:"2026-06-09T17:02:12Z"}]}function O(a){return[{type:"item.updated",seq:a,item:{id:"tool-6",type:"command_execution",status:"in_progress",title:"bun run typecheck && bun run lint:ci",tool_name:"Bash",tool_use_id:"toolu_mock_06",input:{command:"bun run typecheck && bun run lint:ci"},output:{text:"$ tsc --noEmit"},created_at:"2026-06-09T17:02:12Z"}},{type:"item.completed",seq:a+1,item:{id:"tool-6",type:"command_execution",status:"completed",title:"bun run typecheck && bun run lint:ci",tool_name:"Bash",tool_use_id:"toolu_mock_06",input:{command:"bun run typecheck && bun run lint:ci"},output:{text:`$ tsc --noEmit
$ oxlint . --react-plugin --jsx-a11y-plugin --import-plugin --deny-warnings
Found 0 warnings and 0 errors.
Finished in 64ms on 41 files with 102 rules using 10 threads.`},created_at:"2026-06-09T17:02:12Z"}},{type:"item.started",seq:a+2,item:{id:"reasoning-2",type:"reasoning",status:"in_progress",text:"Both gates are green. Summarize the run and hand back to the user.",created_at:"2026-06-09T17:03:02Z"}},{type:"item.completed",seq:a+3,item:{id:"reasoning-2",type:"reasoning",status:"completed",text:"Both gates are green. Summarize the run and hand back to the user.",created_at:"2026-06-09T17:03:02Z"}},{type:"item.completed",seq:a+4,item:{id:"msg-5",type:"assistant_message",status:"completed",text:"Verify gates are green:\n\n- `bun run typecheck` — no errors\n- `bun run lint:ci` — 0 warnings\n\nThe `/agent-chat` surface is registered and renders this mock stream in dev.",created_at:"2026-06-09T17:03:20Z"}}]}function q(a){typeof window>"u"||window.cmuxAgentChatBridge?.receive(a)}function K(){let a=null,e=!1;const t=()=>{a!==null&&(clearInterval(a),a=null)};return{kind:"mock",init(){return Promise.resolve({session:R,daemon_status:"ready"})},subscribe(){if(e||a!==null)return Promise.resolve();q({type:"agent.event",event:{type:"snapshot",seq:1,session:R,items:U()}});const n=O(2);return a=setInterval(()=>{const s=n.shift();if(!s){t();return}q({type:"agent.event",event:s})},W),Promise.resolve()},dispose(){e=!0,t()}}}class G extends Error{code;constructor(e,t){super(e),this.name="AgentChatBridgeError",this.code=t}}const A=new Set;function I(){typeof window>"u"||window.cmuxAgentChatBridge||(window.cmuxAgentChatBridge={applyTheme(a){H(a)},receive(a){for(const e of A)e(a)}})}I();function J(a){return A.add(a),()=>{A.delete(a)}}function V(){if(typeof window>"u")return null;const a=window.webkit?.messageHandlers?.agentChat;return a&&typeof a.postMessage=="function"?a:null}async function M(a,e,t={}){const n=await a.postMessage({id:L(),method:e,params:t});if(!n.ok)throw new G(n.error?.userMessage||"Native bridge request failed.",n.error?.code);return n.value}function X(){I();const a=V();return a?{kind:"native",init:()=>M(a,"chat.init"),subscribe:()=>M(a,"chat.subscribe"),dispose(){}}:K()}function Q(){return{phase:"connecting",session:null,items:[],daemonStatus:"ready",daemonDetail:null,lastSeq:0,hasSnapshot:!1,streamError:null}}function Y(a,e){switch(e.type){case"init":return{...a,phase:"ready",session:e.result.session??a.session,daemonStatus:e.result.daemon_status,daemonDetail:e.result.daemon_detail??null};case"init-failed":return{...a,phase:"failed",daemonStatus:"unavailable",daemonDetail:e.detail};case"inbound":return e.message.type==="daemon.status"?{...a,daemonStatus:e.message.status,daemonDetail:e.message.detail??null}:ee(a,e.message.event)}}function ee(a,e){if(e.type==="snapshot")return{...a,hasSnapshot:!0,lastSeq:e.seq,session:e.session,items:[...e.items],streamError:null};if(e.seq<=a.lastSeq)return a;switch(e.type){case"item.started":case"item.updated":case"item.completed":return{...a,lastSeq:e.seq,items:te(a.items,e.item)};case"session.meta":return{...a,lastSeq:e.seq,session:e.session};case"error":return{...a,lastSeq:e.seq,streamError:{message:e.message,recoverable:e.recoverable}}}}function te(a,e){const t=a.findIndex(s=>s.id===e.id);if(t<0)return[...a,e];const n=a.slice();return n[t]=e,n}function ae(a){return a?a==="claude"?"Claude":a==="codex"?"Codex":a.charAt(0).toUpperCase()+a.slice(1):"Agent"}function ne(a){return a?a.title&&a.title.trim()!==""?a.title:a.transcript_path.split("/").pop()||a.session_id:"Agent session"}const Z={command_execution:"Run",file_change:"Edit",mcp_tool_call:"MCP",dynamic_tool_call:"Tool",web_search:"Search"};function se(a){return a in Z}function z(a){return Z[a]??"Tool"}function oe(a){return a.title||a.tool_name||z(a.type)}const re={command_execution:"$",file_change:"±",mcp_tool_call:"⚙",dynamic_tool_call:"⚙",web_search:"⌕"};function ie(a){return re[a]??"⚙"}function le(a){if(a==null)return"";if(typeof a=="string")return a;try{return JSON.stringify(a,null,2)??String(a)}catch{return String(a)}}function ce(a){switch(a){case"completed":return"✓";case"failed":return"✕";case"declined":return"⊘";case"in_progress":return""}}function de(a){const e=b.c(12),{item:t}=a;if(t.type==="user_message"){let s;return e[0]!==t?(s=r.jsx(ue,{item:t}),e[0]=t,e[1]=s):s=e[1],s}if(t.type==="assistant_message"){let s;return e[2]!==t?(s=r.jsx(pe,{item:t}),e[2]=t,e[3]=s):s=e[3],s}if(t.type==="reasoning"){let s;return e[4]!==t?(s=r.jsx(me,{item:t}),e[4]=t,e[5]=s):s=e[5],s}if(t.type==="plan"){let s;return e[6]!==t?(s=r.jsx(ge,{item:t}),e[6]=t,e[7]=s):s=e[7],s}if(se(t.type)){let s;return e[8]!==t?(s=r.jsx(fe,{item:t}),e[8]=t,e[9]=s):s=e[9],s}let n;return e[10]!==t?(n=r.jsx(be,{item:t}),e[10]=t,e[11]=n):n=e[11],n}function T(a){return{"data-item-id":a.id,"data-item-type":a.type,"data-item-status":a.status}}function ue(a){const e=b.c(7),{item:t}=a;let n;e[0]!==t?(n=T(t),e[0]=t,e[1]=n):n=e[1];const s=t.text??"";let o;e[2]!==s?(o=r.jsx("div",{className:"agent-chat-user-bubble",children:s}),e[2]=s,e[3]=o):o=e[3];let i;return e[4]!==n||e[5]!==o?(i=r.jsx("div",{className:"agent-chat-row agent-chat-user-row",...n,children:o}),e[4]=n,e[5]=o,e[6]=i):i=e[6],i}function pe(a){const e=b.c(12),{item:t}=a;let n;e[0]!==t?(n=T(t),e[0]=t,e[1]=n):n=e[1];const s=t.text??"";let o;e[2]!==s?(o=$(s),e[2]=s,e[3]=o):o=e[3];let i;e[4]!==o?(i=r.jsx("div",{className:"agent-chat-markdown",dangerouslySetInnerHTML:{__html:o}}),e[4]=o,e[5]=i):i=e[5];let l;e[6]!==t.status?(l=t.status!=="completed"?r.jsx(E,{status:t.status}):null,e[6]=t.status,e[7]=l):l=e[7];let c;return e[8]!==n||e[9]!==i||e[10]!==l?(c=r.jsxs("div",{className:"agent-chat-row agent-chat-assistant-row",...n,children:[i,l]}),e[8]=n,e[9]=i,e[10]=l,e[11]=c):c=e[11],c}function me(a){const e=b.c(19),{item:t}=a,[n,s]=N.useState(!1);let o;e[0]!==t?(o=T(t),e[0]=t,e[1]=o):o=e[1];let i;e[2]===Symbol.for("react.memo_cache_sentinel")?(i=()=>s(he),e[2]=i):i=e[2];const l=n?"▾":"▸";let c;e[3]!==l?(c=r.jsx("span",{className:"agent-chat-disclosure-chevron","aria-hidden":"true",children:l}),e[3]=l,e[4]=c):c=e[4];let d;e[5]===Symbol.for("react.memo_cache_sentinel")?(d=r.jsx("span",{className:"agent-chat-reasoning-label",children:"Reasoning"}),e[5]=d):d=e[5];let u;e[6]!==t.status?(u=t.status!=="completed"?r.jsx(E,{status:t.status}):null,e[6]=t.status,e[7]=u):u=e[7];let p;e[8]!==n||e[9]!==c||e[10]!==u?(p=r.jsxs("button",{type:"button",className:"agent-chat-disclosure","aria-expanded":n,onClick:i,children:[c,d,u]}),e[8]=n,e[9]=c,e[10]=u,e[11]=p):p=e[11];let m;e[12]!==n||e[13]!==t.text?(m=n?r.jsx("div",{className:"agent-chat-markdown agent-chat-reasoning-body",dangerouslySetInnerHTML:{__html:$(t.text??"")}}):null,e[12]=n,e[13]=t.text,e[14]=m):m=e[14];let f;return e[15]!==o||e[16]!==p||e[17]!==m?(f=r.jsxs("div",{className:"agent-chat-row agent-chat-reasoning-row",...o,children:[p,m]}),e[15]=o,e[16]=p,e[17]=m,e[18]=f):f=e[18],f}function he(a){return!a}function ge(a){const e=b.c(15),{item:t}=a;let n;e[0]!==t?(n=T(t),e[0]=t,e[1]=n):n=e[1];let s;e[2]===Symbol.for("react.memo_cache_sentinel")?(s=r.jsx("span",{className:"agent-chat-badge",children:"Plan"}),e[2]=s):s=e[2];let o;e[3]!==t.status?(o=t.status!=="completed"?r.jsx(E,{status:t.status}):null,e[3]=t.status,e[4]=o):o=e[4];let i;e[5]!==o?(i=r.jsxs("div",{className:"agent-chat-plan-header",children:[s,o]}),e[5]=o,e[6]=i):i=e[6];const l=t.text??"";let c;e[7]!==l?(c=$(l),e[7]=l,e[8]=c):c=e[8];let d;e[9]!==c?(d=r.jsx("div",{className:"agent-chat-markdown",dangerouslySetInnerHTML:{__html:c}}),e[9]=c,e[10]=d):d=e[10];let u;return e[11]!==n||e[12]!==i||e[13]!==d?(u=r.jsxs("div",{className:"agent-chat-row agent-chat-plan-row",...n,children:[i,d]}),e[11]=n,e[12]=i,e[13]=d,e[14]=u):u=e[14],u}function fe(a){const e=b.c(38),{item:t}=a,[n,s]=N.useState(!1);let o;e[0]!==t.input?(o=le(t.input),e[0]=t.input,e[1]=o):o=e[1];const i=o,l=t.output?.text??"",c=t.output?.image_ids?.length??0,d=t.status==="failed"||t.output?.is_error===!0;let u;e[2]!==t?(u=T(t),e[2]=t,e[3]=u):u=e[3];let p;e[4]===Symbol.for("react.memo_cache_sentinel")?(p=()=>s(xe),e[4]=p):p=e[4];let m;e[5]!==t.status?(m=r.jsx(E,{status:t.status}),e[5]=t.status,e[6]=m):m=e[6];let f;e[7]!==t.type?(f=ie(t.type),e[7]=t.type,e[8]=f):f=e[8];let h;e[9]!==f?(h=r.jsx("span",{"aria-hidden":"true",children:f}),e[9]=f,e[10]=h):h=e[10];let w;e[11]!==t.type?(w=z(t.type),e[11]=t.type,e[12]=w):w=e[12];let g;e[13]!==h||e[14]!==w?(g=r.jsxs("span",{className:"agent-chat-badge agent-chat-tool-badge",children:[h," ",w]}),e[13]=h,e[14]=w,e[15]=g):g=e[15];let y;e[16]!==t?(y=oe(t),e[16]=t,e[17]=y):y=e[17];let _;e[18]!==y?(_=r.jsx("span",{className:"agent-chat-tool-title",children:y}),e[18]=y,e[19]=_):_=e[19];const S=n?"▾":"▸";let v;e[20]!==S?(v=r.jsx("span",{className:"agent-chat-disclosure-chevron","aria-hidden":"true",children:S}),e[20]=S,e[21]=v):v=e[21];let j;e[22]!==n||e[23]!==_||e[24]!==v||e[25]!==m||e[26]!==g?(j=r.jsxs("button",{type:"button",className:"agent-chat-disclosure agent-chat-tool-summary","aria-expanded":n,onClick:p,children:[m,g,_,v]}),e[22]=n,e[23]=_,e[24]=v,e[25]=m,e[26]=g,e[27]=j):j=e[27];let x;e[28]!==n||e[29]!==d||e[30]!==c||e[31]!==i||e[32]!==l?(x=n?r.jsxs("div",{className:"agent-chat-tool-detail",children:[i!==""?r.jsx("pre",{className:"agent-chat-mono agent-chat-tool-input",children:i}):null,l!==""?r.jsx("pre",{className:`agent-chat-mono agent-chat-tool-output${d?" is-error":""}`,children:l}):null,c>0?r.jsx("div",{className:"agent-chat-tool-images",children:c===1?"1 image attachment":`${c} image attachments`}):null,i===""&&l===""&&c===0?r.jsx("div",{className:"agent-chat-tool-images",children:"No input or output recorded."}):null]}):null,e[28]=n,e[29]=d,e[30]=c,e[31]=i,e[32]=l,e[33]=x):x=e[33];let k;return e[34]!==j||e[35]!==x||e[36]!==u?(k=r.jsxs("div",{className:"agent-chat-row agent-chat-tool-row",...u,children:[j,x]}),e[34]=j,e[35]=x,e[36]=u,e[37]=k):k=e[37],k}function xe(a){return!a}function be(a){const e=b.c(11),{item:t}=a,n=t.type==="context_compaction"?"Context compacted":t.type==="error"?"Error":"Event",s=`agent-chat-row agent-chat-system-row${t.type==="error"?" is-error":""}`;let o;e[0]!==t?(o=T(t),e[0]=t,e[1]=o):o=e[1];let i;e[2]!==n?(i=r.jsx("span",{className:"agent-chat-system-label",children:n}),e[2]=n,e[3]=i):i=e[3];let l;e[4]!==t.text?(l=t.text?r.jsx("span",{className:"agent-chat-system-text",children:t.text}):null,e[4]=t.text,e[5]=l):l=e[5];let c;return e[6]!==s||e[7]!==o||e[8]!==i||e[9]!==l?(c=r.jsxs("div",{className:s,...o,children:[i,l]}),e[6]=s,e[7]=o,e[8]=i,e[9]=l,e[10]=c):c=e[10],c}function E(a){const e=b.c(11),{status:t}=a;if(t==="in_progress"){let l,c;e[0]===Symbol.for("react.memo_cache_sentinel")?(l=r.jsx("span",{className:"agent-chat-spinner","aria-hidden":"true"}),c=r.jsx("span",{className:"agent-chat-visually-hidden",children:"In progress"}),e[0]=l,e[1]=c):(l=e[0],c=e[1]);let d;return e[2]!==t?(d=r.jsxs("output",{className:"agent-chat-status is-in-progress","data-status":t,children:[l,c]}),e[2]=t,e[3]=d):d=e[3],d}const n=`agent-chat-status is-${t}`,s=t==="failed"?"Failed":t==="declined"?"Declined":"Completed";let o;e[4]!==t?(o=ce(t),e[4]=t,e[5]=o):o=e[5];let i;return e[6]!==t||e[7]!==n||e[8]!==s||e[9]!==o?(i=r.jsx("span",{className:n,"data-status":t,"aria-label":s,children:o}),e[6]=t,e[7]=n,e[8]=s,e[9]=o,e[10]=i):i=e[10],i}function ye(){const a=b.c(1);let e;return a[0]===Symbol.for("react.memo_cache_sentinel")?(e=r.jsx("div",{className:"agent-chat-turn-separator","aria-hidden":"true"}),a[0]=e):e=a[0],e}const we=24;function _e(a,e){const t=b.c(4);let n,s;t[0]!==e||t[1]!==a?(n=()=>{const o=e?e():X(),i=J(c=>{a({type:"inbound",message:c})});let l=!1;return(async()=>{try{const c=await o.init();if(l)return;a({type:"init",result:c}),await o.subscribe()}catch(c){const d=c;l||a({type:"init-failed",detail:d instanceof Error?d.message:"Agent bridge request failed."})}})(),()=>{l=!0,i(),o.dispose()}},s=[a,e],t[0]=e,t[1]=a,t[2]=n,t[3]=s):(n=t[2],s=t[3]),N.useEffect(n,s)}function ve(a){const e=b.c(12),{createBridge:t}=a,[n,s]=N.useReducer(Y,void 0,Q);_e(s,t);let o;e[0]!==n?(o=r.jsx(je,{state:n}),e[0]=n,e[1]=o):o=e[1];let i;e[2]!==n.daemonDetail||e[3]!==n.daemonStatus||e[4]!==n.items?(i=n.daemonStatus==="unavailable"&&n.items.length>0?r.jsx(Se,{detail:n.daemonDetail}):null,e[2]=n.daemonDetail,e[3]=n.daemonStatus,e[4]=n.items,e[5]=i):i=e[5];let l;e[6]!==n?(l=r.jsx(ke,{state:n}),e[6]=n,e[7]=l):l=e[7];let c;return e[8]!==o||e[9]!==i||e[10]!==l?(c=r.jsxs("div",{className:"agent-chat-shell",children:[o,i,l]}),e[8]=o,e[9]=i,e[10]=l,e[11]=c):c=e[11],c}function je(a){const e=b.c(23),{state:t}=a,n=t.session,s=n?.provider??"",o=n?.provider;let i;e[0]!==o?(i=ae(o),e[0]=o,e[1]=i):i=e[1];let l;e[2]!==s||e[3]!==i?(l=r.jsx("span",{className:"agent-chat-provider-badge","data-provider":s,children:i}),e[2]=s,e[3]=i,e[4]=l):l=e[4];let c;e[5]!==n?(c=ne(n),e[5]=n,e[6]=c):c=e[6];let d;e[7]!==c?(d=r.jsx("span",{className:"agent-chat-header-title",children:c}),e[7]=c,e[8]=d):d=e[8];let u;e[9]!==n?(u=n?.cwd?r.jsx("span",{className:"agent-chat-header-cwd",children:n.cwd}):null,e[9]=n,e[10]=u):u=e[10];let p;e[11]!==d||e[12]!==u?(p=r.jsxs("span",{className:"agent-chat-header-titles",children:[d,u]}),e[11]=d,e[12]=u,e[13]=p):p=e[13];const m=`agent-chat-daemon-status is-${t.daemonStatus}`,f=t.daemonStatus==="unavailable"?t.daemonDetail??void 0:void 0;let h;e[14]===Symbol.for("react.memo_cache_sentinel")?(h=r.jsx("span",{className:"agent-chat-daemon-dot","aria-hidden":"true"}),e[14]=h):h=e[14];const w=t.daemonStatus==="unavailable"?"Daemon unavailable":"Live";let g;e[15]!==f||e[16]!==w||e[17]!==m?(g=r.jsxs("span",{className:m,title:f,children:[h,w]}),e[15]=f,e[16]=w,e[17]=m,e[18]=g):g=e[18];let y;return e[19]!==g||e[20]!==l||e[21]!==p?(y=r.jsxs("header",{className:"agent-chat-header",children:[l,p,g]}),e[19]=g,e[20]=l,e[21]=p,e[22]=y):y=e[22],y}function Se(a){const e=b.c(2),{detail:t}=a,n=t?`: ${t}`:".";let s;return e[0]!==n?(s=r.jsxs("output",{className:"agent-chat-daemon-banner",children:["Agent daemon unavailable",n," Showing the last known transcript."]}),e[0]=n,e[1]=s):s=e[1],s}function ke(a){const e=b.c(8),{state:t}=a;if(t.phase==="connecting"){let s;return e[0]===Symbol.for("react.memo_cache_sentinel")?(s=r.jsx(C,{title:"Connecting",detail:"Reaching the agent daemon…"}),e[0]=s):s=e[0],s}if(t.daemonStatus==="unavailable"&&t.items.length===0){const s=t.daemonDetail??"The conversation daemon could not be reached.";let o;return e[1]!==s?(o=r.jsx(C,{title:"Agent daemon unavailable",detail:s}),e[1]=s,e[2]=o):o=e[2],o}if(!t.hasSnapshot&&t.session===null){let s;return e[3]===Symbol.for("react.memo_cache_sentinel")?(s=r.jsx(C,{title:"No session",detail:"No agent session was resolved for this pane."}),e[3]=s):s=e[3],s}if(!t.hasSnapshot){let s;return e[4]===Symbol.for("react.memo_cache_sentinel")?(s=r.jsx(C,{title:"Loading transcript",detail:"Waiting for the first snapshot…"}),e[4]=s):s=e[4],s}if(t.items.length===0){let s;return e[5]===Symbol.for("react.memo_cache_sentinel")?(s=r.jsx(C,{title:"No conversation yet",detail:"This session's transcript has no items so far."}),e[5]=s):s=e[5],s}let n;return e[6]!==t?(n=r.jsx(Ne,{state:t}),e[6]=t,e[7]=n):n=e[7],n}function C(a){const e=b.c(8),{title:t,detail:n}=a;let s;e[0]!==t?(s=r.jsx("span",{className:"agent-chat-empty-title",children:t}),e[0]=t,e[1]=s):s=e[1];let o;e[2]!==n?(o=r.jsx("span",{className:"agent-chat-empty-detail",children:n}),e[2]=n,e[3]=o):o=e[3];let i;return e[4]!==s||e[5]!==o||e[6]!==t?(i=r.jsxs("div",{className:"agent-chat-empty","data-empty-title":t,children:[s,o]}),e[4]=s,e[5]=o,e[6]=t,e[7]=i):i=e[7],i}function Ne(a){const e=b.c(25),{state:t}=a,[n,s]=N.useState(null),[o,i]=N.useState(null),l=o===null,c=o!==null&&t.lastSeq>o,d=Te;let u;e[0]!==t.lastSeq||e[1]!==o?(u=x=>{const k=x.currentTarget;k.scrollTop+k.clientHeight>=k.scrollHeight-we?o!==null&&i(null):o===null&&i(t.lastSeq)},e[0]=t.lastSeq,e[1]=o,e[2]=u):u=e[2];const p=u;let m;e[3]!==l?(m=x=>{x&&l&&d(x.parentElement)},e[3]=l,e[4]=m):m=e[4];const f=m;let h;e[5]!==t.items?(h=[],t.items.forEach((x,k)=>{x.type==="user_message"&&k>0&&h.push(r.jsx(ye,{},`turn-${x.id}`)),h.push(r.jsx(de,{item:x},x.id))}),e[5]=t.items,e[6]=h):h=e[6];const w=l?"true":"false";let g;e[7]!==t.streamError?(g=t.streamError?r.jsxs("div",{className:"agent-chat-row agent-chat-system-row is-error","data-stream-error":"true",children:[r.jsx("span",{className:"agent-chat-system-label",children:"Stream error"}),r.jsxs("span",{className:"agent-chat-system-text",children:[t.streamError.message,t.streamError.recoverable?" (retrying)":""]})]}):null,e[7]=t.streamError,e[8]=g):g=e[8];const y=`anchor-${t.lastSeq}`;let _;e[9]!==f||e[10]!==y?(_=r.jsx("div",{ref:f,className:"agent-chat-anchor"},y),e[9]=f,e[10]=y,e[11]=_):_=e[11];let S;e[12]!==p||e[13]!==h||e[14]!==w||e[15]!==g||e[16]!==_?(S=r.jsxs("div",{className:"agent-chat-scroll",ref:s,onScroll:p,"data-following":w,children:[h,g,_]}),e[12]=p,e[13]=h,e[14]=w,e[15]=g,e[16]=_,e[17]=S):S=e[17];let v;e[18]!==l||e[19]!==c||e[20]!==n?(v=!l&&c?r.jsx("button",{type:"button",className:"agent-chat-jump-latest",onClick:()=>{d(n),i(null)},children:"↓ Jump to latest"}):null,e[18]=l,e[19]=c,e[20]=n,e[21]=v):v=e[21];let j;return e[22]!==S||e[23]!==v?(j=r.jsxs("div",{className:"agent-chat-timeline",children:[S,v]}),e[22]=S,e[23]=v,e[24]=j):j=e[24],j}function Te(a){a&&(a.scrollTop=a.scrollHeight)}const Ce=':root{color-scheme:light dark}[data-cmux-webview-kind=agent-chat]{margin:0;background:var(--agent-page-bg, #1e1e1e);color:var(--agent-text, #d6d6d6);font-family:var(--vscode-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);font-size:13px;-webkit-font-smoothing:antialiased}.agent-chat-shell{display:flex;flex-direction:column;height:100vh;overflow:hidden}.agent-chat-header{display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--agent-border, rgba(127, 127, 127, .25));flex:none;min-width:0}.agent-chat-provider-badge{flex:none;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;background:var(--agent-control, rgba(127, 127, 127, .16));color:var(--agent-soft, #b8b8b8)}.agent-chat-header-titles{display:flex;flex-direction:column;min-width:0;flex:1}.agent-chat-header-title{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.agent-chat-header-cwd{font-size:11px;color:var(--agent-muted, #8a8a8a);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;text-align:left}.agent-chat-daemon-status{flex:none;display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--agent-muted, #8a8a8a)}.agent-chat-daemon-dot{width:7px;height:7px;border-radius:999px;background:#00a240}.agent-chat-daemon-status.is-unavailable .agent-chat-daemon-dot{background:var(--agent-danger, #e5484d)}.agent-chat-daemon-banner{display:block;flex:none;padding:6px 12px;font-size:11px;color:var(--agent-danger, #e5484d);background:color-mix(in srgb,var(--agent-danger, #e5484d) 10%,transparent);border-bottom:1px solid var(--agent-border, rgba(127, 127, 127, .25))}.agent-chat-timeline{position:relative;flex:1;min-height:0;display:flex}.agent-chat-scroll{flex:1;overflow-y:auto;padding:12px 14px 18px;scrollbar-width:thin}.agent-chat-anchor{height:1px}.agent-chat-row{margin:0 auto;max-width:760px;padding:4px 0}.agent-chat-turn-separator{margin:10px auto;max-width:760px;border-top:1px solid var(--agent-border, rgba(127, 127, 127, .2))}.agent-chat-user-row{display:flex;justify-content:flex-end}.agent-chat-user-bubble{max-width:85%;padding:7px 11px;border-radius:12px;background:var(--agent-control, rgba(127, 127, 127, .16));white-space:pre-wrap;overflow-wrap:anywhere}.agent-chat-assistant-row{display:flex;flex-direction:column;gap:4px}.agent-chat-markdown{line-height:1.5;overflow-wrap:anywhere}.agent-chat-markdown pre{padding:8px 10px;border-radius:8px;background:var(--agent-input-bg, rgba(127, 127, 127, .12));overflow-x:auto;font-family:var(--vscode-editor-font-family, ui-monospace, "SF Mono", Menlo, monospace);font-size:12px}.agent-chat-markdown code{font-family:var(--vscode-editor-font-family, ui-monospace, "SF Mono", Menlo, monospace);font-size:12px}.agent-chat-markdown a{color:var(--agent-accent, #4f8ff7)}.agent-chat-reasoning-row{color:var(--agent-muted, #8a8a8a)}.agent-chat-reasoning-body{padding:4px 0 2px 18px;font-size:12px}.agent-chat-plan-row{border-left:2px solid var(--agent-accent, #4f8ff7);padding-left:10px}.agent-chat-plan-header{display:flex;align-items:center;gap:6px;margin-bottom:2px}.agent-chat-disclosure{display:flex;align-items:center;gap:6px;width:100%;min-width:0;padding:2px 0;border:none;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer}.agent-chat-disclosure-chevron{flex:none;color:var(--agent-muted, #8a8a8a);font-size:10px}.agent-chat-badge{flex:none;padding:1px 6px;border-radius:5px;font-size:10.5px;font-weight:600;background:var(--agent-control, rgba(127, 127, 127, .16));color:var(--agent-soft, #b8b8b8)}.agent-chat-tool-row{font-size:12px}.agent-chat-tool-title{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--agent-soft, #b8b8b8);font-family:var(--vscode-editor-font-family, ui-monospace, "SF Mono", Menlo, monospace);font-size:11.5px}.agent-chat-tool-detail{display:flex;flex-direction:column;gap:6px;padding:4px 0 4px 18px}.agent-chat-mono{margin:0;padding:8px 10px;border-radius:8px;background:var(--agent-input-bg, rgba(127, 127, 127, .12));font-family:var(--vscode-editor-font-family, ui-monospace, "SF Mono", Menlo, monospace);font-size:11.5px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere;overflow-y:auto;max-height:320px}.agent-chat-tool-output.is-error{color:var(--agent-danger, #e5484d)}.agent-chat-tool-images{font-size:11px;color:var(--agent-muted, #8a8a8a)}.agent-chat-system-row{display:flex;gap:8px;align-items:baseline;font-size:11.5px;color:var(--agent-muted, #8a8a8a)}.agent-chat-system-row.is-error{color:var(--agent-danger, #e5484d)}.agent-chat-system-label{flex:none;font-weight:600}.agent-chat-system-text{overflow-wrap:anywhere}.agent-chat-status{flex:none;display:inline-flex;align-items:center;font-size:11px;color:var(--agent-muted, #8a8a8a)}.agent-chat-status.is-failed,.agent-chat-status.is-declined{color:var(--agent-danger, #e5484d)}.agent-chat-spinner{width:10px;height:10px;border-radius:999px;border:1.5px solid var(--agent-muted, #8a8a8a);border-top-color:transparent;animation:agent-chat-spin .8s linear infinite}@keyframes agent-chat-spin{to{transform:rotate(360deg)}}.agent-chat-visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.agent-chat-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;color:var(--agent-muted, #8a8a8a)}.agent-chat-empty-title{font-weight:600;color:var(--agent-soft, #b8b8b8)}.agent-chat-empty-detail{font-size:12px}.agent-chat-jump-latest{position:absolute;bottom:14px;left:50%;transform:translate(-50%);padding:5px 12px;border:1px solid var(--agent-border, rgba(127, 127, 127, .25));border-radius:999px;background:var(--agent-surface-elevated, #2a2a2a);color:var(--agent-text, #d6d6d6);font:inherit;font-size:12px;cursor:pointer;box-shadow:0 2px 8px #00000040}';function qe(a){F("agent-chat",Ce),document.documentElement.dataset.cmuxWebviewKind="agent-chat",document.body.dataset.cmuxWebviewKind="agent-chat";const e=P(()=>r.jsx(ve,{}));B.createRoot(a).render(r.jsx(D,{router:e}))}export{qe as mountAgentChatSurface};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reducer drops unknown events

Low Severity

After the sequence guard, applyAgentEvent only handles known AgentEvent types in its switch. If the wire payload has an unexpected type (version skew or malformed frame), the function returns undefined, which would pass that to useReducer and can break the chat surface.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b7eb759. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 7445ec1: unknown event types are tolerated and counted instead of silently dropped (reserved names like content.delta stay forward-compatible).

…odes, daemon client start state, fail-closed mock bridge

Claude session discovery treats the encoded project dir as an
optimization and falls back to the parsed per-line cwd (the encoding has
varied across Claude versions). Codex object-form tool outputs preserve
exit-code failure detection. The daemon client marks itself started only
after launch succeeds. chat.subscribe always answers its reply handler.
The fixture mock bridge engages only in dev/test; a packaged app with no
WebKit handler fails closed instead of rendering fake data. Plus
truncation re-read errors surface as recoverable error events, blank
subscription_id is invalid_request, clip-path replaces deprecated clip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 4 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a971c96. Configure here.

"transcript_path": transcriptURL.path,
])
subscriptionId = opened["subscription_id"] as? String
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subscribe failure leaves daemon running

Medium Severity

When chat.subscribe fails after AgentDaemonClient.start() (e.g. hello or agent.session.open errors), subscribe() throws without tearing down the child assigned to daemonClient. The chat window stays open, so the cmuxd-remote process can keep running until the user closes the window or opens another session.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a971c96. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c14e701: a failed chat.subscribe tears down the spawned daemon child instead of leaving it running.

dispatch({
type: "init-failed",
detail: error instanceof Error ? error.message : "Agent bridge request failed.",
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subscribe errors mark daemon unavailable

Medium Severity

After a successful chat.init with daemon_status: "ready", a failed chat.subscribe dispatches init-failed, which forces daemonStatus to unavailable. Transcript or open failures then show “Agent daemon unavailable” even when the locator reported the daemon ready and no child was started.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a971c96. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7445ec1: subscribe failure after a ready init is now its own error state and no longer marks the daemon unavailable.

presentNoSessionAlert()
return
}
AgentChatWindowController.shared.present(for: resolution)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing transcript still opens chat

Medium Severity

View Chat presents the window whenever resolve returns a Resolution, even when transcriptURL is nil. The indexed Claude/Codex session exists but no file was found, so users get an empty chat UI instead of the localized no-transcript or no-session alert.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a971c96. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current pane flow guards this: AgentChatPresenter.swift:100 bails to a localized alert when resolution has no transcriptURL, so no chat surface opens (the original window-based path this thread was filed against is gone).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a971c969cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +48 to +49
if let fallback = newestCachedBinary(goOS: goOS, goArch: goArch, excludingVersion: version) {
return .found(fallback)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify fallback daemon compatibility before using it

When the current app version's daemon is not cached, this accepts any other cached cmuxd-remote, including versions from before this commit that do not advertise agent.conversation. In that common post-upgrade state, chat.init reports the daemon as ready and subscribe() ignores the hello capabilities, but agent.session.open then fails with unknown_method; either require the current daemon or check the advertised capability before treating a fallback as usable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c14e701: the newest-cached fallback is gated on a capability check instead of being trusted blindly.

Comment on lines +57 to +59
reader := &transcriptReader{path: config.TranscriptPath}
parser := newTranscriptParser(config.Provider, config.TranscriptPath)
if err := reader.seekForSnapshot(config.MaxSnapshotBytes); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve Codex metadata when tailing large transcripts

For Codex rollouts larger than the 32 MiB snapshot cap, seekForSnapshot jumps to the tail before the parser sees the initial session_meta line, and Open with a transcript_path never falls back to the filename/head scan for the session id or cwd. The emitted snapshot then carries an empty SessionRef and replaces the Swift-provided session in the web reducer, so long Codex conversations lose their header/session identity; parse the head metadata before seeking or derive the Codex id from the path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0f7bb75 (failing test) + f9a0570: when the snapshot cap cuts off the transcript head, the open path recovers session id/cwd/title with the same bounded head scan discovery uses.

title={state.daemonStatus === "unavailable" ? (state.daemonDetail ?? undefined) : undefined}
>
<span className="agent-chat-daemon-dot" aria-hidden="true" />
{state.daemonStatus === "unavailable" ? "Daemon unavailable" : "Live"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Localize the new agent-chat UI strings

The root AGENTS.md requires every user-facing string to be localized, but the new React chat surface hard-codes English labels such as Daemon unavailable/Live here (and similar empty-state/button text elsewhere) without entries in Resources/Localizable.xcstrings or the web message catalogs. This ships untranslated UI for Japanese users; route these strings through the existing localization catalogs before release.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
Sources/AgentChat/AgentDaemonClient.swift (1)

71-82: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

allocateRequestId() doesn't check started state.

The previous review suggested gating allocation on !started || !process.isRunning. Currently, only terminated is checked. If a caller invokes request() before start(), allocateRequestId() succeeds, but the subsequent write to stdinPipe will fail (pipe not connected to a running process).

While callers should call start() first, adding a !started check would fail faster with a clearer error.

Suggested fix
 private func allocateRequestId() throws -> Int {
     lock.lock()
     defer { lock.unlock() }
-    if terminated {
+    if terminated || !started {
         throw DaemonError(code: "terminated", message: "agent daemon is not running")
     }
     let requestId = nextRequestId
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AgentChat/AgentDaemonClient.swift` around lines 71 - 82,
allocateRequestId currently only checks terminated and can return an id before
the daemon is started; modify allocateRequestId to also verify that started is
true and that process.isRunning is true while holding lock (same place you check
terminated), and if either is false throw a DaemonError (e.g., code "notStarted"
or "daemon_not_running") with a clear message so callers fail fast before
attempting to write to stdinPipe; keep the lock.lock()/defer unlock, increment
nextRequestId and return as before.
Sources/AgentChat/AgentChatWebViewController.swift (1)

188-205: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Silent failure on JSON serialization error.

pushToPage silently returns when JSONSerialization.data(withJSONObject:) fails. While rare, if daemon frames contain values that can't be serialized (e.g., NSNull in unexpected places), the webview will never receive the event and there's no diagnostic trace.

Consider adding debug logging for serialization failures to aid troubleshooting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/AgentChat/AgentChatWebViewController.swift` around lines 188 - 205,
The pushToPage helper currently swallows JSONSerialization failures, so update
pushToPage(_:) to catch and log serialization errors (including the message
dictionary and error description) before returning; locate pushToPage and its
caller handleDaemonEvent(_:), ensure the guard around
JSONSerialization.data(withJSONObject:) is replaced with a do/catch or
conditional that logs via your logger or print (including the message contents
and serialization error) when serialization fails, then still return without
calling webView.evaluateJavaScript.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/AgentChat/AgentChatWebViewController.swift`:
- Around line 188-205: The pushToPage helper currently swallows
JSONSerialization failures, so update pushToPage(_:) to catch and log
serialization errors (including the message dictionary and error description)
before returning; locate pushToPage and its caller handleDaemonEvent(_:), ensure
the guard around JSONSerialization.data(withJSONObject:) is replaced with a
do/catch or conditional that logs via your logger or print (including the
message contents and serialization error) when serialization fails, then still
return without calling webView.evaluateJavaScript.

In `@Sources/AgentChat/AgentDaemonClient.swift`:
- Around line 71-82: allocateRequestId currently only checks terminated and can
return an id before the daemon is started; modify allocateRequestId to also
verify that started is true and that process.isRunning is true while holding
lock (same place you check terminated), and if either is false throw a
DaemonError (e.g., code "notStarted" or "daemon_not_running") with a clear
message so callers fail fast before attempting to write to stdinPipe; keep the
lock.lock()/defer unlock, increment nextRequestId and return as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8d9c764b-dc97-459e-aade-03b9e4011d2d

📥 Commits

Reviewing files that changed from the base of the PR and between b7eb759 and a971c96.

📒 Files selected for processing (10)
  • Resources/Localizable.xcstrings
  • Resources/markdown-viewer/webviews-app/chunks/agentChatSurface.mjs
  • Sources/AgentChat/AgentChatWebViewController.swift
  • Sources/AgentChat/AgentDaemonClient.swift
  • daemon/remote/agentconv/codex.go
  • daemon/remote/agentconv/discover.go
  • daemon/remote/agentconv/subscription.go
  • daemon/remote/cmd/cmuxd-remote/agent_conversation.go
  • webviews/src/agent-chat/bridge.ts
  • webviews/src/agent-chat/styles.css

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Dogfood verdict: hold for redesign. (1) View Chat on a resumed session opened nothing (no window, no alert); needs root-cause with logging on the resolution path. (2) Direction change: render as a workspace pane (reuse the agent-session panel infra), not a standalone window, exposed through a command palette action as the shared entrypoint. (3) The layer should also ingest claude/codex/opencode hooks as a live push source feeding the same canonical stream (turn boundaries, tool lifecycle, permission prompts via the reserved request./turn. event names), with transcripts staying the replay/backfill ground truth. Parsers, daemon RPC, protocol, and the /agent-chat surface carry over unchanged.

lawrencecchen and others added 3 commits June 10, 2026 01:14
…sation stream

Activates the reserved turn.started/completed and request.opened/resolved
events. The daemon listens on a per-user unix socket while subscriptions
are open; the new agent-hook-emit verb translates native Claude hook
stdin payloads and is fail-open by design. One merge point owns dedup by
tool_use_id (hook wins latency, transcript wins content). GUI renders
real turn boundaries and a pending-permission banner. Hook config
injection contract documented; Swift launcher wiring is a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lette execution

Replaces the standalone window with an AgentChatPanel opened as a split
(same family as the agent-session panel), one shared Open Agent Chat
action in the command palette with the Window menu routing through it,
debug logging across the presenter/resolver path, and a new
debug.command_palette.execute socket verb that dispatches any palette
command through the same handler registry the palette UI uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ministic palette execution window

Root cause (preflight evidence, fetched= empty in resource timing): WKWebView
does not execute <script type=module src> under file:// (CORS-opaque origin),
so main.mjs never ran and the surface never mounted, while dynamic import()
from injected script works. didFinish now kicks ./main.mjs when #root is
empty; serving over the diff-viewer custom scheme is the principled follow-up.
debug.command_palette.execute falls back to the first cmux main window when
key/main are nil (background or mid-restore), so socket-driven execution is
deterministic. Verified end to end on a resumed claude session: palette
command opens the pane, conversation renders, new prompts tail in live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude 'No response requested.' placeholder lines (isApiErrorMessage
synthetic) are dropped; real API-error lines render as failed error
rows; '[Request interrupted...]' user lines render as Stopped markers
instead of user bubbles; and a derived typing indicator shows while a
hook turn is open, a tool is in progress, or the newest item is an
unanswered user message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… emits it with isApiErrorMessage=false too)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lawrencecchen and others added 3 commits June 11, 2026 23:51
Codex writes session_meta as the first transcript line; a tail-only
replay of a transcript over MaxSnapshotBytes reported an empty session
id/cwd.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ipt head

When seekForSnapshot cuts the head off, run the same bounded head scan
discovery uses and fill only the fields the tail replay left empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lawrencecchen and others added 2 commits June 11, 2026 23:56
The webview asset rebuild for the previous commit was killed by local
memory pressure, leaving agentChatSurface.mjs stale. Budget growth is
+10 lines across four files of socket-verb registration and app wiring
for the chat pane.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cycle

Two autoreview findings. readNewLines materialized the whole transcript
delta per poll (io.ReadAll of size-offset), so one large append could
allocate hundreds of MB; reads are now capped at the snapshot-cap budget
per call and the partial-line carry drains the backlog across polls.
addScriptMessageHandler retains the controller while the controller owns
the web view chain that owns the userContentController, leaking every
closed chat pane's WKWebView; teardownForClose now removes the handler
and clears the web view, mirroring AgentSessionWebRendererCoordinator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two autoreview findings. The Go producer's omitempty drops items from a
zero-item snapshot and the reducer treated the field as required, so a
fresh/metadata-only session crashed instead of rendering the empty
state; the TS contract now marks snapshot items optional (absent ==
empty) and the reducer defaults to []. The chat pane accepted
onRequestPanelFocus but never wired it, so clicking the chat body kept
focus on the previous panel; the controller now hosts an
AgentSessionWebView (the pointer-down focus subclass, nothing in it is
session-specific) and the representable threads the focus callback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lawrencecchen and others added 2 commits June 12, 2026 00:19
Lexicographic sorting put 0.9.0 ahead of 0.10.0, so the cached-daemon
fallback could probe an older incompatible binary and report the cache
too old while a newer compatible one sat next to it. Versions now order
by numeric component (missing parts are 0, non-numeric parts compare as
strings), with Swift Testing coverage wired into the test target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A subscription held open on a busy session appended items (and their id
maps, mirrored by the GUI) without bound. Config gains MaxLiveItems
(default 4096): crossing it trims to 3/4 and resynchronizes the client
with a fresh snapshot, the same protocol path as transcript truncation;
the initial and truncation snapshots trim to the bound too.

The hook ingest socket parent lives at a well-known /tmp name; a
pre-created symlink could redirect socket placement. The listener now
requires a real directory owned by the current uid (Lstat, fail closed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tests job compiles the file against module 'cmux'; the bare
@testable import only existed in Debug as cmux_DEV. Build-only local
gates never compile test targets, so CI caught it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — 5ef315db Deployed Jun 12, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants