fix(app): native iOS text selection in assistant messages via UITextView#1153
fix(app): native iOS text selection in assistant messages via UITextView#1153muzhi1991 wants to merge 1 commit into
Conversation
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
b37abda to
535b760
Compare
|
Updated to fix the playwright CI failure. The web dev bundle was failing because importing Wrapped the import behind a platform-split shim following the existing
Re-verified locally:
|
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 asUILabelon iOS, which doesn't support word selection / drag handles — only "copy the entire block". macOS and web work because CSSuser-select: textis the default.What this PR does
Swap the inline
<Text>used by the markdown renderer forreact-native-uitextview's<UITextView uiTextView selectable>, which renders as a realUITextViewon iOS (native word selection + drag handles + system menu) and falls back to base<Text>on Android/web.react-native-uitextviewis 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
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:
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
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:
Test plan
Verified on iPhone 17 Pro Simulator (iOS 26.5) with a built dev client:
Screenshot
Refs #21, #238, #648