diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 995d57ef1995..8daf56594bd5 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -31,6 +31,7 @@ import type { ThreadUserInputQuestion } from "@t3tools/client-runtime/state/thre import { resolveSubagentPillSegment } from "@t3tools/client-runtime/state/thread-subagents"; import type { QueuedRunEdit } from "../../state/queued-run-edit"; import type { FollowUpBehavior } from "../../lib/followUpBehavior"; +import type { ActiveTurnComposerAction } from "@t3tools/client-runtime/state/composer-dispatch"; import * as Haptics from "expo-haptics"; import { memo, @@ -182,7 +183,7 @@ export interface ThreadDetailScreenProps { readonly onNativePasteText: (paste: ComposerTextPaste) => Promise; readonly onRemoveDraftImage: (imageId: string) => void; readonly onStopThread: () => void; - readonly onSendMessage: () => Promise; + readonly onSendMessage: (followUp?: ActiveTurnComposerAction) => Promise; readonly onReconnectEnvironment: () => void; /** Whether the model picker may offer providers other than this thread's. */ readonly canSwitchThreadProvider: boolean; @@ -857,40 +858,43 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread selectedThreadKey, ]); - const handleSendMessage = useCallback(async () => { - const targetThreadKey = selectedThreadKey; - const hasUserMessage = selectedThreadFeed.some( - (entry) => entry.type === "message" && entry.message.role === "user", - ); - const messageId = await props.onSendMessage(); - if (messageId === null || selectedThreadKeyRef.current !== targetThreadKey) { - return messageId; - } + const handleSendMessage = useCallback( + async (followUp?: ActiveTurnComposerAction) => { + const targetThreadKey = selectedThreadKey; + const hasUserMessage = selectedThreadFeed.some( + (entry) => entry.type === "message" && entry.message.role === "user", + ); + const messageId = await props.onSendMessage(followUp); + if (messageId === null || selectedThreadKeyRef.current !== targetThreadKey) { + return messageId; + } - // A sent message makes the snapshot stale; a refused send leaves it in place. - clearUsageLimitsFor(targetThreadKey); + // A sent message makes the snapshot stale; a refused send leaves it in place. + clearUsageLimitsFor(targetThreadKey); - setSubmittedMessageId(messageId); - setAnchorMessageId( - resolveThreadFeedSubmissionAnchor({ - currentAnchorMessageId: anchorMessageId, - submittedMessageId: messageId, - hasStartedTurn: props.selectedThread.latestRun !== null, - hasUserMessage, - queuedMessageCount: props.selectedThreadQueueCount, - }), - ); - composerEditorRef.current?.blur(); - return messageId; - }, [ - anchorMessageId, - clearUsageLimitsFor, - props.onSendMessage, - props.selectedThread.latestRun, - props.selectedThreadQueueCount, - selectedThreadFeed, - selectedThreadKey, - ]); + setSubmittedMessageId(messageId); + setAnchorMessageId( + resolveThreadFeedSubmissionAnchor({ + currentAnchorMessageId: anchorMessageId, + submittedMessageId: messageId, + hasStartedTurn: props.selectedThread.latestRun !== null, + hasUserMessage, + queuedMessageCount: props.selectedThreadQueueCount, + }), + ); + composerEditorRef.current?.blur(); + return messageId; + }, + [ + anchorMessageId, + clearUsageLimitsFor, + props.onSendMessage, + props.selectedThread.latestRun, + props.selectedThreadQueueCount, + selectedThreadFeed, + selectedThreadKey, + ], + ); const handleEditPendingMessage = useCallback(async (message: QueuedThreadMessage) => { try { diff --git a/patches/@react-native-menu__menu@2.0.0.patch b/patches/@react-native-menu__menu@2.0.0.patch index 65e6f24a7901..261779a3fb54 100644 --- a/patches/@react-native-menu__menu@2.0.0.patch +++ b/patches/@react-native-menu__menu@2.0.0.patch @@ -170,7 +170,7 @@ index 6a2a205f95c1001f3fdf6fb835815fa7c4c02c16..4f42b64349966fb1438f72439cec8256 + } diff --git a/ios/Shared/MenuViewImplementation.swift b/ios/Shared/MenuViewImplementation.swift -index 5c4e0da4292b15d3a27b5ea1555f11452a470815..4d8abe3c4fc45bda48c2958bc9ca933c77f2cc47 100644 +index 5c4e0da4292b15d3a27b5ea1555f11452a470815..475a99b2a3b1800c9d3e3e5f77a64f552fa35828 100644 --- a/ios/Shared/MenuViewImplementation.swift +++ b/ios/Shared/MenuViewImplementation.swift @@ -60,17 +60,46 @@ public class MenuViewImplementation: UIButton { @@ -222,7 +222,7 @@ index 5c4e0da4292b15d3a27b5ea1555f11452a470815..4d8abe3c4fc45bda48c2958bc9ca933c func setup () { let menu = UIMenu(title: _title, identifier: nil, -@@ -86,8 +115,98 @@ public class MenuViewImplementation: UIButton { +@@ -86,8 +115,102 @@ public class MenuViewImplementation: UIButton { } } @@ -309,19 +309,23 @@ index 5c4e0da4292b15d3a27b5ea1555f11452a470815..4d8abe3c4fc45bda48c2958bc9ca933c + if let existing = longPressInteraction { + existing.view?.removeInteraction(existing) + } -+ let interaction = UIContextMenuInteraction(delegate: self) ++ // UIControl's built-in interaction presents a compact menu that ++ // preserves the keyboard; a separate view interaction presents a ++ // rich context menu and temporarily hides it. ++ guard let interaction = self.contextMenuInteraction else { return } + host.addInteraction(interaction) + longPressInteraction = interaction + return + } + if let existing = longPressInteraction { + existing.view?.removeInteraction(existing) ++ self.addInteraction(existing) + longPressInteraction = nil + } } public override func reactSetFrame(_ frame: CGRect) { -@@ -124,4 +243,8 @@ public class MenuViewImplementation: UIButton { +@@ -124,4 +247,8 @@ public class MenuViewImplementation: UIButton { @objc func sendMenuOpen() { // NO-OP (should be overriden by parent) } @@ -379,7 +383,7 @@ index e6509355275596c451f9d082223cc16bfe504e1a..dd16e1a7403970cc3a404355f95ed995 actionsHash: string; // just a workaround to make sure we don't have to manually compare MenuActions manually in C++ (since it's a struct and that's a pain) title?: string; diff --git a/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts b/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts -index 24ee7fdcd9f81cfe2c813cfca9d4e276b846fb7b..5ee7b45bafea8547d68949aaba70584055f9fb53 100644 +index 24ee7fdcd9f81cfe2c813cfca9d4e276b846fb7b..c1b4ea53b29193283295d35c18e068f2836c6176 100644 --- a/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts +++ b/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts @@ -41,6 +41,9 @@ export interface NativeProps extends ViewProps { @@ -393,7 +397,7 @@ index 24ee7fdcd9f81cfe2c813cfca9d4e276b846fb7b..5ee7b45bafea8547d68949aaba705840 actionsHash: string; title?: string; diff --git a/lib/typescript/src/index.d.ts b/lib/typescript/src/index.d.ts -index 61d210aa9364f0354f77bddaecdc0a127b1cdbb4..84ac2bc8b4edac045ed59896d5eb4e6eaef4725d 100644 +index 61d210aa9364f0354f77bddaecdc0a127b1cdbb4..0672bea3352b9ad0623124e308f740c70a7e6940 100644 --- a/lib/typescript/src/index.d.ts +++ b/lib/typescript/src/index.d.ts @@ -4,6 +4,7 @@ declare const MenuView: import("react").ForwardRefExoticComponent<{ @@ -405,7 +409,7 @@ index 61d210aa9364f0354f77bddaecdc0a127b1cdbb4..84ac2bc8b4edac045ed59896d5eb4e6e title?: string; isAnchoredToRight?: boolean; diff --git a/lib/typescript/src/types.d.ts b/lib/typescript/src/types.d.ts -index eac7940ab7eb6a69ac329848c9f3eeffdcbaf43d..271cd27f777be06bc02f05d4b4dac5bb940f5f0f 100644 +index eac7940ab7eb6a69ac329848c9f3eeffdcbaf43d..19e82df15b042bc0cda27349b2b8a8ee1d3f0b51 100644 --- a/lib/typescript/src/types.d.ts +++ b/lib/typescript/src/types.d.ts @@ -113,6 +113,12 @@ type MenuComponentPropsBase = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d2c87a6d9d7..d37b4ad77a05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,7 +97,7 @@ patchedDependencies: '@legendapp/list@3.3.5': 680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806 '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4 '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d - '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd + '@react-native-menu/menu@2.0.0': a229d9974fc0a153eb7fae34599cc51171f2ab9a2bb79dc62f540db1ef592398 '@react-navigation/native-stack@7.17.6': e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552 dbus-next@0.10.2: cfff57561b0ee59b5addb3b2e6c6f20906e967507a530ab67e8db8108e520ba4 effect@4.0.0-rc.115: 0dfc4bb8ebd80fb3e06b91ef61346f5259517ab0f2437644fe95ae531084b1f5 @@ -268,7 +268,7 @@ importers: version: 0.12.0(patch_hash=2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) '@react-native-menu/menu': specifier: ^2.0.0 - version: 2.0.0(patch_hash=f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 2.0.0(patch_hash=a229d9974fc0a153eb7fae34599cc51171f2ab9a2bb79dc62f540db1ef592398)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@react-navigation/elements': specifier: 2.9.26 version: 2.9.26(c10301b6e0c42fc6434d2b643197a81e) @@ -14459,7 +14459,7 @@ snapshots: react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) optional: true - '@react-native-menu/menu@2.0.0(patch_hash=f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@react-native-menu/menu@2.0.0(patch_hash=a229d9974fc0a153eb7fae34599cc51171f2ab9a2bb79dc62f540db1ef592398)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)