feat(mobile): ACP chat on mobile with a Cursor-style UI, behind acp-chat

Merged
HA
1mo ago
+4366-159

Summary

  • Mobile shows and drives ACP (chat-v3) sessions that desktop opens, next to terminal sessions, behind the acp-chat flag.
  • The chat screen is redesigned after Cursor mobile: plain agent text, grey prompt pills, folded "N steps" activity, a queue docked above the composer, and native composer controls for the permission mode and Stop.
  • Fixes the red-screen crash when rendering chat markdown (react-native-worklets 0.10 renamed the Babel option that streamdown relies on).

Why / Context

Chats that desktop opens over ACP have no PTY, so mobile, which built its session lists from terminals, never showed them. Once they appeared, rendering a reply crashed: react-native-streamdown runs remend on a worklet runtime, and its README's workletizableModules option no longer exists in worklets 0.10.1. The plugin ignored it, captured remend as a remote function, and calling it there threw.

How It Works

  • Listing: useHostTerminals adds live chats (from listSessions, which now reports live) to the terminal rows, only with acp-chat on. The flag is in the query key so chats appear as soon as it loads.
  • Transcript (ChatSessionView, ChatRowView):
    • Agent replies render as markdown, with copy and branch buttons under the last one in a group.
    • Each run of thinking and tool calls folds into one grey "N steps" line that opens an Activity sheet. Failed tools say "Failed".
  • Queue: docked above the composer, one line per prompt with steer and delete. The transcript stays pinned to the bottom when it grows.
  • Header: a sessions button (the Sessions sheet gains a + to start one), and a cpu button only while background tasks or subagents run, opening a sheet with Stop per task.
  • Native composer (Swift, modules/composer):
    • ComposerModeMenu is a SwiftUI Menu/Picker beside +, shown while the composer is expanded.
    • Send turns into Stop while the agent works and the draft is empty.
    • New props: modeOptions, selectedModeId, onModeSelect, canStop, onStop. The mode change is optimistic and rolls back if the host refuses it.
  • State sharing: each chat publishes its modes, tasks and stop to its own entry in activeChatStore, keyed by session, which the header and composer read.
  • Babel: importForwarding: { moduleNames: ["remend"] } replaces workletizableModules.

Manual QA Checklist

Checked on the iOS 26.3 simulator against production with a real host:

  • Chat markdown renders with no worklets red screen
  • Live chats appear on Home and open into the chat UI; with the flag off, none appear
  • Prompt pills, plain agent text, grey inline code, copy and branch row
  • Thinking and tool runs fold into "N steps"; tapping opens the Activity sheet
  • Header sessions button opens the Sessions sheet; cpu button appears on a chat with a running background task
  • Composer: mode button beside + (expanded only), send shows as Stop on a running chat
  • Mode popover selection, Stop press, queue steer and delete, and an approval card were not exercised end to end (they act on real agents)

Testing

  • bunx tsc --noEmit -p apps/mobile (no errors in changed files; the known packages/auth better-auth type errors remain)
  • bunx biome check on the changed paths
  • bun test apps/mobile/screens/(authenticated) (68 pass), including new groupActivity/groupPositions tests
  • bun run check:i18n (all locales complete)
  • Native: xcodebuild Debug simulator build of the dev client

Known Limitations

  • The native build needs Xcode 26.4+. On Xcode 26.3 (Swift 6.2.4) Expo's expo-modules-jsi 57.1.1 does not compile. I built locally with three patches to node_modules that are not part of this PR.
  • Messages already on screen keep their old markdown colours until the chat remounts (the native markdown view caches styling).

Risks / Rollout

  • Risk: chat UI changes are gated by acp-chat. The composer's new props are optional, so terminal sessions and the Home composer are unchanged with the flag off. Starting a chat from Home now runs the session bootstrap in the background, so the Home composer frees immediately.
  • Rollout: needs a new native build (Swift composer changes) before the composer controls show. On older builds the props are ignored.
  • Rollback: turn off acp-chat, or revert the PR.

Summary by cubic

Adds ACP chat sessions to mobile, behind the acp-chat flag. Chats desktop opens over ACP have no PTY, so mobile never showed them; they now appear on Home and in session strips and open into a new Cursor-style chat screen. With the flag off, behavior is unchanged. The chat logic mobile and desktop each kept copies of now lives once in @superset/chat, with no behavior change on either app.

New chat screen

  • Agent replies render as plain markdown with copy and branch actions under the turn's last reply; prompts show as grey pills; thinking and tool runs fold into "N steps" rows that open an Activity sheet.
  • The queue docks above the composer with steer and delete; approvals render inline; the header gains a sessions button and a background-tasks button while tasks run. Terminal sessions keep their tab strip when the flag is on.
  • The native Swift composer adds a permission-mode menu beside + and turns send into Stop while the agent works. Mode changes are optimistic and roll back if the host refuses them, and switching to a terminal clears the menu.
  • chat-runtime's listSessions now reports whether each session is live, so mobile only lists chats still backed by an agent. On older hosts without that, the probe is batched ten at a time, up to fifty, so running chats behind closed ones still list.
  • Branching keeps the original chat open. Starting a chat from Home no longer holds the Home composer while the session boots; images go as attachments, and a phone-started chat that never appears says the host hasn't answered.
  • On hosts 1.37.0+, a new chat goes through agents.run with surface: "chat", so the host launches it and sends the first prompt. Older hosts ignore surface and would start a terminal, so they keep the phone-side launch, which also runs when the create timed out at the relay but may have landed.
  • Fixes the crash rendering chat markdown: react-native-worklets 0.10 dropped workletizableModules, so remend used importForwarding instead. Approval details expand past 600 characters and show the proposed edit.

Rollout

  • Needs a new native build (Swift composer changes) before the composer controls show; older builds ignore the new props.
  • The native build requires Xcode 26.4+; on Xcode 26.3 expo-modules-jsi does not compile (worked around locally with uncommitted node_modules patches).

Summary by CodeRabbit

  • New Features
    • Added mobile chat sessions alongside terminal sessions, including chat creation, resuming ended sessions, and visibility in workspace activity.
    • Added chat transcripts with message and tool activity, approval requests, queued prompts, and controls for background tasks.
    • Added composer options for selecting chat modes and stopping active work, plus support for chat attachments.
    • Added localized chat and agent interface text across supported languages.
  • Improvements
    • Chat sessions can be managed from workspace controls, with clearer activity and task status.
    • Chat messages refresh correctly when their display styling changes.