Skip to content

feat(mobile): show agent messages as compact cards - #147

Merged
lukemaj merged 2 commits into
feat/140-agent-message-rowsfrom
feat/145-mobile-agent-message-cards
Oct 7, 2026
Merged

lukemaj merged 2 commits into
feat/140-agent-message-rowsfrom
feat/145-mobile-agent-message-cards

Conversation

@lukemaj

@lukemaj lukemaj commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

What: The mobile app shows reports and messages from other agents as compact, expandable cards, matching web #142, instead of as user bubbles.
Why: #142 only changes the web timeline. The mobile app ships its own UI, so it still showed these messages as user bubbles.
So what: Stacked on #142. Retarget to main once #142 merges. Verified on the iOS simulator; the user approves the merge.

Closes #145

Changes

  • parseAgentMessage and its test move from apps/web to packages/client-runtime (new ./agent-message export). Web imports it from there; the web row is otherwise unchanged.
  • New apps/mobile/src/features/threads/AgentMessageCard.tsx, styled like ThreadAgentSpawnCard. It shows an icon, "Report from"/"Message from", the thread title and a one-line muted preview, and starts collapsed. Tapping it toggles the full body, rendered with AssistantMarkdownContent. Long-press copies the raw text.
  • Tapping the title opens the sender thread with useAdaptiveWorkspaceLayout().selectThread, the same call the thread sidebar uses. The mobile subagent card's members aren't tappable, so it had no navigation to copy. If the thread is gone, the title shows as muted plain text.
  • Previews drop a leading markdown marker, the preview hides while expanded, and the card offers "Open " as an accessibility action (double tap always toggles).
  • ThreadFeed.tsx and packages/client-runtime/package.json are allowlisted in scripts/fork-upstream-edits.txt, owned by a new agent-message-cards entry in docs/fork-features.md.
  • ThreadFeed.tsx (upstream file): one import plus one early return before the user-bubble branch. Parse results are cached per message object in a WeakMap, because the render function can't use hooks.
  • JS only. No native, app config, server or contract changes.

Proof

  • pnpm exec vp test run packages/client-runtime/src/agentMessage.test.ts: 6 passed.
  • tsc --noEmit passes in packages/client-runtime, apps/web and apps/mobile.
  • vp lint on the changed files: no errors. Its warnings are all on existing lines in ThreadFeed.tsx and MessagesTimeline.tsx; the new file has none.
  • iPhone 17 Pro Max simulator (iOS 27.0): all five card checks pass, and the follow-up fixes were rechecked on screen. See the verification comment and the follow-up comment. VoiceOver itself was not run.

Elon record

Requirements and who asked: The user wants mobile to match web #142.
Deleted: Native changes and server changes.
Bottleneck: Mobile still renders agent messages as user bubbles.
Checked myself: The planner confirmed the mobile app bundles its own UI, so web #142 can't reach it.

Implemented by Claude Opus 5.5 (medium effort) in T3 Code (Chromeria) through Claude Code.

🤖 Generated with Claude Code

Child reports and thread-to-thread messages rendered as user bubbles on
mobile. The header parser moves to client-runtime (`./agent-message`) so web
and mobile share it, and the mobile feed now shows these messages as a card
styled like the subagent card: sender, one-line preview, and the full body
inline on tap. The sender's title opens its thread while it still exists.

Refs #145

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Oct 2, 2026
@lukemaj

lukemaj commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Independent review (Prism reviewer, gpt-6-luna) at ea26699: one Medium finding, no others. The cache, keys, expanded state, missing-thread fallback, client-runtime export and web imports all checked out.

Finding dismissed: it said the title's nested Pressable (AgentMessageCard.tsx:94) does not stop the tap reaching the card's onPress, so one tap could both navigate and toggle. React Native's responder system grants the touch to the deepest Pressable that claims it, so the outer card's onPress never fires. DOM-style bubbling doesn't apply here, and apps/mobile is native-only (no react-native-web). This still needs on-device confirmation, which nobody has done yet.

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 13.5 KiB — 15.1 KiB ✅
Codex Thread snapshot wire — 7.1 KiB — 7.3 KiB ✅
Codex Live turn WebSocket wire — 6.4 KiB — 7.8 KiB ✅
Codex Live turn WebSocket decoded — 56.2 KiB — 66.4 KiB ✅
Codex Live turn messages — 9 — 21 ✅
Claude Total thread wire — 13.6 KiB — 15.1 KiB ✅
Claude Thread snapshot wire — 7.1 KiB — 7.3 KiB ✅
Claude Live turn WebSocket wire — 6.5 KiB — 7.8 KiB ✅
Claude Live turn WebSocket decoded — 57.0 KiB — 66.4 KiB ✅
Claude Live turn messages — 9 — 21 ✅

Baseline: unavailable · PR result: 6f6c17e · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 114.0 KiB
  • Claude decoded thread snapshot: 114.7 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@lukemaj

lukemaj commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

What: I could not run the device check: the iOS dev client did not build because CocoaPods is not installed on rocky's Mac (spawn pod ENOENT).
Why: scripts/mobile-native-client.ts ensure ios runs pod install, and this Mac has no pod (no Homebrew cocoapods, only system Ruby 2.6). Installing host software is a Human Gate, and the brief did not cover it.
So what: Nothing about the cards was checked on a device. To unblock it, the user approves installing CocoaPods on rocky's Mac (for example brew install cocoapods), and then the verification gets rerun.

Result

Check Result
Agent messages render as collapsed cards (icon, "Report from"/"Message from", title, one-line preview) Not verified (blocked)
Tapping the card opens the full markdown body, tapping again closes it Not verified (blocked)
Tapping the title opens the sender's thread Not verified (blocked)
Deleted sender shows as plain text Not verified (blocked)
The user's own messages are still normal bubbles Not verified (blocked)

No screenshots: the app never launched.

What ran

  • Worktree chromeria-worktrees/145-mobile-verify at ea26699de0.
  • Test data: VACUUM INTO copy of ~/.t3/userdata/state.sqlite (read-only). It had 1 thread and no [Subagent /[Message from messages. With the server stopped, I seeded these in the worktree .t3 copy (chromeria project):
    • Thread PR 147 fixture: agent messages, with your own messages, an assistant reply, a markdown report from Worker: fix the header parser (that thread exists), and a message from Old scout (that thread has deleted_at set).
    • The first seed left out model_selection_json. The server then failed with PersistenceDecodeError ... Expected string at [2]["modelSelection"]. After I filled that field, the server started cleanly.
  • Backend: vp run dev started cleanly on port 13872 with the seeded data.
  • Metro: vp run dev:client started on :8081.
  • Native client:
    1. The first ensure ios failed with spawn vp ENOENT. The script runs vp, which is not on PATH when run from a shell without vp. I reran with node_modules/.bin on PATH.
    2. That run failed at pod install with spawn pod ENOENT. Signing was never reached.
  • Device tools: device_list returned "Agent device access is turned off for this environment", so I was using the approved simctl/agent-device fallback.

Teardown

I stopped the backend and Metro by their PIDs and shut down the simulator. The worktree is still in place, with the seeded .t3 and the planner's pnpm-lock.yaml change untouched. No code was pushed.

Verified by Claude Opus 5.5 in T3 Code (Chromeria) through Claude Code.

@lukemaj

lukemaj commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

What: All five mobile checks pass on the iPhone 17 Pro Max simulator (iOS 27.0) at ea26699de0. This replaces the blocked result in my previous comment.
Why: After the user approved installing CocoaPods on rocky's Mac, the dev client built and installed.
So what: The checks found nothing blocking. There are three smaller findings below for the author to accept or fix. The merge still needs the user's approval.

Checks

Check Result
Agent messages show as collapsed cards: icon, "Report from"/"Message from", title, one-line preview Pass
Tapping the card opens the full markdown body (heading, bold, inline code, list, code block); tapping again closes it Pass
Tapping the title opens the sender's thread Pass
A deleted sender shows as muted plain text; tapping it toggles the card and does not navigate Pass
The user's own messages are still normal bubbles Pass
Collapsed Expanded Deleted sender, tapped Title tap opens the sender thread

Collapsed again after the second tap: screenshot.

Findings (not blocking)

  1. The preview shows raw markdown. A report whose body starts with ## Summary previews as "## Summary". parseAgentMessage returns the first non-empty line as-is, so web feat(web): show agent messages as compact rows #142 likely does the same.
  2. The preview line stays visible after expanding. The first line of the body then shows twice: once muted in the preview, and again in the body (see the deleted-sender screenshot).
  3. The title link is missing from the accessibility tree. agent-device could not find label="Open Worker: fix the header parser". Only the card's own button shows up, so I had to tap the title by coordinates. That suggests VoiceOver users can't reach the sender's thread. Inferred from the AX snapshot; I did not test with VoiceOver.

Not verified

  • Long-press to copy, Android, dark mode, iPad layout.
  • Real agent-sent messages. The live data had none, so I seeded projection rows in the worktree's .t3 copy: one report from an existing thread and one message from a thread with deleted_at set.

Setup notes

  • device_list reports that device access is turned off, so I drove the simulator with simctl and agent-device@0.21.12.
  • scripts/mobile-native-client.ts ensure ios needs vp on PATH (spawn vp ENOENT otherwise) and needs CocoaPods. Homebrew cocoapods 1.17.0 is now installed on rocky's Mac.
  • Teardown: I stopped the backend and Metro by PID and shut down the simulator. The worktree is still in place. No code was pushed.

Verified by Claude Opus 5.5 in T3 Code (Chromeria) through Claude Code.

- Previews drop the leading markdown marker (`## Summary` reads "Summary").
- The preview hides while the card is expanded, as on web.
- The card offers "Open <sender>" as an accessibility action, and double
  tap always toggles it, since the nested title link is not reachable.
- Allowlist the two upstream edits and add the feature to the fork map so
  the Fork Stack Model check passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@lukemaj

lukemaj commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

What: 6f6c17eb44 fixes the failing Fork Stack Model check and the three findings from my simulator verification. All 14 checks now pass.
Why: The PR edited two upstream files that weren't on the fork allowlist. The verification also found a raw-markdown preview, a duplicated first line when a card is open, and a sender link that VoiceOver couldn't reach.
So what: The PR is ready for the user's merge approval once #142's stack is settled.

Changes

  • Fork check: apps/mobile/src/features/threads/ThreadFeed.tsx and packages/client-runtime/package.json are now in scripts/fork-upstream-edits.txt, with a new agent-message-cards entry in docs/fork-features.md. scripts/fork-check.sh passes locally and in CI.
  • Preview: parseAgentMessage drops a leading heading, list, numbered-list or quote marker. This also applies to web, which shares the parser. New test; 6 of 6 pass.
  • Expanded card: the preview line hides while the card is open, matching web.
  • VoiceOver: the card offers an "Open " accessibility action, and double tap always toggles the card, so it never lands on the nested link by accident.

Rechecked on the iPhone 17 Pro Max simulator

Preview without ## Expanded, no duplicate line
  • Tapping the sender title still opens the sender's thread.
  • tsc --noEmit passes in apps/mobile and packages/client-runtime, and vp lint is clean on the changed files.

Not verified

  • The new accessibility action with VoiceOver actually on. agent-device can't trigger custom actions, and the mobile app has no screen-reader tests.

Implemented and verified by Claude Opus 5.5 in T3 Code (Chromeria) through Claude Code.

@lukemaj
lukemaj merged commit 9d0d349 into feat/140-agent-message-rows Oct 7, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant