Skip to content

fix(app): native iOS text selection in assistant messages via UITextView#1153

Open
muzhi1991 wants to merge 1 commit into
getpaseo:mainfrom
muzhi1991:fix/ios-assistant-text-selection
Open

fix(app): native iOS text selection in assistant messages via UITextView#1153
muzhi1991 wants to merge 1 commit into
getpaseo:mainfrom
muzhi1991:fix/ios-assistant-text-selection

Conversation

@muzhi1991
Copy link
Copy Markdown
Contributor

@muzhi1991 muzhi1991 commented May 24, 2026

Why

Long-press text selection on iOS assistant messages doesn't work — users can only "copy the whole message" via the button, not pick out a command, path, error snippet, or a phrase. Reported in #238 and #648. This is more painful than it sounds when you're vibe-coding from mobile and want to grab one line out of a long agent response.

Root cause

RN's <Text> renders as UILabel on iOS, which doesn't support word selection / drag handles — only "copy the entire block". macOS and web work because CSS user-select: text is the default.

What this PR does

Swap the inline <Text> used by the markdown renderer for react-native-uitextview's <UITextView uiTextView selectable>, which renders as a real UITextView on iOS (native word selection + drag handles + system menu) and falls back to base <Text> on Android/web.

react-native-uitextview is a tiny MIT library (zero runtime deps, ~400★) maintained by Bluesky and used in production by their app for the exact same problem. Smallest patch I could find that gives genuine UITextView behavior without a renderer rewrite.

Why not other approaches

Files changed

  • `packages/app/package.json` — add `react-native-uitextview@^2.2.0`
  • `packages/app/src/components/message.tsx`
    • `MarkdownInheritedText` (text / textgroup / code_inline rules): always `UITextView` (auto-falls back to Text on non-iOS).
    • `MarkdownParagraphView`: iOS uses `UITextView`; other platforms keep `View` (the library's non-iOS fallback to Text would break Android, since Text can't contain View children).
  • `packages/app/src/components/highlighted-code-block.tsx`
    • `` wrapping code/tokens → `` so users can select substrings inside a code block.

Diff is small (+37 / -4 across 4 files including lockfile).

Known limitation

Selection cannot cross paragraph or code-block boundaries — each block is an independent `UITextView` and iOS native selection can't span sibling native views. Selection within a single paragraph (including across inline `bold` / `italic` / `` `code` `` / `link`) or within a single code block works.

Supporting cross-block selection would require rendering an entire message as a single `UITextView`, losing block-level layout (list indent, code-block padding, etc.) — not worth the trade-off in my opinion.

Cross-platform safety

`react-native-uitextview` source confirms a hard `Platform.OS !== 'ios'` early-return that yields base `RNText` — the native components are never instantiated off-iOS:

```tsx
export function UITextView(props) {
if (Platform.OS !== 'ios') {
return <RNText {...props} /> // Android / web / desktop
}
return <UITextViewInner {...props} /> // iOS only
}
```

Additionally `MarkdownParagraphView` has its own `Platform.OS === 'ios'` guard, so on Android/web/desktop the original `` path is preserved (important because paragraph children can include image `` nodes that wouldn't be valid Text children).

Per-platform behavior summary:

Platform Behavior after this PR Mechanism
iOS ✅ Native UITextView word selection UITextViewInner
Android Unchanged — base RN Text (same as before) library Platform guard
Web (react-native-web) Unchanged — base RN Text rendered with default CSS `user-select: text` (same as before) library Platform guard
Desktop (Electron) Unchanged — runs web bundle, same as Web same as Web

Bluesky's social-app uses this library across iOS / Android / RN-web in production, so the cross-platform path is battle-tested upstream.

Architecture notes

  • `react-native-uitextview@2.x` requires RN New Architecture. paseo already has `newArchEnabled: true` ✓
  • No `` children flow into `` — verified that `AssistantMarkdownLink` / `AssistantMarkdownCodeLink` / `AssistantInlineCodePathLink` all render as `` on native.

Disclosure

I'm not an iOS engineer — this fix came out of digging through the existing issues / your `message.tsx` rules with the help of an LLM to identify the right library, then verifying behavior in the simulator myself. Diff is small and contained, but I'd appreciate a sanity check from someone with deeper RN/iOS context, especially on:

  1. Whether `UITextView` interacts cleanly with existing handlers (`onPress` on `AssistantMarkdownLink`, `CopyButton` overlay in `HighlightedCodeBlock`).
  2. Anything in your style pipeline that might assume `` vs `UITextView`.

Test plan

Verified on iPhone 17 Pro Simulator (iOS 26.5) with a built dev client:

  • Long-press on assistant paragraph → drag handles, can select arbitrary word/range within paragraph
  • Selection spans inline `bold` / `italic` / `` `code` `` / `link` within the same paragraph
  • Long-press on syntax-highlighted code block → can select substrings, copy works
  • System menu (Copy / Look Up / Translate / Share) appears
  • User messages unchanged (already used ``)
  • TypeScript typecheck passes; pre-commit lint/format pass
  • Web bundle verified: `expo export --platform web` succeeds (9.76 MB index.js, no `codegenNativeComponent` errors); library's Platform guard prevents any non-iOS native call.
  • Not exercised on Android or in a running web/desktop session — but no behavior change is expected (see Cross-platform safety above), and the upstream library is in production at Bluesky on all those platforms.
  • Not verified on physical iOS device (I don't have a paid Apple Developer account).

Screenshot

Simulator Screenshot - iPhone 17 Pro - 2026-05-24 at 10 25 35

Refs #21, #238, #648

React Native's <Text> renders as UILabel on iOS, which only supports
"copy the entire block" — no word selection, no drag handles. Use
Bluesky's react-native-uitextview to opt the assistant-message
inline-text components into UITextView, giving iOS users native
long-press selection (drag handles + system Copy/Look Up/Translate).

Changes:
- MarkdownInheritedText (text/textgroup/code_inline rules): always
  UITextView; the library auto-falls back to base Text on non-iOS.
- MarkdownParagraphView: iOS uses UITextView, other platforms keep
  View (UITextView's non-iOS fallback is Text, which can't contain
  View children).
- HighlightedCodeBlock: code/token Text → UITextView so users can
  select substrings inside a code block.

Wrap the import behind a platform-split shim
(packages/app/src/components/selectable-text.{native,web,d}.ts) so
the web bundle never imports react-native-uitextview. Importing it
on web pulls in react-native/Libraries/Utilities/codegenNativeComponent
which transitively requires setUpReactDevTools and breaks Metro web
bundling in dev mode (ReactDevToolsSettingsManager source path doesn't
resolve in the web target). On web, base RN Text already renders with
default user-select: text, so selection behavior is unchanged.

Trade-off: selection cannot span paragraph or code-block boundaries —
each block is an independent UITextView and iOS native selection can't
cross sibling native views. Single-block selection works (including
across inline bold/italic/code/link).

Refs getpaseo#21, getpaseo#238, getpaseo#648
@muzhi1991 muzhi1991 force-pushed the fix/ios-assistant-text-selection branch from b37abda to 535b760 Compare May 24, 2026 08:25
@muzhi1991
Copy link
Copy Markdown
Contributor Author

Updated to fix the playwright CI failure.

The web dev bundle was failing because importing react-native-uitextview transitively pulls in react-native/Libraries/Utilities/codegenNativeComponentsetUpReactDevToolsReactDevToolsSettingsManager, which doesn't resolve under the web target in dev mode. (Production expo export --platform web tree-shakes dev tools, which is why my initial local web-bundle check passed but playwright — running the web dev bundle — failed.)

Wrapped the import behind a platform-split shim following the existing composer-height-mirror.{native,web,d}.ts convention in this repo:

  • packages/app/src/components/selectable-text.native.tsx — re-exports react-native-uitextview
  • packages/app/src/components/selectable-text.web.tsx — falls back to base RN Text (which already renders with default user-select: text on web)
  • packages/app/src/components/selectable-text.d.ts — TS module shim

message.tsx and highlighted-code-block.tsx now import from @/components/selectable-text instead of react-native-uitextview directly, so the web bundle never touches the upstream library.

Re-verified locally:

  • TypeScript typecheck ✓
  • expo export --platform web
  • expo export --platform ios ✓ (13.8 MB iOS bundle)
  • pre-commit hook (lint + format + typecheck) ✓
  • iPhone 17 Pro Simulator behavior unchanged

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