Repository navigation
Agent conversation protocol P1: Go normalizer in cmuxd-remote + TanStack chat surface - #5736
lawrencecchen wants to merge 29 commits into
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds 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. ChangesAgent Chat Feature
🎯 4 (Complex) | ⏱️ ~60 minutes
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
💡 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".
| 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('-') | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
Greptile SummaryAdds a normalized agent-conversation layer (
Confidence Score: 4/5Safe 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
Sequence DiagramsequenceDiagram
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
Reviews (3): Last reviewed commit: "Address review feedback: discovery cwd r..." | Re-trigger Greptile |
| 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 | ||
| } |
There was a problem hiding this comment.
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().
There was a problem hiding this comment.
Fixed in c14e701: the daemon binary locate runs off the main actor; the controller awaits it.
| } | ||
| 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." | ||
| ) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in c14e701: error copy is localized human text (en+ja); env var names no longer appear in user-facing strings.
There was a problem hiding this comment.
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
📒 Files selected for processing (46)
Resources/Localizable.xcstringsResources/markdown-viewer/webviews-app/agent-chat.htmlResources/markdown-viewer/webviews-app/chunks/agentChatSurface.mjsResources/markdown-viewer/webviews-app/chunks/agentSessionSurface.mjsResources/markdown-viewer/webviews-app/chunks/diffSurface.mjsResources/markdown-viewer/webviews-app/chunks/installWebviewStyles.mjsResources/markdown-viewer/webviews-app/chunks/markdown.mjsResources/markdown-viewer/webviews-app/main.mjsSources/AgentChat/AgentChatPresenter.swiftSources/AgentChat/AgentChatTranscriptResolver.swiftSources/AgentChat/AgentChatWebViewController.swiftSources/AgentChat/AgentChatWindowController.swiftSources/AgentChat/AgentDaemonBinaryLocator.swiftSources/AgentChat/AgentDaemonClient.swiftSources/cmuxApp.swiftcmux.xcodeproj/project.pbxprojdaemon/remote/README.mddaemon/remote/agentconv/claude.godaemon/remote/agentconv/codex.godaemon/remote/agentconv/conversation.godaemon/remote/agentconv/discover.godaemon/remote/agentconv/parser_test.godaemon/remote/agentconv/protocol.godaemon/remote/agentconv/subscription.godaemon/remote/agentconv/subscription_test.godaemon/remote/agentconv/testdata/claude/basic.jsonldaemon/remote/agentconv/testdata/claude/expected.jsondaemon/remote/agentconv/testdata/codex/basic.jsonldaemon/remote/agentconv/testdata/codex/expected.jsondaemon/remote/cmd/cmuxd-remote/agent_conversation.godaemon/remote/cmd/cmuxd-remote/agent_conversation_test.godaemon/remote/cmd/cmuxd-remote/main.godocs/agent-conversation-protocol.mdscripts/build-webviews-app.shwebviews/src/agent-chat/bridge.tswebviews/src/agent-chat/conversationStore.test.tswebviews/src/agent-chat/conversationStore.tswebviews/src/agent-chat/mockBridge.tswebviews/src/agent-chat/protocol.tswebviews/src/agent-chat/react/AgentChatApp.tsxwebviews/src/agent-chat/react/display.tswebviews/src/agent-chat/react/rows.tsxwebviews/src/agent-chat/styles.csswebviews/src/main.tsxwebviews/src/router.tsxwebviews/src/surfaces/agentChatSurface.tsx
…es the chat window) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| …`},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}; |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit b7eb759. Configure here.
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
There are 4 total unresolved issues (including 1 from previous review).
❌ 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 | ||
| } |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit a971c96. Configure here.
There was a problem hiding this comment.
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.", | ||
| }); |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit a971c96. Configure here.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit a971c96. Configure here.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
💡 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".
| if let fallback = newestCachedBinary(goOS: goOS, goArch: goArch, excludingVersion: version) { | ||
| return .found(fallback) |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Fixed in c14e701: the newest-cached fallback is gated on a capability check instead of being trusted blindly.
| reader := &transcriptReader{path: config.TranscriptPath} | ||
| parser := newTranscriptParser(config.Provider, config.TranscriptPath) | ||
| if err := reader.seekForSnapshot(config.MaxSnapshotBytes); err != nil { |
There was a problem hiding this comment.
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 👍 / 👎.
| title={state.daemonStatus === "unavailable" ? (state.daemonDetail ?? undefined) : undefined} | ||
| > | ||
| <span className="agent-chat-daemon-dot" aria-hidden="true" /> | ||
| {state.daemonStatus === "unavailable" ? "Daemon unavailable" : "Live"} |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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 checkstartedstate.The previous review suggested gating allocation on
!started || !process.isRunning. Currently, onlyterminatedis checked. If a caller invokesrequest()beforestart(),allocateRequestId()succeeds, but the subsequent write tostdinPipewill fail (pipe not connected to a running process).While callers should call
start()first, adding a!startedcheck 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 valueSilent failure on JSON serialization error.
pushToPagesilently returns whenJSONSerialization.data(withJSONObject:)fails. While rare, if daemon frames contain values that can't be serialized (e.g.,NSNullin 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
📒 Files selected for processing (10)
Resources/Localizable.xcstringsResources/markdown-viewer/webviews-app/chunks/agentChatSurface.mjsSources/AgentChat/AgentChatWebViewController.swiftSources/AgentChat/AgentDaemonClient.swiftdaemon/remote/agentconv/codex.godaemon/remote/agentconv/discover.godaemon/remote/agentconv/subscription.godaemon/remote/cmd/cmuxd-remote/agent_conversation.gowebviews/src/agent-chat/bridge.tswebviews/src/agent-chat/styles.css
|
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. |
…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>
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>
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>
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>


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 bydaemon/remote/agentconv/protocol.go. Golden fixtures underdaemon/remote/agentconv/testdata/are the wire-form contract both sides test against.Producer (Go, in cmuxd-remote).
daemon/remote/agentconvparses 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 verbsagent.sessions.list/agent.session.open/agent.session.closenext to the existingpty.*verbs, capabilityagent.conversation, working over plainserve --stdio. One implementation runs everywhere transcripts live: headless Linux cmux servers and macOS.GUI (webviews TanStack). New
/agent-chatsurface: 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(honoringCLAUDE_CONFIG_DIR,~/.codex-accounts/claude/*,CODEX_HOME, same roots as the resume path), spawns the cachedcmuxd-remotebinary as a local stdio child, and relays canonical events into the webview over theagentChatbridge. Binary resolution: theCMUX_REMOTE_DAEMON_BINARYdev override (gated byCMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1), then the existing checksum-verifiedremote-daemonscache (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~/.claudeand~/.codextranscripts (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.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 inResources/Localizable.xcstrings. The webview surface's strings are English-only, matching the existing agent-session surface.Known limits (by design, P1)
claude --resume.content.deltatoken streaming (transcripts append whole items; names reserved).cmux sshbootstrap).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
Need help on this PR? Tag
/codesmithwith 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.htmlentry (data-cmux-webview-kind="agent-chat") that boots the bundled TanStack/React chunk. The surface talks to the host via theagentChatWebKit 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-remoteand 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
cmux_DEV) and Release (cmux) builds.Migration
cmuxd-remoteexists or setCMUX_REMOTE_DAEMON_ALLOW_LOCAL_BUILD=1withCMUX_REMOTE_DAEMON_BINARY; the app gates on theagent.conversationcapability.Written for commit 5ef315d. Summary will update on new commits.
Summary by CodeRabbit
New Features
Documentation