Skip to content

feat(mobile): make message prose selectable on Android#4549

Closed
r4iju wants to merge 8 commits into
pingdotgg:mainfrom
r4iju:feat/mobile-selectable-message-text
Closed

feat(mobile): make message prose selectable on Android#4549
r4iju wants to merge 8 commits into
pingdotgg:mainfrom
r4iju:feat/mobile-selectable-message-text

Conversation

@r4iju

@r4iju r4iju commented Jul 26, 2026

Copy link
Copy Markdown

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 the react-native-nitro-markdown fallback renders prose in plain Text without the selectable prop — so message bodies are entirely non-selectable (only code blocks and the whole-message copy button work).

Fix

  • Add a pnpm patch for react-native-nitro-markdown@0.5.8 introducing a selectable prop on <Markdown>, threaded through MarkdownContext and applied to the root Text nodes: the inline-group wrapper, standalone text nodes, and headings (the only places Android honors selectable). Patched in src/ (Metro), lib/ (module + commonjs), and the .d.ts outputs.
  • Pass selectable at the three fallback <Markdown> call sites in ThreadFeed.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 --noEmit clean; mobile unit tests pass.
  • On a Pixel 8 Pro emulator (API 34): long-press on assistant prose selects a word with drag handles and the native Copy/Share/Select all menu; extending the selection and copying, then pasting into the composer, yields exactly the selected fragment.

🤖 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 selectable prop to react-native-nitro-markdown, and ThreadFeed passes 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 terminalBufferLinks module scans the PTY buffer (ANSI strip, wrap reassembly, OSC 8, dedupe) and TerminalLinkBar shows tappable chips above the terminal in both the thread panel and full terminal route—tap opens via tryOpenExternalUrl with target terminal-link, long-press copies.

Personal builds: app.config.ts honors T3CODE_ANDROID_PACKAGE, T3CODE_EAS_PROJECT_ID, and T3CODE_EAS_OWNER; eas.json gains a personal submit 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

  • Patches react-native-nitro-markdown@0.5.8 to add a selectable prop, which propagates to top-level Text nodes and headings so users can select and copy rendered Markdown on Android.
  • Passes selectable={true} to the Markdown renderer for assistant and user messages in ThreadFeed.tsx.
  • Adds a TerminalLinkBar component 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.
  • Adds URL extraction logic in terminalBufferLinks.ts with deduplication, recency capping, and a configurable scan window.
📊 Macroscope summarized a7c82dd. 13 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

r4iju added 8 commits July 23, 2026 13:41
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.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d765e59-ba8d-4dd6-8f64-58717eff58ec

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ 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.

@r4iju

r4iju commented Jul 26, 2026

Copy link
Copy Markdown
Author

Opened against the wrong repo (this is fork-local work) — apologies for the noise.

@r4iju r4iju closed this Jul 26, 2026
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Jul 26, 2026
Comment thread apps/mobile/app.config.ts

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.

🟡 Medium

url: "https://u.expo.dev/d763fcb8-d37c-41ea-a773-b54a0ab4a454",

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];

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.

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

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

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

❌ 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;
}

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a7c82dd. Configure here.

Comment thread apps/mobile/eas.json
"android": {
"serviceAccountKeyPath": "./credentials/android/barbellry-45151cd5babe.json",
"track": "internal"
}

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a7c82dd. Configure here.

deduped.push(url);
}

return deduped.slice(0, TERMINAL_LINK_MAX_RESULTS);

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a7c82dd. Configure here.

if (url === undefined) continue;
if (deduped.some((existing) => existing.startsWith(url))) continue;
deduped.push(url);
}

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.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a7c82dd. Configure here.

@macroscopeapp

macroscopeapp Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@r4iju
r4iju deleted the feat/mobile-selectable-message-text branch July 26, 2026 01:50
r4iju added a commit to r4iju/t3code that referenced this pull request Jul 26, 2026
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.
r4iju added a commit to r4iju/t3code that referenced this pull request Jul 26, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant