From c04c4cd3b9fdb1d59828d4b57e58505d06e02f8e Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:25:08 -0700 Subject: [PATCH] fix(mobile): keep the long-press actions sheet on reasoning rows for Android --- .../MobileNativeChatLongPressContent.tsx | 22 +++++++++++++ .../MobileNativeChatMessage.android.test.ts | 31 +++++++++++++++++++ .../src/session/MobileNativeChatMessage.tsx | 31 ++++++------------- .../session/MobileNativeChatReasoningRow.tsx | 11 +++++-- 4 files changed, 71 insertions(+), 24 deletions(-) create mode 100644 mobile/src/session/MobileNativeChatLongPressContent.tsx diff --git a/mobile/src/session/MobileNativeChatLongPressContent.tsx b/mobile/src/session/MobileNativeChatLongPressContent.tsx new file mode 100644 index 00000000000..923e949b7b4 --- /dev/null +++ b/mobile/src/session/MobileNativeChatLongPressContent.tsx @@ -0,0 +1,22 @@ +import type { ComponentProps, ReactNode } from 'react' +import { Pressable, View } from 'react-native' + +/** A message body that opens the actions sheet on long press (Android, which has no inline + * selection). Keep the existing responder hierarchy on platforms with inline selection. */ +export function MobileNativeChatLongPressContent({ + onLongPress, + style, + children +}: { + onLongPress?: () => void + style: ComponentProps['style'] + children: ReactNode +}): React.JSX.Element { + return onLongPress ? ( + + {children} + + ) : ( + {children} + ) +} diff --git a/mobile/src/session/MobileNativeChatMessage.android.test.ts b/mobile/src/session/MobileNativeChatMessage.android.test.ts index fff2a8d9eba..4184a28a869 100644 --- a/mobile/src/session/MobileNativeChatMessage.android.test.ts +++ b/mobile/src/session/MobileNativeChatMessage.android.test.ts @@ -32,6 +32,7 @@ vi.mock('react-native', async () => { vi.mock('expo-clipboard', () => ({ setStringAsync: vi.fn() })) vi.mock('lucide-react-native', () => ({ ArrowUp: 'ArrowUp', + Brain: 'Brain', ChevronDown: 'ChevronDown', Copy: 'Copy', SquareChevronRight: 'SquareChevronRight', @@ -106,6 +107,36 @@ describe('MobileNativeChatMessage on Android', () => { expect(byType('MessageActionsSheet')).toHaveLength(1) }) + it('opens the actions sheet from a long press on an expanded reasoning row, not on its headline', () => { + const reasoning: NativeChatMessage = { + ...message, + id: 'r1', + role: 'reasoning', + blocks: [{ type: 'text', text: 'Weighing two approaches' }], + timestamp: 1_000, + state: 'completed', + completedAt: 4_000 + } + act(() => { + renderer = create(createElement(MobileNativeChatMessage, { message: reasoning })) + }) + const [toggle] = byType('Pressable') + // A long press on the headline stays a plain toggle tap. + expect(toggle!.props.onLongPress).toBeUndefined() + act(() => toggle!.props.onPress()) + + const body = byType('Pressable').find((node) => node.props.onLongPress !== undefined) + expect(typeof body?.props.onLongPress).toBe('function') + const [markdown] = byType('MobileMarkdown') + expect(markdown!.props.onLongPress).toBe(body!.props.onLongPress) + + act(() => body!.props.onLongPress()) + const [sheet] = byType('MessageActionsSheet') + expect(sheet!.props.message).toBe(reasoning) + act(() => sheet!.props.onClose()) + expect(byType('MessageActionsSheet')).toHaveLength(0) + }) + it('renders the user bubble without inline selection', () => { act(() => { renderer = create( diff --git a/mobile/src/session/MobileNativeChatMessage.tsx b/mobile/src/session/MobileNativeChatMessage.tsx index b56b4e07108..803d914b9a3 100644 --- a/mobile/src/session/MobileNativeChatMessage.tsx +++ b/mobile/src/session/MobileNativeChatMessage.tsx @@ -1,8 +1,9 @@ import { MobileSelectableText as Text } from '../components/MobileSelectableText' -import { memo, useCallback, useState, type ComponentProps, type ReactNode } from 'react' -import { Image, Text as NativeText, Pressable, View } from 'react-native' +import { memo, useCallback, useState } from 'react' +import { Image, Text as NativeText, View } from 'react-native' import { INLINE_TEXT_SELECTION } from '../components/inline-text-selection' import { MobileNativeChatMessageActionsSheet } from './MobileNativeChatMessageActionsSheet' +import { MobileNativeChatLongPressContent as Content } from './MobileNativeChatLongPressContent' import { splitNativeChatBlocks } from '../../../src/shared/native-chat-tool-fold' import { selectActiveToolCall } from '../../../src/shared/native-chat-tool-activity' import { isImageRefBlock, isTextBlock } from '../../../src/shared/native-chat-types' @@ -90,25 +91,6 @@ function Prose({ return null } -// Keep the existing responder hierarchy on platforms with inline selection. -function Content({ - onLongPress, - style, - children -}: { - onLongPress?: () => void - style: ComponentProps['style'] - children: ReactNode -}): React.JSX.Element { - return onLongPress ? ( - - {children} - - ) : ( - {children} - ) -} - function MobileNativeChatMessageImpl({ message, toolsExpanded = false, @@ -193,9 +175,16 @@ function MobileNativeChatMessageImpl({ markdown={markdown} fontScale={fontScale} onOpenFile={onOpenFile} + onLongPress={onLongPress} /> ) : null} + {actionsOpen ? ( + setActionsOpen(false)} + /> + ) : null} {turnStatusAbove ? null : statusRow} ) diff --git a/mobile/src/session/MobileNativeChatReasoningRow.tsx b/mobile/src/session/MobileNativeChatReasoningRow.tsx index da2f753b455..a173a37c92d 100644 --- a/mobile/src/session/MobileNativeChatReasoningRow.tsx +++ b/mobile/src/session/MobileNativeChatReasoningRow.tsx @@ -8,6 +8,7 @@ import { import type { NativeChatMessage } from '../../../src/shared/native-chat-types' import { MobileMarkdown } from '../components/MobileMarkdown' import { colors } from '../theme/mobile-theme' +import { MobileNativeChatLongPressContent } from './MobileNativeChatLongPressContent' import { styles } from './mobile-native-chat-message-styles' /** A reasoning row, collapsed to its headline; its text mounts only once opened. Desktop parity: @@ -16,12 +17,15 @@ export function MobileNativeChatReasoningRow({ message, markdown, fontScale, - onOpenFile + onOpenFile, + onLongPress }: { message: Pick markdown: string fontScale: number onOpenFile?: (relativePath: string) => void + /** Android only: opens the message's actions sheet, as a long press on any other message does. */ + onLongPress?: () => void }): React.JSX.Element { const [expanded, setExpanded] = useState(false) const headline = nativeChatReasoningHeadlineText(nativeChatReasoningHeadline(message)) @@ -46,14 +50,15 @@ export function MobileNativeChatReasoningRow({ {expanded ? ( - + - + ) : null} )