Skip to content

feat(mobile): structured native Claude chat - #18741

Merged
brennanb2025 merged 9 commits into
mainfrom
brennanb2025/mobile-claude-native-chat
Sep 6, 2026
Merged

brennanb2025 merged 9 commits into
mainfrom
brennanb2025/mobile-claude-native-chat

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​834 $\color{#cf222e}{\Huge{\mathbf{−}}}$​18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​816
Prod 16 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​636 $\color{#cf222e}{\Huge{\mathbf{−}}}$​185 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​451

What

Orca mobile now supports the restructured Claude native chat, on the same code paths Codex already uses.

Why it didn't work

The host was already Claude-ready — it defines agent-session.structured.claude.v1 and filters Claude structured tabs out of any client that doesn't advertise it (session-tab-agent-status-projection.ts:34). Mobile simply wasn't on the other side of that gate.

The structured lane in mobile/ turned out to be almost entirely agent-agnostic already: the journal reducer, message projection, options catalog and prompt tokens all work off shared types and key on tab type === 'agent-session', never on agent identity. So this opens the gate rather than building a second lane.

Gates opened

  • Advertise agent-session.structured.claude.v1.
  • resolveMobileNativeChat accepts any structured provider via the shared isAgentSessionHandleProvider, instead of a tab.agent === 'codex' literal.
  • Widen the agent-session route type off 'codex'.
  • Route bare Claude launches through agentSession.createSupport like Codex. It still degrades to a terminal whenever the host refuses — remote, WSL, win32, managed-account mismatch, or structured chat switched off.

Deduplication

Renderer and mobile each assembled the agentSession.create params by hand. The fingerprint must be computed over the same fields the host recomputes, so both now build it in one shared structuredAgentSessionCreateParams (src/shared/structured-agent-session-create.ts). Mobile's Codex-only launcher became createMobileStructuredAgentSession(client, worktreeId, agent) and reuses the existing TUI_AGENT_DISPLAY_NAMES map; two copies of a random-UUID fallback collapsed into one.

A real bug this fixes

Claude's AskUserQuestion has a grouped form — more than one question, or one multi-select question. For those the host deliberately emits the flat options: [] with no free-text id and puts the real content in body.questions (claude-structured-prompt-items.ts:114-129). Mobile read only the flat shape, so it would have rendered a card with nothing to tap and stalled the turn with no way out. Codex never emits this shape, which is why the Codex lane never hit it.

Mobile now steps through the group one question at a time (the phone has room for one) and submits the whole group once, reusing the shared encodeAgentSessionQuestionAnswers / isValidAgentSessionQuestionAnswers rather than inventing a second encoding.

I ablated the fix to confirm the test goes red without it — it fails on the exact "2 grouped questions from Claude" placeholder.

Also

  • Prompt responses moved into useMobileStructuredPromptResponses. Grouped questions carry a multi-step draft the rest of the session doesn't touch, and the session hook was at the 300-line cap — split along that seam rather than bumping max-lines.
  • Pinned the mobile capability list against the host's parser bounds. parseRuntimeClientCapabilities fails closed to no capabilities at all past 64 entries, which would look exactly like an old client; mobile advertises 11.

Verification

  • Mobile tsc clean; 4095 mobile tests pass (505 files, 8 new).
  • Node + web typechecks clean.
  • Both of CI's oxlint configs pass. The two remaining no-cycle warnings in mobile/src/transport/ are pre-existing — proven by reverting my files and re-running.
  • Renderer structured-launch tests confirm the shared refactor is behaviour-preserving.
  • Rebased onto current main (0a821e5bc8e) and re-ran everything; no file overlap with the five commits that landed since.

mobile-session-route-parity.test.ts is re-pinned: the create-actions edit drops one runtime string literal and changes one nested function body. I ablated to confirm that file is the sole cause before re-pinning.

Known gaps, deliberately left

  • experimentalStructuredNativeChat must be enabled in desktop settings for mobile to see any structured tab. Pre-existing mobile-only gate that applies to Codex identically.
  • The Claude managed-account refusal reports reason: 'wsl' when the active Claude account is WSL-bound. Mobile degrades correctly, but the reason string is misleading. Not introduced here.
  • agentSession.requestHandoff is absent from the mobile method allowlist, so mobile can't initiate native↔TUI handoff. Pre-existing for Codex too.

@coderabbitai

coderabbitai Bot commented Sep 4, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 766267aa-eaae-4a05-bed0-7cff2457806d

📥 Commits

Reviewing files that changed from the base of the PR and between c5830e5c51dd963c0f4988d104bf75dc7cc60f78 and 9d5afd9bf2644363369d0b30e4d6233965f0a9cd.

📒 Files selected for processing (2)
  • mobile/src/session/MobileNativeChatQuestion.test.tsx
  • mobile/src/session/MobileNativeChatQuestion.tsx

Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The mobile structured agent-session flow now supports recognized providers such as Claude instead of only Codex. Shared helpers build provider-specific session identifiers and replayable creation envelopes. Mobile capability advertisements include Claude support. Grouped structured questions now project one question at a time, preserve draft answers, validate responses, and submit encoded answer groups. Tests cover provider handling, capability limits, session launch behavior, and grouped-question flows.

Merge Risk: ⚪ Minimal · up to 9d5af

Mobile Claude structured chat now supports provider-aware sessions and grouped question responses. Duplicate-label selections preserve row identity, including tokenless options, with no remaining actionable merge risk.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides detailed change rationale and verification, but it omits required template sections and information, including a linked issue, visual proof or an explicit N/A explanation, the… Update the description to follow the repository template. Add the required linked issue, provide before/after visual proof or write N/A with a reason, complete the Testing section and checkboxes, address AI Disclosure, Review, Agent skill u…
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 46 functions across 27 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: adding structured native Claude chat support to mobile.
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.
Full details: Description check

Explanation

The description provides detailed change rationale and verification, but it omits required template sections and information, including a linked issue, visual proof or an explicit N/A explanation, the testing checklist, AI disclosure, review section, agent skill boundary checklist, and final checklist.

Resolution

Update the description to follow the repository template. Add the required linked issue, provide before/after visual proof or write N/A with a reason, complete the Testing section and checkboxes, address AI Disclosure, Review, Agent skill upstream boundary, Notes, and Checklist items, and retain the existing technical details.

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: f8231cd0-9312-4352-8b75-27227b697763

📥 Commits

Reviewing files that changed from the base of the PR and between 0a821e5 and 8dc782d.

📒 Files selected for processing (19)
  • mobile/src/session/mobile-native-chat-eligibility.test.ts
  • mobile/src/session/mobile-native-chat-eligibility.ts
  • mobile/src/session/mobile-session-route-parity.test.ts
  • mobile/src/session/mobile-session-route-types.ts
  • mobile/src/session/mobile-structured-agent-prompts-grouped.test.ts
  • mobile/src/session/mobile-structured-agent-prompts.ts
  • mobile/src/session/mobile-structured-agent-session-launch.test.ts
  • mobile/src/session/mobile-structured-agent-session-launch.ts
  • mobile/src/session/mobile-structured-agent-session-rpc.ts
  • mobile/src/session/mobile-structured-grouped-question.test.ts
  • mobile/src/session/mobile-structured-grouped-question.ts
  • mobile/src/session/use-mobile-session-terminal-create-actions.ts
  • mobile/src/session/use-mobile-structured-agent-session.ts
  • mobile/src/session/use-mobile-structured-prompt-responses.ts
  • mobile/src/transport/mobile-runtime-client-capabilities.test.ts
  • mobile/src/transport/mobile-runtime-client-capabilities.ts
  • mobile/src/transport/rpc-client-capabilities.test.ts
  • src/renderer/src/lib/launch-structured-agent-session.ts
  • src/shared/structured-agent-session-create.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

  • Capability gate — MOBILE_RUNTIME_CLIENT_CAPABILITIES advertises agent-session.structured.claude.v1, with a new test pinning the list against the host's fail-closed parser bounds (≤64 entries, ≤128 chars, no duplicates).
  • Eligibility + route type — resolveMobileNativeChat resolves any isAgentSessionHandleProvider agent for agent-session tabs, and MobileSessionTab widens agent: 'codex' → AgentSessionHandleProvider.
  • Agent-agnostic launch — createMobileStructuredCodexSession becomes createMobileStructuredAgentSession(client, worktreeId, agent), routing bare Claude launches through agentSession.createSupport and naming the refusing agent in failure copy.
  • Shared create envelope — new structured-agent-session-create.ts dedupes the renderer/mobile agentSession.create envelope build. I verified it is byte-behavior-preserving: structuredAgentSessionCreateFingerprint delegates to structuredAgentSessionPayloadFingerprint({ method: 'agentSession.create', sessionId, fields: { worktree, agent } }), matching both old inline sites.
  • Grouped Claude AskUserQuestion — projectStructuredQuestion routes body.questions through the new mobile-structured-grouped-question.ts, stepping one question at a time and submitting the whole group via the shared encodeAgentSessionQuestionAnswers. This fixes a genuine stall (empty flat options: [] card) and reuses the host-accepted encoding/validation.
  • State split — prompt responses + grouped draft move to useMobileStructuredPromptResponses.

I traced the grouped-answer wire path end to end: option ids are the journal encodeClaudeQuestionOptionId('qN','choice-M') ids, questionId is the qN address, and free text keys on freeTextQuestionId; the host's performPrompt/groupedQuestionResponse accept this shape via isValidAgentSessionQuestionAnswers. The token encoding is collision-safe (percent-encoded JSON keeps the : separator and ', ' multi-select join unambiguous), and single-question Claude prompts route correctly through the grouped branch since the host always sets body.questions.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes (since prior pullfrog review @ 8dc782d)

  • Derived grouped-question draft — useMobileStructuredPromptResponses now stores { sessionKey, draft } and derives groupedDraft on read, dropping the useEffect/useLayoutEffect/useRef machinery that cleared the draft on session switch.

I verified the refactor is behavior-preserving and correct: respondQuestion reads the derived groupedDraft and lists it (plus sessionKey) in its dependency array, so the stale-closure problem the old groupedDraftRef handled is now solved by React's own memoization. The sessionKey check on read ensures a draft collected in another session is never projected into the current one, and the per-prompt promptKey (itemId:revision) check still discards drafts against a superseded revision. A revised prompt or a resolved question still resets to step one, and a pending group still starts from the first unanswered question. No functional regression.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 8d93ff77-b129-4331-a262-23fc99e81eea

📥 Commits

Reviewing files that changed from the base of the PR and between 8dc782d and e8769ce.

📒 Files selected for processing (2)
  • mobile/src/session/mobile-structured-grouped-question.test.ts
  • mobile/src/session/use-mobile-structured-prompt-responses.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.

@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.

Caution

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

⚠️ Outside diff range comments (1)
mobile/src/session/MobileNativeChatQuestion.tsx (1)

56-56: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the selected option index in the single-select fallback.

Line 56 calls formatQuestionAnswer(question, [option]) when the selected option has no token. That formatter uses options.indexOf(label). If duplicate labels exist and only the first duplicate has a token, selecting a later plain-bullet row submits the first row token.

Use the index-based formatter for this path. Add a test with duplicate labels and optionTokens: ['first-token', null].

Proposed fix
-    const token = question.optionTokens[optionIndex]
-    await sendAnswer(token && token.length > 0 ? token : formatQuestionAnswer(question, [option]))
+    await sendAnswer(formatQuestionAnswerByIndexes(question, [optionIndex]))

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: af72e16e-eb2e-446d-9be3-3bdee0f3d864

📥 Commits

Reviewing files that changed from the base of the PR and between e8769ce and df628c2.

📒 Files selected for processing (10)
  • mobile/src/session/MobileNativeChatQuestion.test.tsx
  • mobile/src/session/MobileNativeChatQuestion.tsx
  • mobile/src/session/mobile-native-chat-question.ts
  • mobile/src/session/mobile-structured-agent-prompts-grouped.test.ts
  • mobile/src/session/mobile-structured-agent-prompts.ts
  • mobile/src/session/mobile-structured-agent-session-launch.test.ts
  • mobile/src/session/mobile-structured-agent-session-launch.ts
  • mobile/src/session/mobile-structured-grouped-question.test.ts
  • mobile/src/session/mobile-structured-grouped-question.ts
  • mobile/src/session/use-mobile-structured-prompt-responses.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes (since prior pullfrog review @ 138b298de)

  • Position-based option selection — MobileNativeChatQuestion now tracks selected option indexes instead of labels, so a repeated label (Claude can ask the same question twice in one group) submits the correct row by position. New formatQuestionAnswerByIndexes / formatQuestionAnswerWithOtherByIndexes helpers mirror this, with tests covering duplicate-label and multi-select-plus-free-text submissions.
  • Hardened grouped-answer parsing — answerFromResponse now validates every comma-joined part's promptKey (newly added to each token payload) and questionId, checks option ids against the offered set, enforces single-vs-multi-select cardinality, and accepts option-plus-free-text answers it previously dropped. decodeGroupedFreeTextAnswer also guards decodeURIComponent in a try/catch.
  • Option descriptions surfaced — flat and grouped question projections both pass per-option optionDescriptions through to the card.
  • Transient createSupport retry — the launch path retries agentSession.createSupport on selector_not_found with [50, 150, 300]ms backoff, matching the renderer's hostSupportsCreate, so a worktree that is only transiently unresolvable right after creation is no longer mistaken for a refusal.
  • Test pinning — grouped steps assert distinct card keys when the questions read identically, plus new launch-retry and grouped-answer validation tests.

I verified the retry loop terminates (bounded to four createSupport attempts), handles both the thrown-error and ok:false response shapes via hasRuntimeRpcErrorCode, and mirrors the renderer's delays and selector_not_found predicate exactly. The token payload change is client-internal: tokens are decoded locally and re-encoded through the shared encodeAgentSessionQuestionAnswers, so nothing the host reads changed.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: clean

Reviewed and pushed two focused fixes to this branch. PR head is now df628c20973, and fresh PR CI is fully green.

Fixes added

  • Hardened grouped Claude question answers by binding every token to the exact prompt item/revision, rejecting malformed or mixed foreign multi-select payloads atomically, preserving duplicate-label row identity, retaining multi-select + Other state, and rendering structured option descriptions.
  • Added a bounded retry for transient selector_not_found support probes (50/150/300 ms). Permanent and non-selector failures still take the existing legacy fallback immediately.
  • Preserved the upstream draft-state and grouped-session-key commits already on the branch.

Review and verification

  • Iterative adversarial review finished with no in-scope issues remaining.
  • Opening and closing readiness audits found zero candidate P0/P1/P2 findings.
  • Local focused verification: 26 mobile tests, mobile TypeScript, changed-code quality, type-aware analysis, React Doctor, formatting, and diff checks all passed.
  • Closing readiness suite: 70 mobile tests passed.
  • Fresh GitHub checks passed: Mobile verify, static analysis, typecheck, package + Windows package, all 8 test shards, aggregate verify, and Pullfrog.

Visual proof

Validated on an iPhone 17 Pro simulator against an isolated Electron dev host: mobile created a native structured Claude session, rendered a two-step grouped AskUserQuestion with descriptions, advanced from single-select to multi-select + Other, submitted, dismissed the question card, and returned to the answered chat state.

Native Claude session

Grouped question step 1

Grouped question step 2

Answered chat state

Electron backing session

Remaining QA gaps

  • Duplicate-label selection is regression-tested but was not produced by the live Claude prompt on device.
  • The Other text field was visibly rendered and typed into, but Claude's final recap did not prove the custom text landed end-to-end.
  • Android was not exercised.
  • Screenshots were captured at a77f4941ed0; the final df628c20973 commit only adds the non-visual bounded support-probe retry.

No merge-blocking issues remain from this review.

@brennanb2025
brennanb2025 force-pushed the brennanb2025/mobile-claude-native-chat branch 2 times, most recently from 1a9fd82 to 4aaed2e Compare September 5, 2026 06:52

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes (since prior pullfrog review @ df628c2)

  • Compact grouped-answer encoding — encodeAgentSessionQuestionAnswers now escapes only % (replaceAll('%', '%25')) instead of fully percent-encoding the JSON, keeping Claude's maximum 4×4 choice group within the host's 512-char bound while the shared decodeURIComponent-based decoder still reads both representations.
  • Host option-id bound widened to 1024 — RespondParams.optionId uses a new maxLength argument on Identifier, so a legacy fully-encoded 610-char payload from an already-deployed desktop renderer is still admitted.
  • Grouped-draft clear race fixed — setCollected clears the draft only when the completing submission's sessionKey and promptKey still match the collected draft, so an in-flight submission resolving after a session switch or revision bump no longer wipes the newer draft.
  • New coverage — encoder round-trip/legacy-compat/Unicode/size-bound tests, a schema-boundary test admitting the 610-char legacy optionId and rejecting 1025, and a react-test-renderer hook test covering both race scenarios.

I traced the wire-compat both directions and ran the touched suites (encoder, schema, hook, plus the mobile/host grouped-question tests) — all green. The %-only escape is a bijection (decodeURIComponent never throws, and literal %, %xx, and Unicode round-trip), so mixed-version clients stay compatible: a new client's compact payload fits an old host's 512 cap, and a new host's 1024 cap admits an old client's 610-char payload.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes (since prior pullfrog review @ c5830e5)

  • Tokenless single-select now submits by position — MobileNativeChatQuestion.answerSingle was narrowed to (optionIndex) and its fallback switched from formatQuestionAnswer(question, [option]) (label indexOf) to formatQuestionAnswerByIndexes(question, [optionIndex]). Selecting the second of two identically-labelled options when only the first carries a token now submits the correct row instead of the first duplicate's token. This closes the open coderabbit thread from the prior round.

The new submits a tokenless duplicate-label row by position test is exact and correctly ablate-red: with the old code it would submit 'first-token' (via options.indexOf('Choice') → 0) rather than the expected 'Choice'. The token-preserving branch is unchanged, and the remaining fallback path is equivalent to the correct by-index formatting, so the fix is behavior-preserving elsewhere.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Exact-head mobile QA evidence (9d5afd9bf26)

Validated at pushed commit 9d5afd9bf2644363369d0b30e4d6233965f0a9cd on an iPhone 17 Pro simulator against an isolated Electron dev host (qa-18741-9d5). Metro on 8081. Interaction was orca emulator AX/taps/type only.

  • Mobile advertised structured + Claude capabilities; native Claude Chat rendered (not “Open on desktop”).
  • Grouped AskUserQuestion stepped 1 of 2 → 2 of 2, then submitted to the answered chat state (Database: Postgres, Regions: us-east + custom Apac).
  • The structured Claude Chat tab remained after a later desktop TUI Claude tab projected in (mobile then showed 2 tabs).

Grouped question 1 of 2

Answered grouped chat

Isolated Electron backing session

Desktop New tab → Claude opened a TUI Claude Code pane rather than auto-hydrating the mobile-created structured tab. Android not exercised.

@brennanb2025

brennanb2025 commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Review status: clean at 9d5afd9bf26; latest-main rebase required

This supersedes the earlier review status in #18741 (comment).

Review fixes now on the branch

  • Hardened grouped Claude answers against stale session/prompt completions, mixed or malformed grouped payloads, overlong cross-version encoding, and duplicate-label row ambiguity, including the tokenless later-row case.
  • Added a bounded selector_not_found retry for transient structured-create support probes; permanent and unrelated failures keep the existing immediate fallback behavior.
  • Preserved the upstream grouped-draft session-key correction and its regression coverage.

Review and verification

  • Same-model adversarial review/fix/re-review loop finished clean with no remaining in-scope correctness, regression, or performance findings.
  • Opening and closing readiness audits found zero candidate P0/P1/P2 issues, including mobile wire/backcompat, SSH/relay/folder boundaries, Android/iOS shared paths, Windows/Linux compatibility, retry/resource bounds, and security/supply-chain review.
  • Focused closing checks passed: 73 mobile tests, 108 root runtime/shared/renderer tests, root and mobile typechecks, changed-code quality, and diff checks.
  • All GitHub checks for 9d5afd9 are green: Mobile verify, static analysis, typecheck, cross-version wire compatibility, macOS/Windows packaging, all eight test shards, aggregate verify, Pullfrog, and CodeRabbit with no actionable findings.

Exact-head visual proof

Validated 9d5afd9 on an iPhone 17 Pro simulator against an isolated Electron host using orca emulator for mobile interaction and Playwright CDP for Electron:

  • runtime.clientCapabilities.update succeeded, proven by native Claude Chat projection instead of an unsupported placeholder.
  • A grouped AskUserQuestion advanced through both steps and submitted to the answered state, including multi-select plus custom Other text.
  • The structured Claude tab survived projection after a desktop Claude TUI tab appeared.

Evidence: full exact-head QA comment · grouped question · answered chat · Electron host

Current merge status and residual gaps

GitHub now reports the PR as conflicting with the latest main. A fresh merge-tree check found one conflict in mobile/src/session/mobile-structured-agent-session-launch.ts: current main adds the definitive structured-create refusal classification from #18697, while this PR generalizes the same launch path and adds the bounded support-probe retry. The next rebase should preserve both behaviors, then rerun the focused launch tests and CI. I did not rebase or force-push over the coordinator-owned branch history.

Live QA did not exercise Android or produce duplicate-label options; those paths are covered by shared-code review and regression tests. Electron did not auto-hydrate the mobile-created structured tab into its tab bar, while mobile retained the structured tab and the host persisted the session.

Verdict: the reviewed implementation at 9d5afd9 is clean, but the PR is not merge-ready until the latest-main conflict is reconciled and the rebased head is revalidated.

@brennanb2025
brennanb2025 force-pushed the brennanb2025/mobile-claude-native-chat branch from 9d5afd9 to b987f42 Compare September 5, 2026 08:38

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes (since prior pullfrog review @ 9d5afd9)

  • Launch tests retargeted to the generalized create API — mobile-structured-agent-session-launch.test.ts now calls createMobileStructuredAgentSession(client, worktreeId, agent), matching the rename from earlier commits. The rebase onto #18697 left these tests calling the pre-rename createMobileStructuredCodexSession, and mobile tsc excludes test files, so the stale reference wasn't caught until the tests ran.
  • Refusal-code correction — the "names the refusing agent" test now uses method_not_found (a definitive refusal per the shared allowlist) instead of agent_session_refused, which isDefinitiveAgentSessionCreateRefusal correctly leaves unknown, so it never reached the failure copy the test asserted.

No production code changed in this delta; the isDefinitiveAgentSessionCreateRefusal classification on the launch path is the rebased merge of #18697 (already reviewed on main). The it.each tables already pin the definitive codes (structured_agent_session_unsupported, method_not_found) against the unknown codes (agent_session_operation_unknown, agent_session_ownership_unknown, runtime_error, future codes) exactly as the shared allowlist defines them. Verified the retargeted suite passes (19 tests).

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Mobile emulator QA — PASS at b987f42a955 (post-rebase, post-conflict-resolution)

Re-ran end to end at the current head. The earlier QA on this PR was at 9d5afd9bf26, before the rebase onto #18697 and my conflict resolution in mobile-structured-agent-session-launch.ts — which is exactly the create path Create Tab → Claude exercises, so it needed re-proving.

Rig: isolated worktree at b987f42a955; own iOS Simulator (iPhone 17 Pro, dedicated UDID, created and destroyed by this run); own Metro on :8093; isolated Electron host with its own ORCA_DEV_USER_DATA_PATH, CDP port owner verified. Local-only pairing. All mobile interaction through orca emulator AX/taps/type.

Proof

  1. Capability round trip works. The tab renders as native "Claude Chat" — not Open on desktop (fix(native-chat): tell old mobile builds why a structured chat is missing #18756's placeholder for a client lacking agent-session.structured.claude.v1) and not Update to view. That title is only reachable when the host actually recorded both structured capabilities on the mobile socket.

  2. Host recorded a mobile-created Claude session. agent-sessions.json in the isolated profile holds one record claude_ccad65e3… — note the claude_ prefix from the shared createStructuredAgentSessionId — with provider: claude, lease.claimStatus: live, runtimeFence: 1.

  3. Grouped AskUserQuestion is answerable. Claude issued one AskUserQuestion with two questions. Mobile stepped it:

    • Database: Which database? (1 of 2) — single-select, options carrying descriptions, free-text row present
    • Regions: Which regions? (2 of 2) — multi-select with Submit selected options
    • Group submitted once; Claude replied "Both answers are in: Database: Postgres / Regions: us-east, eu-west"

    On main this prompt arrives with options: [] and would render an unanswerable card, stalling the turn.

Claude Chat tab Grouped Q 1 of 2 Q 2 of 2 multi-select Group answered
tab q1 q2 answered

Desktop Electron QA — BLOCKED by a pre-existing main defect, not by this PR

The Electron renderer never mounts in dev mode: #root stays empty and the window is blank.

ReferenceError: process is not defined
  at src/shared/process-table-snapshot.ts:2:24

process-table-snapshot.ts evaluates process.platform at module top level, and it is now pulled into the renderer bundle via src/renderer/src/components/terminal-pane/agent-completion-poll-interval.ts (#18742). process is undefined in the browser, so the module throws during evaluation and React never mounts.

Not from this PR — proven, not inferred: both files are byte-identical to main here, neither is in this PR's diff, and I checked the same rig out at plain origin/main (12e05203a4c) and reproduced the identical crash, same file and line. Filing separately; flagging here because it blocks pnpm dev desktop QA generally.

Gaps

  • Android not exercised (iOS Simulator only).
  • Duplicate-label grouped options not produced by this live prompt (covered by unit test instead).
  • Desktop-side rendering of the mobile-created structured tab could not be checked, for the reason above.

Merge Sim added 9 commits September 6, 2026 10:31
Mobile already spoke the structured agent-session protocol for Codex, and the
host already had a Claude capability gate — mobile just never advertised it, so
`projectAgentSessionTabsOut` stripped every Claude tab before it left the
desktop. The structured lane in mobile/ turned out to be agent-agnostic already
(shared reducer, message projection, option catalog, prompt tokens), so this
opens the gate rather than building a second lane:

- advertise `agent-session.structured.claude.v1`
- resolve any structured provider in `resolveMobileNativeChat` via the shared
  `isAgentSessionHandleProvider`, instead of a `'codex'` literal
- widen the `agent-session` route type off `'codex'`
- route bare Claude launches through `agentSession.createSupport` like Codex,
  which still degrades to a terminal when the host refuses (remote, WSL, win32,
  managed-account mismatch, or structured chat switched off)

Deduplicate the create envelope. Renderer and mobile each assembled the
`agentSession.create` params by hand; the fingerprint has to be computed over
the same fields the host recomputes, so both now build it in one shared
`structuredAgentSessionCreateParams`. Mobile's Codex-only launcher becomes
`createMobileStructuredAgentSession(client, worktreeId, agent)` and reuses the
shared display-name map; two copies of a random-UUID fallback collapse into one.

Answer grouped Claude questions. A Claude AskUserQuestion carrying more than one
question — or one multi-select question — is emitted with the real content in
`body.questions` and the flat `options` left EMPTY, so mobile rendered a card
with nothing to tap and the turn stalled with no way out. Codex never emits this
shape. The phone has room for one question at a time, so the group is answered
as steps and submitted once, reusing the shared
`encodeAgentSessionQuestionAnswers` / `isValidAgentSessionQuestionAnswers`
rather than a second encoding.

Prompt responses move into `useMobileStructuredPromptResponses` because grouped
questions carry a multi-step draft the rest of the session does not touch, and
the session hook was at the 300-line cap.

Pin the mobile capability list against the host's parser bounds: it fails closed
to NO capabilities when the array exceeds 64 entries, which would look exactly
like an old client.

Re-pin mobile-session-route-parity: the create-actions edit drops one runtime
string literal and changes one nested function body. Ablated to confirm that
file is the sole cause.
… in an effect

The React Doctor gate flagged the session-change reset as a state adjustment
after a prop change, which renders the stale draft for a frame. Store the
session the answers were collected in alongside them and check it on read, so a
session switch drops the draft during render with no effect at all.
…d identically

Claude can ask the same text twice in one group (once per file, say). The view
keys the question card by its projected content, so identical wording must still
key apart or step 1's checkboxes would be submitted as step 2's answer.
The rebase onto #18697 brought its definitive-refusal tests in cleanly, but they
call the pre-rename createMobileStructuredCodexSession, and mobile tsc excludes
test files so nothing caught it. Retarget them and give the agent-copy test a
code that is actually in the definitive allowlist - agent_session_refused now
correctly stays unknown, so it never reached the failure copy it asserted.
Main moved its own runtime-string pin to 547; this branch drops the 'codex'
literal from the create-actions gate. Ablated against main's pins to confirm
that file is the sole cause before re-deriving.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/mobile-claude-native-chat branch from b987f42 to b51aaf1 Compare September 6, 2026 17:34

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes (since prior pullfrog review @ b987f42)

  • Rebase onto main — all 8 prior commits were re-applied on current main (da48ad2), gaining fix(mobile):/test(mobile): subject prefixes with no production content change; no file overlap with the commits that landed on main since.
  • Route-parity test re-pin — mobile-session-route-parity.test.ts re-pinned: main had moved its own runtime-string pin to 547, and this branch's 'codex' literal drop (from the create-actions gate) brings the count to 546, with HEAD_NESTED_FUNCTION_SHA256 and HEAD_RUNTIME_STRING_SHA256 re-derived.

The sole new commit is test-only and correctly scoped: the parity test is a snapshot over extracted route strings, and the count/hash drift is fully explained by main's own bump plus the previously-reviewed agent === 'codex' → isAgentSessionHandleProvider(agent) gate change dropping the 'codex' literal. No production behavior changed, and the ablation described in the commit message is consistent with the three touched lines.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 merged commit d07c475 into main Sep 6, 2026
29 checks passed
Jinwoo-H added a commit that referenced this pull request Sep 6, 2026
Merges #18741, which opens the structured agent-session lane to Claude on
mobile. Three conflicts, all in mobile/src/session:

- mobile-native-chat-eligibility.ts: import block only. Kept the branch's
  MobileWebNativeChatAgentStatus/AgentWorkingMode pair and added main's
  isAgentSessionHandleProvider. Main's AgentStatusEntry import is dropped
  because the branch no longer references it. Main's generalized
  agent-session resolution auto-merged unchanged.
- use-mobile-session-terminal-create-actions.ts: kept the branch's hosted
  page-adapter prelude, then main's generalized bare-launch gate whole
  (isAgentSessionHandleProvider + createMobileStructuredAgentSession).
- mobile-session-route-parity.test.ts: took the branch's pins and
  recomputed from the test's own printed values. Ablated first:
  use-mobile-session-terminal-create-actions.ts is the only changed file
  in MOBILE_SESSION_ROUTE_SOURCE_FILES, matching main's own ablation.
  Runtime strings 476 -> 475 (the dropped 'codex' literal) and the nested
  function body re-froze; JSX, style, identity, navigation and capability
  digests were all unaffected.

The hybrid page path carries no agent-session surface at all, for Codex or
Claude, so nothing in src/shared/mobile-web or mobile/src/mobile-web needed
a wire change. Left as an open parity item.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Correction to my QA comment above

Two claims in that comment turned out to be wrong. Correcting the record rather than editing, so what I asserted and what proved true both stay visible.

1. The process is not defined renderer crash — nothing to file. I said I was filing it separately. It was already fixed on main by #18814 ("stop a Node-only process-table module blanking the app at boot") before I wrote that. There is no open issue and none is needed.

2. "Desktop New Tab → Claude produced a TUI pane" was not a product defect — retract it. That was my test fixture. The QA repo was a bare git init with a single README, no package.json, so it resolved no project runtime. agent-launch-routing.ts refuses the structured route when projectRuntime?.status === 'repair-required', which sends the launch to legacy-native-chat — a TUI terminal. Real repos resolve a runtime and take the structured route; confirmed working on a normal workspace.

That also explains the part I found puzzling at the time: the host answered agentSession.createSupport: {supported: true} while the renderer still fell back. Two independent gates — the host was willing, and the client refused on runtime status before ever asking it.

Still accurate from that comment: the capability round trip, the host recording provider: claude with a live lease, and the grouped AskUserQuestion stepping and answering end to end. The desktop→mobile direction was separately verified afterwards with a Codex control, and a separate review confirmed the grouped-answer encoding needs no capability gate — only Claude emits body.questions, and the producer and decoder both shipped in #18560, so no version skew is reachable.

Genuinely open: Android was never exercised.

enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
* feat(mobile): structured native Claude chat

Mobile already spoke the structured agent-session protocol for Codex, and the
host already had a Claude capability gate — mobile just never advertised it, so
`projectAgentSessionTabsOut` stripped every Claude tab before it left the
desktop. The structured lane in mobile/ turned out to be agent-agnostic already
(shared reducer, message projection, option catalog, prompt tokens), so this
opens the gate rather than building a second lane:

- advertise `agent-session.structured.claude.v1`
- resolve any structured provider in `resolveMobileNativeChat` via the shared
  `isAgentSessionHandleProvider`, instead of a `'codex'` literal
- widen the `agent-session` route type off `'codex'`
- route bare Claude launches through `agentSession.createSupport` like Codex,
  which still degrades to a terminal when the host refuses (remote, WSL, win32,
  managed-account mismatch, or structured chat switched off)

Deduplicate the create envelope. Renderer and mobile each assembled the
`agentSession.create` params by hand; the fingerprint has to be computed over
the same fields the host recomputes, so both now build it in one shared
`structuredAgentSessionCreateParams`. Mobile's Codex-only launcher becomes
`createMobileStructuredAgentSession(client, worktreeId, agent)` and reuses the
shared display-name map; two copies of a random-UUID fallback collapse into one.

Answer grouped Claude questions. A Claude AskUserQuestion carrying more than one
question — or one multi-select question — is emitted with the real content in
`body.questions` and the flat `options` left EMPTY, so mobile rendered a card
with nothing to tap and the turn stalled with no way out. Codex never emits this
shape. The phone has room for one question at a time, so the group is answered
as steps and submitted once, reusing the shared
`encodeAgentSessionQuestionAnswers` / `isValidAgentSessionQuestionAnswers`
rather than a second encoding.

Prompt responses move into `useMobileStructuredPromptResponses` because grouped
questions carry a multi-step draft the rest of the session does not touch, and
the session hook was at the 300-line cap.

Pin the mobile capability list against the host's parser bounds: it fails closed
to NO capabilities when the array exceeds 64 entries, which would look exactly
like an old client.

Re-pin mobile-session-route-parity: the create-actions edit drops one runtime
string literal and changes one nested function body. Ablated to confirm that
file is the sole cause.

* fix(mobile): derive the grouped-question draft instead of clearing it in an effect

The React Doctor gate flagged the session-change reset as a state adjustment
after a prop change, which renders the stale draft for a frame. Store the
session the answers were collected in alongside them and check it on read, so a
session switch drops the draft during render with no effect at all.

* test(mobile): pin that grouped steps key apart when the questions read identically

Claude can ask the same text twice in one group (once per file, say). The view
keys the question card by its projected content, so identical wording must still
key apart or step 1's checkboxes would be submitted as step 2's answer.

* fix(mobile): harden grouped Claude question answers

* fix(mobile): retry transient structured support probes

* fix(mobile): preserve grouped prompt response compatibility

* fix(mobile): preserve tokenless duplicate choice identity

* fix(mobile): point the launch tests at the generalized create API

The rebase onto stablyai#18697 brought its definitive-refusal tests in cleanly, but they
call the pre-rename createMobileStructuredCodexSession, and mobile tsc excludes
test files so nothing caught it. Retarget them and give the agent-copy test a
code that is actually in the definitive allowlist - agent_session_refused now
correctly stays unknown, so it never reached the failure copy it asserted.

* test(mobile): re-pin route parity after the rebase onto main

Main moved its own runtime-string pin to 547; this branch drops the 'codex'
literal from the create-actions gate. Ablated against main's pins to confirm
that file is the sole cause before re-deriving.

---------

Co-authored-by: Merge Sim <sim@local>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant