feat(chat): make assistant message text copyable and selectable - #184
Closed
omgoshjosh wants to merge 2 commits into
Closed
feat(chat): make assistant message text copyable and selectable#184omgoshjosh wants to merge 2 commits into
omgoshjosh wants to merge 2 commits into
Conversation
Assistant prose had no copy path at all. src/components/markdown/Markdown.tsx defines a CustomRenderer whose entire purpose is to strip the `selectable` prop that react-native-marked hardcodes on every plain-text node, because a selectable <Text> nested in a FlatList row hits react/react-native#46999 on Android. Chat messages are rows of the session screen's inverted FlatList, so every markdown text node hits it. That workaround is correct as far as it goes, but its stated justification -- "code content is still copyable via CodeBlock's explicit Copy button, so dropping `selectable` on plain text costs little" -- undercounts the cost. User messages are plainly `selectable` and tool output is `selectable`, but assistant prose, the thing users most want to copy, could not be selected, copied, or shared by any means. Rather than re-enabling `selectable` inside the FlatList row (which is what RN#46999 punishes), add a copy path that reads the source text from the message parts: - message-copy-text.ts: pure extractCopyText/extractReasoningText/ hasCopyableText over a message's parts. Dependency-free, unit-tested. - SelectableTextModal: renders that text in a `selectable` <Text> inside a <Modal>. The modal renders outside the transcript FlatList, so RN#46999 does not apply and real partial selection works. - MessageBubble: long-press enabled for both roles (was user-only). - session/[id].tsx: the action sheet offers "Copy message" and "Select text" for either role; "Edit message" stays user-only since reverting to an assistant message is unsupported. Returns early when a message has no prose so tool-only messages don't open an empty sheet. Known gap: app/demo.tsx renders MessageBubble without onLongPress, so the demo conversation is still uncopyable. Left out to keep this reviewable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Under Expo's mandatory edge-to-edge display the modal sheet extends beneath the system navigation bar, so the fixed paddingBottom:20 left the hint drawn behind it -- confirmed on an Android 12 emulator. Pad by the real safe-area bottom inset instead, with a floor so the hint still has breathing room on devices reporting an inset of 0. Same class of edge-to-edge inset bug as the composer/keyboard issue (dzianisv#156). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
|
Folding this into #182 to keep the tracker tidy — same commit, same verification, just consolidated with the other two Android fixes rather than three separate PRs. Sorry for the noise. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Assistant prose currently has no copy path at all on Android.
Why
src/components/markdown/Markdown.tsxdefines aCustomRendererwhose whole purpose is to strip theselectableprop thatreact-native-markedhardcodes on every plain-text node, because aselectable<Text>inside aFlatListrow hits facebook/react-native#46999 on Android. Chat messages are rows of the session screen's invertedFlatList, so every markdown node hits it.That workaround is correct as far as it goes. But its stated justification —
— undercounts the cost. Current coverage:
<Text selectable>inMessageBubbleCodeBlock's Copy button<Text selectable>inToolCallCardSo the one thing users most want to copy — the assistant's actual answer — can't be selected, copied or shared by any means.
Approach
Rather than re-enabling
selectableinside the FlatList row (exactly what RN#46999 punishes), add a copy path that reads the source text from the message parts:message-copy-text.ts— pureextractCopyText/extractReasoningText/hasCopyableTextover a message's parts. Dependency-free, unit-tested.SelectableTextModal— renders that text in aselectable<Text>inside a<Modal>. The modal is the point: it renders into its own host view outside the transcript FlatList, so RN#46999 does not apply and real partial selection works.MessageBubble— long-press enabled for both roles (was user-only).session/[id].tsx— the action sheet offers "Copy message" and "Select text" for either role; "Edit message" stays user-only since reverting to an assistant message isn't supported. Returns early when a message has no prose, so tool-only messages don't open an empty sheet.i18n strings added to both
enandzh-Hans(catalog-parity test enforced).The second commit fixes a safe-area bug in the new modal: under edge-to-edge the sheet extends beneath the navigation bar, so a fixed
paddingBottomleft the hint drawn behind it. It now pads by the real bottom inset.Verification
Verified on an Android 12 emulator against a live server:
Partial selection of assistant prose now works where it was previously impossible.
tsc --noEmitclean; full suite passes (294 tests) on this branch.Note
app/demo.tsxrendersMessageBubblewithoutonLongPress, so the demo conversation is still uncopyable. Deliberately left out to keep this reviewable — happy to add it here or in a follow-up, whichever you prefer.