feat(mobile): make message prose selectable on Android#4549
Conversation
Additive eas.json submit profile for the personal TestFlight app (ascAppId 6793805091, ASC API key read from local decent-measure credentials). Upstream's production profile stays untouched.
T3CODE_ANDROID_PACKAGE env hook mirrors the iOS personal-team override so personal builds ship under com.raijustudios.t3code. The personal submit profile gains the Play internal-track config using decent-measure's service account key.
Sync upstream/main (54 commits, 2026-07-25)
* Make submit credentials self-contained Submit keys now live in gitignored apps/mobile/credentials/ instead of absolute paths into the decent-measure checkout. * Add personal release-pipeline runbook and research docs Ported from the planning repo (r4iju/t3-code) ahead of archiving it; docs/personal/ is additive and stays clear of upstream merges.
The native Ghostty surface renders text on the GPU with no link hit-testing, so URLs printed in the terminal (e.g. the claude OAuth login URL) could not be opened on mobile. Extract http(s) URLs from the raw PTY stream — stripping ANSI sequences and reassembling URLs hard-wrapped to the terminal width — and show them as a chip bar above the terminal. Tap opens the full URL, long-press copies it.
Android had no way to select or copy parts of a chat message: the native selectable-markdown view is iOS-only, and the react-native- nitro-markdown fallback rendered prose in plain Text without the selectable prop. Patch react-native-nitro-markdown (pnpm patch) to accept a selectable option, threaded through its context onto the root Text nodes (inline group wrapper, standalone text, headings) where Android honors it, and pass it at the three fallback Markdown call sites in ThreadFeed. Verified on a Pixel 8 Pro emulator: long-press selects a word with drag handles, extending and copying pastes exactly the selected fragment.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Opened against the wrong repo (this is fork-local work) — apologies for the noise. |
There was a problem hiding this comment.
🟡 Medium
t3code/apps/mobile/app.config.ts
Line 177 in a7c82dd
Setting T3CODE_EAS_PROJECT_ID changes extra.eas.projectId but leaves updates.url hard-coded to https://u.expo.dev/d763fcb8-d37c-41ea-a773-b54a0ab4a454, so binaries built for an overridden project query the upstream project's Expo update service and never receive OTA updates published to the configured project. updates.url must contain the same project ID, so derive it from the resolved value instead of using a separate hard-coded constant.
- url: "https://u.expo.dev/d763fcb8-d37c-41ea-a773-b54a0ab4a454",
+ url: `https://u.expo.dev/${process.env.T3CODE_EAS_PROJECT_ID ?? "d763fcb8-d37c-41ea-a773-b54a0ab4a454"}`,🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/app.config.ts around line 177:
Setting `T3CODE_EAS_PROJECT_ID` changes `extra.eas.projectId` but leaves `updates.url` hard-coded to `https://u.expo.dev/d763fcb8-d37c-41ea-a773-b54a0ab4a454`, so binaries built for an overridden project query the upstream project's Expo update service and never receive OTA updates published to the configured project. `updates.url` must contain the same project ID, so derive it from the resolved value instead of using a separate hard-coded constant.
| const text = stripAnsi(window); | ||
| const lines = text.split("\n"); | ||
|
|
||
| const ordered: string[] = [...oscUrls]; |
There was a problem hiding this comment.
🟡 Medium terminal/terminalBufferLinks.ts:128
extractTerminalBufferLinks does not return URLs most-recent-first: it places every OSC 8 hyperlink before every visible-text URL, so a newer OSC link gets ordered before older plain-text URLs and slice(0, 3) drops it. If an OAuth login URL is emitted as an OSC hyperlink after three earlier plain URLs, the actually newest link is omitted from the results. The ordered array is built with [...oscUrls, ...visibleUrls] regardless of where each URL appears in the buffer, so the reverse-traversal dedup preserves that wrong order. Consider collecting OSC and visible URLs together in buffer position order instead.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/terminal/terminalBufferLinks.ts around line 128:
`extractTerminalBufferLinks` does not return URLs most-recent-first: it places every OSC 8 hyperlink before every visible-text URL, so a newer OSC link gets ordered before older plain-text URLs and `slice(0, 3)` drops it. If an OAuth login URL is emitted as an OSC hyperlink after three earlier plain URLs, the actually newest link is omitted from the results. The `ordered` array is built with `[...oscUrls, ...visibleUrls]` regardless of where each URL appears in the buffer, so the reverse-traversal dedup preserves that wrong order. Consider collecting OSC and visible URLs together in buffer position order instead.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a7c82dd. Configure here.
| } | ||
|
|
||
| return url; | ||
| } |
There was a problem hiding this comment.
Adjacent URLs wrongly joined
High Severity
The joinWrappedUrl function's logic for identifying URL continuations is too permissive. It can incorrectly concatenate a URL with a subsequent line that is either a separate full URL or the complete version of a truncated one. This results in invalid, merged URLs being offered in the chip bar, impacting critical links like OAuth URLs.
Reviewed by Cursor Bugbot for commit a7c82dd. Configure here.
| "android": { | ||
| "serviceAccountKeyPath": "./credentials/android/barbellry-45151cd5babe.json", | ||
| "track": "internal" | ||
| } |
There was a problem hiding this comment.
Personal fork config committed
Medium Severity
This PR adds a personal EAS submit profile with private Apple team/ASC credentials and a Play service account path, along with docs/personal/* runbooks. This personal fork-specific configuration and documentation are unrelated to the PR's stated purpose and do not belong in the upstream repository.
Reviewed by Cursor Bugbot for commit a7c82dd. Configure here.
| deduped.push(url); | ||
| } | ||
|
|
||
| return deduped.slice(0, TERMINAL_LINK_MAX_RESULTS); |
There was a problem hiding this comment.
OSC links lose recency ranking
Medium Severity
OSC 8 hyperlink targets are prepended onto ordered before plain-text URLs, but results are ranked by reverse iteration (“most recent first”) and capped at three. That makes every OSC URL older than every plain URL in the window, so a recent OSC-only target can be dropped whenever three or more other http(s) URLs appear in the scan window.
Reviewed by Cursor Bugbot for commit a7c82dd. Configure here.
| if (url === undefined) continue; | ||
| if (deduped.some((existing) => existing.startsWith(url))) continue; | ||
| deduped.push(url); | ||
| } |
There was a problem hiding this comment.
Prefix dedup drops distinct URLs
Medium Severity
Deduping with existing.startsWith(url) removes any earlier URL that is a string prefix of a kept one. That catches spinner truncations, but also drops legitimate distinct links such as https://github.com/org/repo when https://github.com/org/repo-utils is present, so the chip bar can hide valid recent URLs.
Reviewed by Cursor Bugbot for commit a7c82dd. Configure here.
ApprovabilityVerdict: Needs human review 2 blocking correctness issues found. This PR introduces new runtime features (TerminalLinkBar, selectable text) and includes unrelated personal fork configuration. Multiple unresolved review comments identify bugs in the URL extraction logic, including a high-severity issue with adjacent URLs being incorrectly joined. You can customize Macroscope's approvability policy. Learn more. |
Address review findings from the accidental upstream PR (pingdotgg#4549): - Never join an adjacent URL onto a wrapped one: a continuation line that starts its own http(s) scheme is a separate link, not a wrap continuation of the previous line. - Rank OSC 8 hyperlinks by buffer position instead of prepending them, so a recent OSC link is no longer dropped by the 3-result cap when older plain-text URLs exist. - Replace prefix dedup (which hid distinct URLs like .../repo next to .../repo-utils) with \r-overwrite resolution: keep only the final paint of a line, which removes spinner-truncated intermediates at the source.
…k chips (#10) Updates the exit-path register now that the pingdotgg#4549 review findings are fixed (#8): the terminal link chips delta is code-ready, blocked only on device verification media. Captures the full remaining path — issue-first framing as mobile parity with web link handling, the exact seven-file trimmed diff to cherry-pick from upstream/main, and what the PR body must disclose (native alternative, heuristic limits, candid pingdotgg#4549 reference) — so the contribution can be picked up cold another day.


Problem
On Android you cannot select or copy parts of a chat message. The native selectable-markdown component (
@t3tools/mobile-markdown-text) is iOS-only, and thereact-native-nitro-markdownfallback renders prose in plainTextwithout theselectableprop — so message bodies are entirely non-selectable (only code blocks and the whole-message copy button work).Fix
react-native-nitro-markdown@0.5.8introducing aselectableprop on<Markdown>, threaded throughMarkdownContextand applied to the rootTextnodes: the inline-group wrapper, standalone text nodes, and headings (the only places Android honorsselectable). Patched insrc/(Metro),lib/(module + commonjs), and the.d.tsoutputs.selectableat the three fallback<Markdown>call sites inThreadFeed.tsx(assistant messages, user messages, user-message segments).iOS is unaffected — it uses the native
UITextView-backed path which already supports partial selection.Verification
tsc --noEmitclean; mobile unit tests pass.🤖 Generated with Claude Code
Note
Medium Risk
Terminal URL parsing opens arbitrary http(s) links from PTY output; the nitro-markdown patch must stay in sync with dependency upgrades.
Overview
This PR bundles several mobile improvements plus personal-fork release wiring.
Android chat copy: A pnpm patch adds a
selectableprop toreact-native-nitro-markdown, andThreadFeedpasses it on all fallback<Markdown>paths so Android users can long-press to copy message prose (iOS still uses native selectable markdown).Terminal URLs: Because the Ghostty surface has no link hit-testing, a new
terminalBufferLinksmodule scans the PTY buffer (ANSI strip, wrap reassembly, OSC 8, dedupe) andTerminalLinkBarshows tappable chips above the terminal in both the thread panel and full terminal route—tap opens viatryOpenExternalUrlwith targetterminal-link, long-press copies.Personal builds:
app.config.tshonorsT3CODE_ANDROID_PACKAGE,T3CODE_EAS_PROJECT_ID, andT3CODE_EAS_OWNER;eas.jsongains apersonalsubmit profile;credentials/is gitignored. New docs cover the personal release ritual and direct-LAN server/auth research.Reviewed by Cursor Bugbot for commit a7c82dd. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Make message prose and terminal links selectable on Android
react-native-nitro-markdown@0.5.8to add aselectableprop, which propagates to top-levelTextnodes and headings so users can select and copy rendered Markdown on Android.selectable={true}to the Markdown renderer for assistant and user messages in ThreadFeed.tsx.TerminalLinkBarcomponent that extracts HTTP(S) URLs from the terminal PTY buffer (handling ANSI sequences, OSC 8 hyperlinks, and hard-wrapped lines), displays them as tappable chips, and supports tap-to-open and long-press-to-copy.📊 Macroscope summarized a7c82dd. 13 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.