Skip to content

feat(mobile): live ACP agent sessions end to end (host harness, mobile UI, desktop gate) - #5580

Merged
saddlepaddle merged 14 commits into
ParadevAI:mainfrom
bra1nDump:docs/acp-vs-sdk-comparison
Jul 11, 2026
Merged

saddlepaddle merged 14 commits into
ParadevAI:mainfrom
bra1nDump:docs/acp-vs-sdk-comparison

Conversation

@bra1nDump

@bra1nDump bra1nDump commented Jul 11, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

This adds an experimental way to start and control a coding-agent session running on a desktop host from the iOS app. The mobile app is a remote UI; the agent process, workspace access, permissions, and native session files stay on the desktop.

The feature is off by default behind Desktop → Settings → Security → Enable live agent sessions (experimental).

iOS session UI
  → relay (authenticated HTTP + WebSocket proxy)
  → host-service tRPC + session stream
  → AcpSessionManager
  → one claude-agent-acp child per active session
  → Claude's native on-disk session store

The detailed ACP-vs-direct-SDK walkthrough is in the standalone comparison page. This PR body is the shorter implementation guide for reviewers.

What changed

Area Responsibility
packages/host-service Owns adapter processes, ACP JSON-RPC, session state, permissions, the bounded update journal, WebSocket fan-out, tRPC commands, and restart resurrection.
packages/host-client (new) Platform-neutral relay transport: SuperJSON tRPC calls, one forced-token refresh after a 401, and fresh authenticated WebSocket URLs on reconnect.
packages/session-protocol (new) Shared session/frame contracts, input schemas, timeline fold, reconnect/dedup/reset client, and React hooks.
apps/mobile iOS session list and thread UI: open a workspace's context menu → Live sessions, then drive messages, plans, nested tool calls, model/mode/effort controls, permission and question cards, cancel, pagination, and reconnect/error banners.
apps/desktop Stores the opt-in security setting and restarts host children with SUPERSET_ACP_SESSIONS=1. It does not render this session UI.

The existing Mastra chat path is not rewritten by this PR; live ACP sessions have a separate mobile route while the feature is evaluated.

Protocol flow

Create and run a turn

The public Superset session id is different from the adapter's native session id.

mobile → host tRPC  acpSessions.create { sessionId, workspaceId }
host   → adapter    initialize
host   → adapter    session/new { cwd, mcpServers: [] }
adapter→ host       { sessionId: <native ACP id>, modes, configOptions }
host   → mobile     { sessionId: <Superset id>, status: "idle", ... }

A prompt is admission-acknowledged over HTTP; a potentially long-running turn does not keep a relay request open:

mobile → host tRPC  acpSessions.prompt { sessionId, prompt }
host   → mobile     { accepted: true }
host   → adapter    session/prompt

adapter→ host       session/update notifications
host   → mobile WS  { seq, sessionId, frame }

When the adapter blocks on a tool or AskUserQuestion, the host parks the ACP request and streams a renderable card to mobile. The mobile response resolves that exact request id:

adapter→ host       session/request_permission { toolCall, options }
host   → mobile WS  permission_requested { requestId, ... }
mobile → host tRPC  respondToPermission { requestId, outcome }
host   → adapter    { outcome }
host   → mobile WS  permission_resolved

Single-select, multi-select, and skipped question answers are supported. The adapter's paired free-text *_custom fields are intentionally ignored today, so the mobile flow is tap-only. Multi-select answers use ACP's _meta extension while preserving a normal first optionId for single-select consumers.

Reconnect and restart

Each runtime has a gapless seq journal. A reconnect presents its last sequence; the host replays (since, latest] and then attaches the live subscriber without a handoff gap. Duplicates are ignored. A missing/evicted cursor produces a terminal reset, after which the client refetches state/history and opens a fresh stream URL/token.

Current caveat: the numeric cursor does not yet include a journal-incarnation id. A new journal starts at 1 after host resurrection, so an overlapping pre-restart cursor is not guaranteed to be recognized as stale by number alone. The mobile hook's normal restart path refetches state/history before attaching; making cross-incarnation rejection intrinsic to every cursor is tracked as P0 follow-up.

Host SQLite stores only the durable binding needed to find the harness-owned session again:

Superset session id → workspace id + native ACP session id + harness + cwd

After a host restart, list/get return the row as status: "offline" without starting a process. The first live operation (getMessages, prompt, permission/config command, or stream attach) spawns a new adapter and calls:

host → adapter  session/load { sessionId: <native ACP id>, cwd }
adapter→ host   replayed session/update notifications
host → mobile   rebuilt transcript + live updates

An in-flight turn and pending permission callbacks do not survive a host restart. Replayed tool calls that were left open are terminalized instead of appearing to run forever.

If the native Claude session file was deleted or is corrupt, session/load fails. The registry row remains offline and retryable; the stream emits reset: "session_load_failed", the hook retries through tRPC, and the actual load error is shown in the thread's destructive error banner. The composer stays hidden until resurrection succeeds, and no replacement session is silently created.

Security and feature gating

  • The setting defaults off. When off, the WebSocket route is not mounted and every ACP procedure except list rejects with PRECONDITION_FAILED.
  • list remains authenticated but returns { items: [], enabled: false }, so the sessions screen can feature-detect without an extra capability endpoint.
  • Existing host auth protects both tRPC and WebSocket paths; reconnects mint a fresh short-lived JWT.
  • ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN are removed at the adapter spawn boundary, so a repo .env cannot override the user's Claude login.
  • Host SQLite stores registry metadata only. Message text, tool payloads, permission payloads, and journal frames are not copied into that table.

History, memory, and validation boundaries

The host journal is a fixed-capacity O(1) circular buffer (5,000 envelopes), and pre-runtime session/load replay buffering is capped to the same window. This prevents lifetime-unbounded host memory, but the journal still serves both recent reconnect catch-up and getMessages pagination. Because ACP exposes whole-transcript replay rather than paginated native history, history older than the retained window is not currently available through Superset.

The React hook retains the pages the user loaded plus live envelopes for the current mount so it can refold the timeline. Separating disk-backed history from a much smaller catch-up ring, and bounding the live client overlay, is explicitly follow-up work rather than something this PR claims to have solved.

TypeScript is strict across the production path, but runtime validation is not complete. tRPC inputs are Zod-validated and the official ACP SDK validates its registered request/notification boundary; nested WebSocket payloads, generic relay outputs, adapter responses, and SQLite rows are not all fully parsed by canonical Zod schemas yet. The current truth and the hardening plan are documented in packages/host-service/docs/acp-sessions.md and plans/acp-session-follow-ups.md.

Test coverage

Primary confidence: authenticated real-Claude runs

The ACP_E2E=1 suites are the important compatibility evidence. Nothing at the model/adapter boundary is mocked: they use this Mac's authenticated Claude CLI account, a real Sonnet model, the pinned claude-agent-acp executable, real ACP JSON-RPC over stdio, AcpSessionManager, and the real WebSocket route/client.

The manager suite covers real initialize/create/prompt/fold behavior, a saved multi-agent Workflow, AskUserQuestion, tool permissions, cancellation after permission, first-response-wins, parallel tool use, and adapter death. The stream suite runs the same real adapter/model through the actual WebSocket route and sync client, covering concurrent subscribers, a mid-turn disconnect/cursor reconnect with no gaps or duplicates, and eviction reset/re-attach.

The Workflow assertion does not stop at Claude's asynchronous async_launched acknowledgement. It waits for the persisted run to reach completed, then verifies all five Sonnet agents, non-zero real token/tool usage, two parallel audits, and the final structured verdict. In the latest complete authenticated run, that Workflow finished in 36.1 seconds with 5 Sonnet agents, 77,981 tokens, and 14 tool calls.

Run the full authenticated lane on a logged-in Mac whenever changing the ACP runtime, adapter/SDK, Workflow handling, permissions, questions, cancellation, stream/reconnect/sequencing, or resurrection:

cd packages/host-service
ACP_E2E=1 ACP_E2E_MODEL=sonnet ACP_E2E_EFFORT=low \
  bun test \
    test/integration/acp-sessions.integration.test.ts \
    test/integration/acp-sessions-stream.integration.test.ts

These tests are skipped in ordinary CI only because CI does not have a Claude login and the runs spend real tokens. That is an infrastructure limitation, not a signal that this lane is optional; until an authenticated runner exists, the agent making a relevant change is expected to run it locally and report the real result.

Latest full authenticated result: 10/10 passed, 113 assertions, 72.1 seconds.

One current adapter behavior is pinned explicitly: Claude emits two parallel Bash tool uses together, but claude-agent-acp 0.56.0 exposes their permission callbacks to Superset one at a time. Superset still supports truly simultaneous pending permission requests; that state-machine case is exercised by the deterministic backup below.

Belt and suspenders: deterministic regression breadth

The always-run deterministic ACP adapter is deliberately secondary. It speaks real ACP JSON-RPC over stdio through the official SDK and gives cheap breadth plus exact fault injection, but it uses no Claude model/account/network and cannot establish real adapter/model compatibility.

Covered scenarios include:

  • a 30-turn marathon with gapless streaming, pagination, and identical refolding;
  • prompt admission/rejection/recovery and concurrent turns;
  • allow/deny, stale duplicate answers, multiple pending permissions, and single-/multi-question elicitation forms;
  • cancel mid-tool, cancel while permission-blocked, adapter death, and sibling-session isolation;
  • stream drop/reconnect from cursor, duplicate suppression, malformed frames, journal eviction/reset, and two concurrent subscribers;
  • real host-client question answering, in-flight cancellation, simultaneous permissions, cursor reconnect, host/app/server/SQLite close and rebuild, passive offline listing, session/load transcript replay, bounded replay, and deleted native-session failure;
  • mobile presentation regression coverage for the exact missing-transcript banner, the Session could not be resumed empty state, and the disabled composer;
  • create idempotency/workspace conflicts, list/message cursor validation, model/mode/config updates, title clearing, feature gate/auth, and credential scrubbing;
  • relay serialization, real tRPC error messages, 401 refresh-once, and fresh WebSocket tokens.

The host-client boundary suite is another belt-and-suspenders layer: it starts the real createApp host and crosses actual auth, HTTP/SuperJSON/tRPC, WebSockets, child-process stdio, and on-disk SQLite through @superset/host-client; only the model/adapter behavior is deterministic. It is especially valuable for the question, abort, simultaneous-permission, reconnect, full host/app/server/DB rebuild, resume, and missing-native-transcript paths, but it does not replace the authenticated real-Claude lane.

Current ACP evidence on this head: the authenticated Sonnet lane is 10/10 (113 assertions), and the focused deterministic ACP/runtime/host-client lane is 49/49 (553 assertions). Host-service typecheck and root lint also pass. The broader branch baseline previously passed Sherif, 34/34 Turbo typecheck tasks, 14/14 Turbo test tasks (including 835 host-service tests), release typecheck, 15/15 release tests, unified-version validation, and an iOS production Metro export (5,556 modules).

Suggested review order

  1. packages/host-service/docs/acp-sessions.md — current behavior and known limits.
  2. packages/host-service/src/runtime/acp-sessions/acp-sessions.ts — lifecycle and ACP bridge.
  3. packages/host-service/src/runtime/acp-sessions/{persistence,journal,stream}.ts — durability and delivery.
  4. packages/session-protocol/src/{api,envelope,state}.ts and src/client/subscribeToSession — shared wire contract.
  5. packages/session-protocol/src/react/useAcpSession — resync/pagination state owner.
  6. apps/mobile/.../chat/acp/ — iOS presentation and interaction layer.
  7. packages/host-service/test/integration/acp-sessions*.test.ts — executable behavior matrix.

Remaining architecture and production-hardening work is centralized in plans/acp-session-follow-ups.md; completed implementation history is archived under plans/done/.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an ACP live-session system spanning shared protocol contracts, host runtime and streaming APIs, relay transport, desktop enablement, and mobile session browsing/chat UI. It also adds deterministic integration coverage, database settings support, presentation updates, and implementation plans.

Changes

ACP live sessions

Layer / File(s) Summary
Shared protocol and folding
packages/session-protocol/...
Defines session state, envelopes, APIs, pagination, folding, React hooks, and reconnectable WebSocket subscriptions with tests.
Host runtime and transport
packages/host-service/..., packages/host-client/...
Spawns ACP adapters, journals session updates, handles permissions and elicitation, exposes tRPC/WebSocket APIs, and adds relay HTTP/WebSocket transport.
Desktop enablement
apps/desktop/..., packages/local-db/...
Adds the persisted ACP-session setting, security toggle, host environment flag, and migration metadata.
Mobile live-session UI
apps/mobile/...
Adds session listing, routing, chat rendering, composer controls, permission cards, timeline views, and host-client integration.
Validation and plans
packages/host-service/test/..., plans/..., apps/mobile/plans/...
Adds deterministic adapter tests, integration scenarios, ACP comparison material, and implementation plans.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Mobile as Mobile ACP UI
  participant HostClient as Host client transport
  participant HostService as Host service
  participant Manager as AcpSessionManager
  participant Adapter as claude-agent-acp
  Mobile->>HostClient: Create or load session
  HostClient->>HostService: Call acpSessions procedure
  HostService->>Manager: Create, query, or prompt session
  Manager->>Adapter: Send ACP request
  Adapter-->>Manager: Emit session updates
  Manager-->>HostService: Journal and stream envelopes
  HostService-->>HostClient: WebSocket session updates
  HostClient-->>Mobile: Fold timeline and render session
Loading

Suggested reviewers: saddlepaddle, texpobros1984

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning It has detailed notes, but it does not follow the required template and omits the Description, Related Issues, Type of Change, Testing, Screenshots, and Additional Notes sections. Rewrite the PR body using the repository template and fill in all required headings, including issue links, change type, concrete testing, screenshots if relevant, and extra notes.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: end-to-end live ACP agent sessions with host, mobile, and desktop gating.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a standalone comparison page for the mobile ACP and Claude SDK harnesses.

  • New apps/mobile/plans/acp-vs-claude-sdk.html plan document.
  • Interactive ACP/Both/SDK and Differences filters.
  • Detailed protocol waterfall, scoreboard, payload examples, and inline highlighting.

Confidence Score: 4/5

The standalone page is mergeable after a small browser-compatibility cleanup.

  • The change is isolated to a documentation HTML page.
  • The inline highlighter can stop all page interactivity in browsers without regex lookbehind support.
  • No application runtime or security issue was found in the changed file.

apps/mobile/plans/acp-vs-claude-sdk.html

Important Files Changed

Filename Overview
apps/mobile/plans/acp-vs-claude-sdk.html Adds a self-contained interactive protocol comparison page; the inline script has one browser compatibility issue.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/mobile/plans/acp-vs-claude-sdk.html:1796
**Lookbehind Breaks Page Script**

When this local page is opened in a browser that does not support regex lookbehind, the `(?<!...)` in the inline highlighter causes a parse-time script error. That stops the whole script from running, so the ACP/Both/SDK filter buttons and Differences mode do not work.

Reviews (1): Last reviewed commit: "docs(mobile): add ACP vs Claude SDK prot..." | Re-trigger Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment •

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.

All reported issues were addressed across 1 file

Architecture diagram
sequenceDiagram
    participant Browser as Browser (User)
    participant Page as acp-vs-claude-sdk.html
    participant ACP as ACP Harness
    participant SDK as Claude SDK Harness
    participant Journal as Ring Journal (in-memory)
    participant Transcript as Transcript File (disk)
    participant ChildProc as ACP Child Process

    Note over Browser,ChildProc: Comparison Page Loading & Interactive Filtering

    Browser->>Page: Open self-contained HTML
    Page->>Page: Initialize shared envelope skeleton
    Page->>Page: Generate 46-hop message waterfall

    alt ACP Lane Selected
        Page->>ACP: Load ACP-specific frames (6 kinds)
        ACP->>ChildProc: JSON-RPC over stdio
        ChildProc-->>ACP: Semantic stream chunks
        ACP->>Journal: Store frames (ephemeral)
        Note over ACP,Journal: D1: Host restart loses history
        ACP-->>Page: Render ACP hop data
        Page->>Page: Show semantic frame kinds (D2)
        Page->>Page: Client taps optionId for permissions (D3)
        Page->>Page: Host mints permission cards (D4)
        Page->>Page: Child process crash isolation (D5)
        Page->>Page: Show plain content blocks & mid-turn sends (D6)
        Page->>Page: Show stopReason → state frame only (D7)
    else SDK Lane Selected
        Page->>SDK: Load SDK-specific frames (9 kinds)
        SDK->>Transcript: Read/write persistent JSONL
        Note over SDK,Transcript: D1: Survives restarts, retry() starts new epoch
        Transcript-->>SDK: Full transcript history
        SDK-->>Page: Render SDK hop data
        Page->>Page: Show raw deltas & authoritative replace (D2)
        Page->>Page: Client constructs PermissionResult (D3)
        Page->>Page: Client parses AskUserQuestion (D4)
        Page->>Page: In-process SDK, no crash isolation (D5)
        Page->>Page: Show SDKUserMessage envelope, idle-only (D6)
        Page->>Page: Show journaled result with cost/usage (D7)
    else Both Lane Selected
        Page->>ACP: Load ACP frames
        Page->>SDK: Load SDK frames
        Page->>Page: Side-by-side comparison
        alt Differences-Only Mode
            Page->>Page: Filter to D1-D7 dimensions only
        end
        Page->>Page: Highlight ACP strength (crash isolation, simpler)
        Page->>Page: Highlight SDK strength (fidelity, durability)
    end

    Note over Page: Shared skeleton functions identically
    Page->>Page: Envelope {seq, sessionId, ts, frame}
    Page->>Page: Host-only gapless seq from 1
    Page->>Page: In-memory ring journal (cap 5,000)
    Page->>Page: WS replay (since, latest] with client dedupe
    Page->>Page: Full-snapshot state frames
    Page->>Page: Parked-promise permissions
    Page->>Page: Admission-ack sends
    Page->>Page: Pure client-side fold
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

if (str) return '<span class="' + (colon ? 'k' : 's') + '">' + str + '</span>' + (colon || '');
if (kw) return '<span class="n">' + kw + '</span>';
if (num) return '<span class="n">' + num + '</span>';
if (verb !== undefined) return nl + '<span class="m">' + verb + '</span>';

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.

P2 Lookbehind Breaks Page Script

When this local page is opened in a browser that does not support regex lookbehind, the (?<!...) in the inline highlighter causes a parse-time script error. That stops the whole script from running, so the ACP/Both/SDK filter buttons and Differences mode do not work.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/mobile/plans/acp-vs-claude-sdk.html
Line: 1796

Comment:
**Lookbehind Breaks Page Script**

When this local page is opened in a browser that does not support regex lookbehind, the `(?<!...)` in the inline highlighter causes a parse-time script error. That stops the whole script from running, so the ACP/Both/SDK filter buttons and Differences mode do not work.

How can I resolve this? If you propose a fix, please make it concise.

…e UI, desktop gate)

Everything needed to start and drive a coding-agent session on a desktop
host from the mobile app, over the existing relay:

- packages/host-service: AcpSessionManager runtime (claude-agent-acp
  adapter children over JSON-RPC/stdio), per-session journal ring buffer
  with gapless seq + resync contract, WS update stream, tRPC surface.
  Gated off by default; `list` stays ungated and answers `enabled: false`
  as a zero-extra-request capability probe.
- packages/session-protocol: shared envelope/state/fold contracts, ACP
  type re-exports, subscribeToSession client, React hooks.
- packages/host-client: transport + ACP client extracted from
  apps/mobile/lib/host (SuperJSON tRPC calls, 401 retry-once, stream URLs).
- apps/mobile: sessions list + live session thread (timeline, tool calls,
  plan view, permission cards incl. multi-select, composer), gated on the
  host capability signal.
- apps/desktop: off-by-default "live agent sessions" security setting next
  to the relay toggle (local-db migration 0042); coordinator injects
  SUPERSET_ACP_SESSIONS=1 into host children and restarts them on change;
  Anthropic env vars scrubbed at the adapter spawn site so agent children
  use the user's own login.
@bra1nDump bra1nDump changed the title docs(mobile): add ACP vs Claude SDK protocol comparison page feat(mobile): live ACP agent sessions end to end (host harness, mobile UI, desktop gate) Jul 11, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
apps/mobile/components/ai-elements/tool.tsx (1)

119-155: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Status information no longer reaches assistive technology.

The removed statusLabels/statusIcons badge conveyed textual status ("Running", "Failed", etc.). Now, running state is signaled only by an opacity pulse (imperceptible to screen readers) and error state by a bare XIcon with no accessibilityLabel. Screen-reader users can no longer distinguish running/completed/failed tool calls from this header.

🔧 Proposed fix — add an accessible status label
 			<View className="flex-1 flex-row items-center gap-2">
 				<Icon as={WrenchIcon} className="size-4 text-muted-foreground" />
 				<Animated.View className="shrink" style={titleStyle}>
 					<Text className="font-medium text-sm">{title ?? derivedName}</Text>
 				</Animated.View>
 				{/* No status chrome — only a failure leaves a subtle faded mark. */}
 				{state === "output-error" ? (
-					<Icon as={XIcon} className="size-3.5 text-destructive/70" />
+					<Icon
+						as={XIcon}
+						className="size-3.5 text-destructive/70"
+						accessibilityLabel="Tool call failed"
+					/>
 				) : null}
 			</View>

Consider also setting accessibilityLabel/accessibilityValue on the outer View (or CollapsibleTrigger) reflecting isRunning/error state so the pulse's meaning isn't lost entirely.

🤖 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 `@apps/mobile/components/ai-elements/tool.tsx` around lines 119 - 155, Add an
accessible status announcement to the tool header in the
CollapsibleTrigger/titleStyle section, preserving textual distinctions for
running, completed, and error states. Derive a status label/value from isRunning
and state (including output-error), expose it through the outer
CollapsibleTrigger or wrapping View via accessibilityLabel/accessibilityValue,
and give the error XIcon an explicit accessibility label or hide it when
redundant.
🧹 Nitpick comments (6)
plans/session-harness-claude-agent-sdk.md (1)

142-142: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add language specifiers to fenced code blocks.

Two fenced code blocks (the directory structure at line 142 and the API listing at line 246) lack language specifiers, triggering markdownlint MD040 warnings. Adding text as the language tag resolves this.

♻️ Proposed fix
-```
+```text
 packages/session-protocol/
   package.json            # exports ".", "./client"
-```
+```text
 sessions.list({ workspaceId?, cursor?, limit })      → { items: SessionScopedState[], nextCursor }

Also applies to: 246-246

🤖 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 `@plans/session-harness-claude-agent-sdk.md` at line 142, Add the text language
specifier to the two fenced code blocks in the document: the directory structure
block near the session protocol listing and the API listing block near the
sessions.list example. Change each opening fence to ```text while preserving the
existing contents.

Source: Linters/SAST tools

apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionRequestView/PermissionRequestView.tsx (1)

48-53: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Handle already_resolved to clear stale optimistic highlight.

When onRespond resolves with { status: "already_resolved" }, pendingOptionId remains set to the user's tapped option, but the actual resolution may have selected a different option. The stream will eventually deliver the correct view.resolution and self-correct, but there's a brief window where the wrong option is highlighted. Adding a .then() to reset pendingOptionId when the result is already_resolved would prevent this.

♻️ Proposed fix
 					setPendingOptionId(option.optionId);
 					onRespond(view.requestId, {
 						outcome: "selected",
 						optionId: option.optionId,
-					}).catch(() => setPendingOptionId(null));
+					})
+						.then((result) => {
+							if (result.status === "already_resolved") setPendingOptionId(null);
+						})
+						.catch(() => setPendingOptionId(null));
🤖 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
`@apps/mobile/screens/`(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionRequestView/PermissionRequestView.tsx
around lines 48 - 53, Add handling in the PermissionRequestView option onPress
callback after onRespond: when the resolved result has status
"already_resolved", clear pendingOptionId, while preserving the existing catch
reset for rejected responses. Use the onRespond invocation and
setPendingOptionId symbols to update the optimistic state immediately.
plans/host-chat-session-permisstance.md (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Filename typo: "permisstance" → "persistence".

Consider renaming the file to host-chat-session-persistence.md for discoverability and to avoid propagating the typo into future cross-references.

#!/bin/bash
# Find any references to this filename that would need updating on rename.
rg -n 'host-chat-session-permisstance' -g '*.md'
🤖 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 `@plans/host-chat-session-permisstance.md` at line 1, Rename the documentation
file from “host-chat-session-permisstance.md” to
“host-chat-session-persistence.md” and update every matching reference found by
searching for the misspelled filename, including cross-references and links.
apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionStack/PermissionStack.tsx (1)

32-33: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Front card ordering trusts array order, not requestedAt.

PendingPermission carries a requestedAt timestamp specifically for ordering, but front is derived from raw array position (visible[0]) rather than sorting by it. The docstring promises "the oldest request is the front card" — if the upstream pendingPermissions array is ever reordered (e.g., after a state snapshot replace), the wrong card would surface as "front."

🔧 Proposed defensive sort
-	const visible = pending.filter((entry) => !answeredIds.has(entry.requestId));
+	const visible = pending
+		.filter((entry) => !answeredIds.has(entry.requestId))
+		.sort((a, b) => a.requestedAt - b.requestedAt);
 	const front = visible[0];
🤖 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
`@apps/mobile/screens/`(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionStack/PermissionStack.tsx
around lines 32 - 33, Update the visible pending-permission selection in
PermissionStack so entries are defensively sorted by requestedAt in ascending
order after filtering answeredIds, then derive front from the sorted result;
preserve the existing oldest-request-first behavior even when pendingPermissions
is reordered.
apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionStack/components/PermissionCard/PermissionCard.tsx (1)

94-117: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Selection state not exposed to accessibility tree.

Multi-select options only convey "picked" via a checkmark icon and text color; add accessibilityState={{ selected: picked.has(option.optionId) }} so VoiceOver/TalkBack users get a spoken confirmation when toggling.

🤖 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
`@apps/mobile/screens/`(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionStack/components/PermissionCard/PermissionCard.tsx
around lines 94 - 117, The multi-select state in the option Pressables is not
exposed to assistive technologies. In the options map within PermissionCard, add
accessibilityState with selected set to picked.has(option.optionId) on each
Pressable so VoiceOver and TalkBack announce selection changes.
packages/host-client/src/transport/transport.ts (1)

60-60: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider URL-encoding routingKey in URL construction.

routingKey is interpolated directly into both the HTTP and WebSocket URLs without encodeURIComponent. If it ever contains characters like /, ?, or #, the URL path could be malformed or enable path traversal. This is low-risk if routingKey is always a safe identifier (e.g., UUID), but encoding it is a cheap defense-in-depth measure.

Optional fix
- const base = `${relayUrl()}/hosts/${routingKey}/trpc/${procedure}`;
+ const base = `${relayUrl()}/hosts/${encodeURIComponent(routingKey)}/trpc/${procedure}`;
- const base = `${wsBase}/hosts/${routingKey}/${path}`;
+ const base = `${wsBase}/hosts/${encodeURIComponent(routingKey)}/${path}`;

Also applies to: 114-114

🤖 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 `@packages/host-client/src/transport/transport.ts` at line 60, URL-encode
routingKey before interpolating it into the HTTP and WebSocket URLs. Update the
URL construction in the transport logic, including the base URL expression and
the corresponding WebSocket URL, to use encodeURIComponent(routingKey) so
reserved characters cannot alter the path.
🤖 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
`@apps/mobile/screens/`(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/components/ToolCallItemView/ToolCallItemView.tsx:
- Around line 1-6: The shared ToolCallItemView currently imports iOS/tvOS-only
Swift UI components without an Android fallback. Split the sheet rendering by
platform in ToolCallItemView, using the existing Swift UI implementation only
for iOS/tvOS and an Android-compatible implementation or universal Expo UI
components for Android; ensure tool rows render correctly on both platforms.

In `@plans/host-integration-test.md`:
- Line 21: Add a language identifier to the unlabeled Markdown code fence in the
architecture section, using text or plaintext for the diagram so it satisfies
markdownlint MD040.

In `@plans/session-harness-acp.md`:
- Line 184: Add an explicit language identifier to the three fenced code blocks
in plans/session-harness-acp.md: mark the architecture, package-tree, and router
examples as text (or another appropriate language) to satisfy markdownlint
MD040.
- Line 407: Update the dependency entry for
`@agentclientprotocol/claude-agent-acp` to document 0.56.0 as the installed
exact-pinned version, and identify 0.58.1 as the pending upgrade rather than the
current dependency.
- Line 336: Update the “Model selection” capability-matrix entry to state that
set_config_option returns the refreshed configOptions catalog in the RPC
response, and remove the claim that it emits a config_option_update
notification, matching the response-authoritative behavior described elsewhere.

---

Outside diff comments:
In `@apps/mobile/components/ai-elements/tool.tsx`:
- Around line 119-155: Add an accessible status announcement to the tool header
in the CollapsibleTrigger/titleStyle section, preserving textual distinctions
for running, completed, and error states. Derive a status label/value from
isRunning and state (including output-error), expose it through the outer
CollapsibleTrigger or wrapping View via accessibilityLabel/accessibilityValue,
and give the error XIcon an explicit accessibility label or hide it when
redundant.

---

Nitpick comments:
In
`@apps/mobile/screens/`(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionRequestView/PermissionRequestView.tsx:
- Around line 48-53: Add handling in the PermissionRequestView option onPress
callback after onRespond: when the resolved result has status
"already_resolved", clear pendingOptionId, while preserving the existing catch
reset for rejected responses. Use the onRespond invocation and
setPendingOptionId symbols to update the optimistic state immediately.

In
`@apps/mobile/screens/`(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionStack/components/PermissionCard/PermissionCard.tsx:
- Around line 94-117: The multi-select state in the option Pressables is not
exposed to assistive technologies. In the options map within PermissionCard, add
accessibilityState with selected set to picked.has(option.optionId) on each
Pressable so VoiceOver and TalkBack announce selection changes.

In
`@apps/mobile/screens/`(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionStack/PermissionStack.tsx:
- Around line 32-33: Update the visible pending-permission selection in
PermissionStack so entries are defensively sorted by requestedAt in ascending
order after filtering answeredIds, then derive front from the sorted result;
preserve the existing oldest-request-first behavior even when pendingPermissions
is reordered.

In `@packages/host-client/src/transport/transport.ts`:
- Line 60: URL-encode routingKey before interpolating it into the HTTP and
WebSocket URLs. Update the URL construction in the transport logic, including
the base URL expression and the corresponding WebSocket URL, to use
encodeURIComponent(routingKey) so reserved characters cannot alter the path.

In `@plans/host-chat-session-permisstance.md`:
- Line 1: Rename the documentation file from “host-chat-session-permisstance.md”
to “host-chat-session-persistence.md” and update every matching reference found
by searching for the misspelled filename, including cross-references and links.

In `@plans/session-harness-claude-agent-sdk.md`:
- Line 142: Add the text language specifier to the two fenced code blocks in the
document: the directory structure block near the session protocol listing and
the API listing block near the sessions.list example. Change each opening fence
to ```text while preserving the existing contents.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 32baac1e-b232-435c-8d8b-e19970fafe45

📥 Commits

Reviewing files that changed from the base of the PR and between a80fd85 and bf0dfe3.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (92)
  • apps/desktop/electron.vite.config.ts
  • apps/desktop/package.json
  • apps/desktop/src/lib/trpc/routers/settings/index.ts
  • apps/desktop/src/main/lib/host-service-coordinator.ts
  • apps/desktop/src/renderer/routes/_authenticated/settings/security/components/SecuritySettings/SecuritySettings.tsx
  • apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
  • apps/desktop/src/shared/constants.ts
  • apps/mobile/app/(authenticated)/workspace/[id]/chat/_layout.tsx
  • apps/mobile/app/(authenticated)/workspace/[id]/chat/acp/[sessionId].tsx
  • apps/mobile/components/ai-elements/conversation.tsx
  • apps/mobile/components/ai-elements/tool.tsx
  • apps/mobile/lib/host/client.ts
  • apps/mobile/package.json
  • apps/mobile/plans/acp-vs-claude-sdk.html
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/ChatSessionsScreen.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/[sessionId]/ChatThreadScreen.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/AcpSessionThreadScreen.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/SessionThread.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/Composer/Composer.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/Composer/components/OptionPicker/OptionPicker.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/Composer/components/OptionPicker/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/Composer/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionRequestView/PermissionRequestView.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionRequestView/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionStack/PermissionStack.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionStack/components/PermissionCard/PermissionCard.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionStack/components/PermissionCard/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionStack/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/TimelineItemView.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/components/MessageItemView/MessageItemView.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/components/MessageItemView/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/components/PlanItemView/PlanItemView.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/components/PlanItemView/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/components/ToolCallItemView/ToolCallItemView.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/components/ToolCallItemView/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/components/GlassHeaderTitle/GlassHeaderTitle.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/components/GlassHeaderTitle/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/hooks/useHostRoutingKey/index.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/hooks/useHostRoutingKey/useHostRoutingKey.ts
  • packages/host-client/package.json
  • packages/host-client/src/acp/acp.ts
  • packages/host-client/src/acp/index.ts
  • packages/host-client/src/index.ts
  • packages/host-client/src/transport/index.ts
  • packages/host-client/src/transport/transport.test.ts
  • packages/host-client/src/transport/transport.ts
  • packages/host-client/tsconfig.json
  • packages/host-service/package.json
  • packages/host-service/src/app.ts
  • packages/host-service/src/runtime/acp-sessions/acp-sessions.ts
  • packages/host-service/src/runtime/acp-sessions/index.ts
  • packages/host-service/src/runtime/acp-sessions/journal.test.ts
  • packages/host-service/src/runtime/acp-sessions/journal.ts
  • packages/host-service/src/runtime/acp-sessions/stream.test.ts
  • packages/host-service/src/runtime/acp-sessions/stream.ts
  • packages/host-service/src/trpc/router/acp-sessions/acp-sessions.ts
  • packages/host-service/src/trpc/router/acp-sessions/index.ts
  • packages/host-service/src/trpc/router/router.ts
  • packages/host-service/src/types.ts
  • packages/host-service/test/integration/acp-sessions-stream.integration.test.ts
  • packages/host-service/test/integration/acp-sessions.integration.test.ts
  • packages/local-db/drizzle/0042_add_acp_sessions_enabled_setting.sql
  • packages/local-db/drizzle/meta/0042_snapshot.json
  • packages/local-db/drizzle/meta/_journal.json
  • packages/local-db/src/schema/schema.ts
  • packages/session-protocol/package.json
  • packages/session-protocol/src/acp.ts
  • packages/session-protocol/src/api.test.ts
  • packages/session-protocol/src/api.ts
  • packages/session-protocol/src/client/index.ts
  • packages/session-protocol/src/client/subscribeToSession/index.ts
  • packages/session-protocol/src/client/subscribeToSession/subscribeToSession.test.ts
  • packages/session-protocol/src/client/subscribeToSession/subscribeToSession.ts
  • packages/session-protocol/src/envelope.ts
  • packages/session-protocol/src/fold/fold.test.ts
  • packages/session-protocol/src/fold/fold.ts
  • packages/session-protocol/src/fold/index.ts
  • packages/session-protocol/src/index.ts
  • packages/session-protocol/src/react/index.ts
  • packages/session-protocol/src/react/useAcpPermissions/index.ts
  • packages/session-protocol/src/react/useAcpPermissions/useAcpPermissions.ts
  • packages/session-protocol/src/react/useAcpSession/index.ts
  • packages/session-protocol/src/react/useAcpSession/useAcpSession.ts
  • packages/session-protocol/src/state.ts
  • packages/session-protocol/tsconfig.json
  • plans/host-chat-session-permisstance.md
  • plans/host-integration-test.md
  • plans/session-harness-acp.md
  • plans/session-harness-claude-agent-sdk.md

Comment on lines +1 to +6
import { BottomSheet, Group, Host, RNHostView } from "@expo/ui/swift-ui";
import {
background,
environment,
presentationDragIndicator,
} from "@expo/ui/swift-ui/modifiers";

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C3 '`@expo/ui/`(swift-ui|jetpack-compose)|Platform\.select|\.ios\.|\.android\.' apps/mobile --glob '*.{ts,tsx}'

Repository: superset-sh/superset

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby usage sites
sed -n '1,240p' 'apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/components/ToolCallItemView/ToolCallItemView.tsx'

printf '\n--- call sites ---\n'
rg -n -C3 'ToolCallItemView' apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId] --glob '*.{ts,tsx}'

printf '\n--- platform gating nearby ---\n'
rg -n -C3 'Platform\.OS|Platform\.select|ios|android|web' 'apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]' --glob '*.{ts,tsx}'

printf '\n--- package / dependency evidence ---\n'
rg -n '"`@expo/ui`"|swift-ui|jetpack-compose' apps/mobile/package.json apps/mobile/bun.lock package.json --glob '*.{json,jsonc,ts,tsx}'

Repository: superset-sh/superset

Length of output: 7308


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- ToolCallItemView definition ---\n'
sed -n '1,240p' 'apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/components/ToolCallItemView/ToolCallItemView.tsx'

printf '\n--- call sites ---\n'
rg -n -C2 'ToolCallItemView' 'apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]' --glob '*.{ts,tsx}'

printf '\n--- platform gating in ACP chat tree ---\n'
rg -n -C2 'Platform\.OS|Platform\.select|ios|android|native' 'apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]' --glob '*.{ts,tsx}'

printf '\n--- package evidence for Expo UI ---\n'
rg -n '"`@expo/ui`"|swift-ui|jetpack-compose' apps/mobile/package.json package.json bun.lock --glob '*.{json,jsonc,yaml,yml,ts,tsx}'

Repository: superset-sh/superset

Length of output: 37410


🌐 Web query:

Expo UI @expo/ui/swift-ui platform support iOS tvOS Android Jetpack Compose documentation

💡 Result:

Expo UI provides platform-specific and universal components to integrate native UI toolkits into React Native applications [1][2][3]. Platform-Specific Support: - @expo/ui/swift-ui: Designed for iOS and tvOS [4][3]. It enables the use of SwiftUI components within React Native by bridging them through a Host component [4]. - @expo/ui/jetpack-compose: Designed specifically for Android [5][3][6]. It allows for the use of Jetpack Compose components within React Native, also requiring a Host component for rendering [5][6]. Universal Support: - @expo/ui/universal: Provides a cross-platform API layer that enables developers to write a single component tree that runs across Android, iOS, and web [1]. This universal API automatically delegates to the appropriate native toolkit (@expo/ui/jetpack-compose on Android and @expo/ui/swift-ui on iOS) [1]. When to use each: - Use the universal components when you want a shared UI codebase that preserves a native look and feel on each platform [1]. - Use the platform-specific packages (@expo/ui/swift-ui or @expo/ui/jetpack-compose) directly when you require platform-specific controls, modifiers, or behaviors that are not covered by the universal API [1].

Citations:


Split this sheet by platform. @expo/ui/swift-ui is iOS/tvOS-only, and this shared ACP row has no Android fallback. Add an Android implementation or switch to the universal Expo UI path before enabling tool rows on Android.

🤖 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
`@apps/mobile/screens/`(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/components/ToolCallItemView/ToolCallItemView.tsx
around lines 1 - 6, The shared ToolCallItemView currently imports iOS/tvOS-only
Swift UI components without an Android fallback. Split the sheet rendering by
platform in ToolCallItemView, using the existing Swift UI implementation only
for iOS/tvOS and an Android-compatible implementation or universal Expo UI
components for Android; ensure tool rows render correctly on both platforms.

Source: MCP tools

Comment thread plans/host-integration-test.md Resolved
Comment thread plans/session-harness-acp.md Resolved
Comment thread plans/session-harness-acp.md Resolved
Comment thread plans/session-harness-acp.md Resolved

@cubic-dev-ai cubic-dev-ai Bot left a comment •

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.

3 issues found across 92 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/host-service/src/runtime/acp-sessions/acp-sessions.ts">

<violation number="1" location="packages/host-service/src/runtime/acp-sessions/acp-sessions.ts:222">
P2: After an adapter exits, the session remains in `list()` because this filter does not exclude `runtime.dead`. That contradicts the live-session-list acceptance flow (which expects the dead session to remain addressable through `get()` but disappear from `list()`), so the enabled ACP integration test fails and mobile can render dead sessions as active-list rows. Filtering dead runtimes here while retaining them in the map would preserve transcript/error access through `get()` and `getMessages()`.</violation>

<violation number="2" location="packages/host-service/src/runtime/acp-sessions/acp-sessions.ts:231">
P3: Malformed list cursors are silently interpreted as sort positions, so a truncated or corrupted cursor can make pagination look exhausted and hide all sessions instead of surfacing a bad request. Validating the complete `<createdAt>:<sessionId>` shape before using it would make cursor failures recoverable and consistent with `getMessages`.</violation>
</file>

<file name="apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/Composer/components/OptionPicker/OptionPicker.tsx">

<violation number="1" location="apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/Composer/components/OptionPicker/OptionPicker.tsx:1">
P1: ACP tool detail rows currently render `@expo/ui/swift-ui` `BottomSheet` unconditionally. That package does not support Android, so opening tool details on Android can fail instead of showing the details sheet. A platform guard with an Android-safe fallback would keep the ACP thread usable across platforms.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread apps/desktop/electron.vite.config.ts Resolved
@@ -0,0 +1,134 @@
import { BottomSheet, Group, Host, RNHostView } from "@expo/ui/swift-ui";

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: ACP tool detail rows currently render @expo/ui/swift-ui BottomSheet unconditionally. That package does not support Android, so opening tool details on Android can fail instead of showing the details sheet. A platform guard with an Android-safe fallback would keep the ACP thread usable across platforms.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/Composer/components/OptionPicker/OptionPicker.tsx, line 1:

<comment>ACP tool detail rows currently render `@expo/ui/swift-ui` `BottomSheet` unconditionally. That package does not support Android, so opening tool details on Android can fail instead of showing the details sheet. A platform guard with an Android-safe fallback would keep the ACP thread usable across platforms.</comment>

<file context>
@@ -0,0 +1,134 @@
+import { BottomSheet, Group, Host, RNHostView } from "@expo/ui/swift-ui";
+import {
+	background,
</file context>

Comment thread apps/mobile/lib/host/client.ts Resolved
Comment thread plans/host-integration-test.md Resolved
Comment thread packages/session-protocol/tsconfig.json Resolved
);
let start = 0;
if (input.cursor) {
const separator = input.cursor.indexOf(":");

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.

P3: Malformed list cursors are silently interpreted as sort positions, so a truncated or corrupted cursor can make pagination look exhausted and hide all sessions instead of surfacing a bad request. Validating the complete <createdAt>:<sessionId> shape before using it would make cursor failures recoverable and consistent with getMessages.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/runtime/acp-sessions/acp-sessions.ts, line 231:

<comment>Malformed list cursors are silently interpreted as sort positions, so a truncated or corrupted cursor can make pagination look exhausted and hide all sessions instead of surfacing a bad request. Validating the complete `<createdAt>:<sessionId>` shape before using it would make cursor failures recoverable and consistent with `getMessages`.</comment>

<file context>
@@ -0,0 +1,1069 @@
+			);
+		let start = 0;
+		if (input.cursor) {
+			const separator = input.cursor.indexOf(":");
+			const createdAt = Number(input.cursor.slice(0, separator));
+			const sessionId = input.cursor.slice(separator + 1);
</file context>

Adds a scripted fake claude-agent-acp adapter (real ACP over stdio via the
official SDK, no model/tokens/network) and wires AcpSessionManager's new
adapterEntry option into the spawn site so tests can inject it.

The new e2e suite runs in every `bun test` (~600ms) and covers: a 30-turn
marathon with gapless WS streams and getMessages pagination folds,
permission allow/deny with duplicate-answer staleness, single- and
multi-select elicitations (answers riding the _meta extension), cancel
mid-tool-call terminalization, adapter crash (dead-but-listed, readable
transcript), host restart (session_not_found reset + re-create resync),
and stale-cursor journal eviction resyncs.
ChatSessionsScreen goes back to its pre-ACP mastra-only form (byte-identical
to main) instead of serving both session kinds — the mastra list is slated
for removal once live sessions prove out, so mixing them bought complexity
for a screen that's about to die.

Live sessions get a forked AcpSessionsScreen at chat/acp/ with the create
button, status rows, pull-to-refresh, and a disabled-host explainer. Entry
is a static "Live" header link on the chat list: zero background requests
for hosts with the feature off; the capability probe (list's enabled flag)
only fires when the user navigates in.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 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
`@apps/mobile/screens/`(authenticated)/workspace/[id]/chat/acp/AcpSessionsScreen/AcpSessionsScreen.tsx:
- Around line 37-59: Add a dedicated loading state near the existing sessions
state and set it true before the initial fetch, then false in a finally block
within load, while preserving refresh behavior as appropriate. Update
ListEmptyComponent (and its loaded condition) to render a simple activity
indicator while the first load is pending, ensuring the screen is not blank
before load resolves.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad644c7d-0f2c-4078-90a8-33d4c8e3015c

📥 Commits

Reviewing files that changed from the base of the PR and between 6e4d595 and 9b7308e.

📒 Files selected for processing (4)
  • apps/mobile/app/(authenticated)/workspace/[id]/chat/_layout.tsx
  • apps/mobile/app/(authenticated)/workspace/[id]/chat/acp/index.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/AcpSessionsScreen/AcpSessionsScreen.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/AcpSessionsScreen/index.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/AcpSessionsScreen/index.ts

The fake adapter's payloads are now copied verbatim from the vendored
claude-agent-acp dist (buildAvailableModes, buildConfigOptions,
describeAlwaysAllow's allow_always/allow/reject triple,
askUserQuestionsToCreateRequest incl. the question_<n>_custom "Other"
fields), so tests exercise exactly what real runs put on the wire.

New manager-level e2e coverage: non-fatal prompt rejection + recovery,
create idempotency and workspace-mismatch conflicts, setMode /
setConfigOption round trips, cancel and adapter-death with pending
permission cards, url/empty elicitation edge modes, sequential
two-question forms with Skip, adapter-owned elicitation tool calls
(exactly one terminal update), ANTHROPIC_* spawn scrubbing, concurrent
turns not sweeping each other's open tools, dead-session graveyard
eviction, list pagination/filtering, title clearing, and subscriber
throw isolation.

New router-layer integration test via createTestHost (acpSessions
passthrough): feature gate closed-by-default with list feature-detect,
domain-error → TRPC-code mapping (NOT_FOUND/CONFLICT/BAD_REQUEST/
PRECONDITION_FAILED), and a full create → prompt → poll → fold round
trip over real tRPC.

Note: the manager's boolean setConfigOption branch stays untested at
the wire level on purpose — our initialize never declares
session.configOptions.boolean, so the real adapter can only ever send
the select fallback.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 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
`@packages/host-service/test/integration/acp-sessions.router.integration.test.ts`:
- Line 88: Add cleanup for the temporary workspace created by mkdtempSync in the
test suite’s afterAll block, alongside host.dispose(). Use rmSync with recursive
and force options on workspaceDir, ensuring the directory is removed after
integration tests complete.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 909198d2-8584-42aa-9915-c9c36654eb80

📥 Commits

Reviewing files that changed from the base of the PR and between 9b7308e and de2a0e9.

📒 Files selected for processing (5)
  • packages/host-service/src/runtime/acp-sessions/stream.ts
  • packages/host-service/test/fixtures/fake-acp-adapter.ts
  • packages/host-service/test/helpers/createTestHost.ts
  • packages/host-service/test/integration/acp-sessions.e2e.test.ts
  • packages/host-service/test/integration/acp-sessions.router.integration.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/host-service/src/runtime/acp-sessions/stream.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment •

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.

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Bot-review triage for ParadevAI#5580 — the accepted set:

- host-client: 401 retry now passes the force-refreshed token into the
  retried call instead of re-reading a possibly stale provider cache;
  transport test pins the cached→fresh header sequence.
- session-protocol: subscribeToSession structurally validates incoming
  envelopes (resync instead of crash on non-envelope JSON) and survives a
  throwing WebSocket factory during reconnect (both under test);
  list-cursor input schema rejects malformed cursors as BAD_REQUEST
  (router integration test added); PermissionView carries multiSelect;
  useAcpSession only clears rendered state on actual sessionId change and
  warns instead of swallowing loadOlder failures; tsconfig drops the
  no-op outDir.
- mobile: PermissionRequestView implements multi-select permissions
  (collect picks, Done submits via selectedOptionIds _meta) mirroring
  PermissionCard; AcpSessionsScreen shows a spinner on first load;
  TimelineItemView switch is exhaustiveness-checked; tool header and
  OptionPicker options get accessibility state/labels.
- desktop: revert the ambient-ANTHROPIC env scrub in electron.vite.config
  — spawn-boundary scrubbing in AcpSessionManager (covered by the
  env-scrub e2e test) is the right layer; the desktop-wide deletion
  changed how the rest of the host works.
- AGENTS.md: record that apps/mobile is iOS-only for the time being.
- plans: sync host-integration-test.md with the shipped host-client
  extraction, fix session-harness-acp.md's config-option-update and
  adapter-version claims, mark host-chat-session-permisstance.md as a
  hypothetical fallback (ACP shipped instead), language-tag bare fences.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/session-protocol/src/react/useAcpSession/useAcpSession.ts (1)

177-207: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Reset older-loading state when resync starts. If an in-flight loadOlder gets invalidated by a resync and that resync fails, loadingOlderRef/isLoadingOlder stay true until the next successful resync, which blocks further history loads and leaves the spinner stuck. Clear those flags at the start of resync.

🤖 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 `@packages/session-protocol/src/react/useAcpSession/useAcpSession.ts` around
lines 177 - 207, Reset the older-history loading state at the beginning of the
resync logic. In the resync function, clear loadingOlderRef.current and
setIsLoadingOlder(false) before starting the resync request so an invalidated
loadOlder cannot leave history loading blocked or the spinner stuck when resync
fails.

Source: Linters/SAST tools

🤖 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
`@apps/mobile/screens/`(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionRequestView/PermissionRequestView.tsx:
- Around line 92-100: Update the option text styling in PermissionRequestView so
selected options remain highlighted after answering. Replace the current
answered/unanswered branching that uses picked with
selectedIds.has(option.optionId) for the primary, bold style, while retaining
muted styling for answered options not in selectedIds and the default style
otherwise.

---

Outside diff comments:
In `@packages/session-protocol/src/react/useAcpSession/useAcpSession.ts`:
- Around line 177-207: Reset the older-history loading state at the beginning of
the resync logic. In the resync function, clear loadingOlderRef.current and
setIsLoadingOlder(false) before starting the resync request so an invalidated
loadOlder cannot leave history loading blocked or the spinner stuck when resync
fails.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: d2e5a07f-667a-47d5-a941-72736e803281

📥 Commits

Reviewing files that changed from the base of the PR and between de2a0e9 and 4cc7ea1.

📒 Files selected for processing (19)
  • AGENTS.md
  • apps/mobile/components/ai-elements/tool.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/AcpSessionsScreen/AcpSessionsScreen.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/Composer/components/OptionPicker/OptionPicker.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/PermissionRequestView/PermissionRequestView.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/TimelineItemView.tsx
  • packages/host-client/src/transport/transport.test.ts
  • packages/host-client/src/transport/transport.ts
  • packages/host-service/test/integration/acp-sessions.integration.test.ts
  • packages/host-service/test/integration/acp-sessions.router.integration.test.ts
  • packages/session-protocol/src/api.ts
  • packages/session-protocol/src/client/subscribeToSession/subscribeToSession.test.ts
  • packages/session-protocol/src/client/subscribeToSession/subscribeToSession.ts
  • packages/session-protocol/src/fold/fold.ts
  • packages/session-protocol/src/react/useAcpSession/useAcpSession.ts
  • packages/session-protocol/tsconfig.json
  • plans/host-chat-session-permisstance.md
  • plans/host-integration-test.md
  • plans/session-harness-acp.md
💤 Files with no reviewable changes (1)
  • packages/session-protocol/tsconfig.json
✅ Files skipped from review due to trivial changes (3)
  • plans/host-integration-test.md
  • plans/session-harness-acp.md
  • plans/host-chat-session-permisstance.md
🚧 Files skipped from review as they are similar to previous changes (11)
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/TimelineItemView/TimelineItemView.tsx
  • apps/mobile/components/ai-elements/tool.tsx
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/AcpSessionsScreen/AcpSessionsScreen.tsx
  • packages/host-service/test/integration/acp-sessions.router.integration.test.ts
  • packages/host-client/src/transport/transport.test.ts
  • packages/host-service/test/integration/acp-sessions.integration.test.ts
  • apps/mobile/screens/(authenticated)/workspace/[id]/chat/acp/[sessionId]/components/SessionThread/components/Composer/components/OptionPicker/OptionPicker.tsx
  • packages/host-client/src/transport/transport.ts
  • packages/session-protocol/src/api.ts
  • packages/session-protocol/src/client/subscribeToSession/subscribeToSession.ts
  • packages/session-protocol/src/fold/fold.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment •

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.

All reported issues were addressed across 8 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread plans/host-integration-test.md Resolved
bra1nDump and others added 2 commits July 11, 2026 10:51
…d of a security setting

The pre-release ACP session harness no longer has a user-facing toggle:
the host-service coordinator injects SUPERSET_ACP_SESSIONS=1 whenever the
build is internal (semver prerelease like 0.0.53-canary, or unpackaged dev
runs). Stable builds never mount the surface. Removes the settings
procedures, security toggle UI, search entries, and local-db migration
0042; host-side gating via the env var is unchanged.
@saddlepaddle
saddlepaddle merged commit b8ebcf4 into ParadevAI:main Jul 11, 2026
16 of 19 checks passed

This branch had an error being deployed

1 failed deployment
preview — cec18efe Deployed Jul 11, 2026 by saddlepaddle via E2E Mobile (Maestro on EAS) #6379
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants