authorgravatar for 91240327+shivamhwp@users.noreply.github.comshivam <91240327+shivamhwp@users.noreply.github.com> 2026-10-08 02:03:57+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-08 02:03:57+05:30
log8a92ebe413dd9e4569724c94e74a1ed54675eff7
tree656c34a8f5eadb6b2e3f0ecd210626b75508f8cc
parent468ade30495c450ae4e731483187f77bc3c5e42b
signature Signed by PGP key B5690EEEBB952194

fix(mobile): Android composer picker scrolls past the first four rows (#15856)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

5 files changed, 376 insertions(+), 247 deletions(-)

apps/mobile/src/features/threads/ComposerPopoverAnchor.android.tsx created+70
...@@ -0,0 +1,70 @@
1import type { ReactNode } from "react";
2import { useEffect, useRef, useState } from "react";
3import { View, type ViewInstance } from "react-native";
4
5import { useComposerPopoverHost } from "./ComposerPopoverHost";
6
7const COMPOSER_GAP = 8;
8
9type Frame = { readonly x: number; readonly y: number; readonly width: number };
10
11/**
12 * Places a composer popover just above the composer. Android only delivers
13 * drags to views inside their parent's bounds, so a popover hung above the
14 * composer with `bottom-full` takes taps but its list never scrolls. Render it
15 * into ComposerPopoverHost instead, which shares the composer's keyboard
16 * transform, so only the composer's resting layout needs measuring.
17 */
18export function ComposerPopoverAnchor(props: { readonly children: ReactNode }) {
19 const host = useComposerPopoverHost();
20 const anchorRef = useRef<ViewInstance>(null);
21 const [frame, setFrame] = useState<Frame | null>(null);
22
23 const measure = () => {
24 const hostView = host?.hostRef.current;
25 if (!hostView) {
26 return;
27 }
28 anchorRef.current?.measureLayout(hostView, (x, y, width) => {
29 setFrame((current) =>
30 current?.x === x && current.y === y && current.width === width ? current : { x, y, width },
31 );
32 });
33 };
34
35 // No dependency array: the composer re-renders when its padding or expanded
36 // state changes, which moves it without resizing the anchor, so measure and
37 // re-project after every render.
38 useEffect(() => {
39 measure();
40 host?.setContent(
41 frame === null ? null : (
42 <View
43 pointerEvents="box-none"
44 className="absolute top-0 justify-end"
45 style={{ left: frame.x, width: frame.width, height: frame.y - COMPOSER_GAP }}
46 >
47 {props.children}
48 </View>
49 ),
50 );
51 });
52
53 const setHostContent = host?.setContent;
54 useEffect(() => () => setHostContent?.(null), [setHostContent]);
55
56 if (host === null) {
57 return <View className="absolute inset-x-0 bottom-full z-10 mb-2">{props.children}</View>;
58 }
59
60 // Covers the composer, so its onLayout fires whenever the composer resizes.
61 return (
62 <View
63 ref={anchorRef}
64 collapsable={false}
65 pointerEvents="none"
66 className="absolute inset-0"
67 onLayout={measure}
68 />
69 );
70}
apps/mobile/src/features/threads/ComposerPopoverAnchor.tsx created+7
...@@ -0,0 +1,7 @@
1import type { ReactNode } from "react";
2import { View } from "react-native";
3
4/** Places a composer popover just above the composer. */
5export function ComposerPopoverAnchor(props: { readonly children: ReactNode }) {
6 return <View className="absolute inset-x-0 bottom-full z-10 mb-2">{props.children}</View>;
7}
apps/mobile/src/features/threads/ComposerPopoverHost.tsx created+49
...@@ -0,0 +1,49 @@
1import type { ReactNode, RefObject } from "react";
2import { createContext, useContext, useMemo, useRef, useState } from "react";
3import { View, type ViewInstance } from "react-native";
4
5type ComposerPopoverHostValue = {
6 readonly hostRef: RefObject<ViewInstance | null>;
7 readonly setContent: (content: ReactNode) => void;
8 /** Changes when the host resizes, which moves the bottom-anchored composer. */
9 readonly layoutVersion: number;
10};
11
12const ComposerPopoverHostContext = createContext<ComposerPopoverHostValue | null>(null);
13
14export function useComposerPopoverHost() {
15 return useContext(ComposerPopoverHostContext);
16}
17
18/**
19 * Full-size layer around the composer for its popovers. Mount it inside the
20 * composer's KeyboardStickyView: popovers placed here move with the composer
21 * and sit inside a parent that covers them, so Android delivers their scroll
22 * gestures. It wraps the composer because measureLayout needs an ancestor.
23 * Pass `hidden` whenever the composer itself is hidden: popovers live outside
24 * the composer's subtree, so they don't hide with it.
25 */
26export function ComposerPopoverHost(props: {
27 readonly hidden: boolean;
28 readonly children: ReactNode;
29}) {
30 const hostRef = useRef<ViewInstance>(null);
31 const [content, setContent] = useState<ReactNode>(null);
32 const [layoutVersion, setLayoutVersion] = useState(0);
33 const value = useMemo(() => ({ hostRef, setContent, layoutVersion }), [layoutVersion]);
34
35 return (
36 <ComposerPopoverHostContext.Provider value={value}>
37 <View
38 ref={hostRef}
39 collapsable={false}
40 pointerEvents="box-none"
41 className="absolute inset-0"
42 onLayout={() => setLayoutVersion((version) => version + 1)}
43 >
44 {props.children}
45 {props.hidden ? null : content}
46 </View>
47 </ComposerPopoverHostContext.Provider>
48 );
49}
apps/mobile/src/features/threads/ThreadComposer.tsx+3-2
...@@ -102,6 +102,7 @@ import {...@@ -102,6 +102,7 @@ import {
102 type ComposerSendPresentation,102 type ComposerSendPresentation,
103} from "./composerSendPresentation";103} from "./composerSendPresentation";
104import { ComposerCommandPopover } from "./ComposerCommandPopover";104import { ComposerCommandPopover } from "./ComposerCommandPopover";
105import { ComposerPopoverAnchor } from "./ComposerPopoverAnchor";
105import { ComposerQueuedEditAttachments } from "./ComposerQueuedEdit";106import { ComposerQueuedEditAttachments } from "./ComposerQueuedEdit";
106import { useComposerCommandMenu } from "./use-composer-command-menu";107import { useComposerCommandMenu } from "./use-composer-command-menu";
107import {108import {
...@@ -773,7 +774,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer...@@ -773,7 +774,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
773 {!voiceInput.isBusy &&774 {!voiceInput.isBusy &&
774 composerMenu.trigger &&775 composerMenu.trigger &&
775 (composerMenu.items.length > 0 || composerMenu.trigger.kind === "pull-request") ? (776 (composerMenu.items.length > 0 || composerMenu.trigger.kind === "pull-request") ? (
776 <View className="absolute inset-x-0 bottom-full z-10 mb-2">777 <ComposerPopoverAnchor>
777 <ComposerCommandPopover778 <ComposerCommandPopover
778 items={composerMenu.items}779 items={composerMenu.items}
779 triggerKind={composerMenu.trigger.kind}780 triggerKind={composerMenu.trigger.kind}
...@@ -781,7 +782,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer...@@ -781,7 +782,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
781 error={composerMenu.error}782 error={composerMenu.error}
782 onSelect={composerMenu.onSelect}783 onSelect={composerMenu.onSelect}
783 />784 />
784 </View>785 </ComposerPopoverAnchor>
785 ) : null}786 ) : null}
786787
787 {selectedProviderStatus?.compatibilityAdvisory?.message &&788 {selectedProviderStatus?.compatibilityAdvisory?.message &&
apps/mobile/src/features/threads/ThreadDetailScreen.tsx+247-245
...@@ -137,6 +137,7 @@ import {...@@ -137,6 +137,7 @@ import {
137 COMPOSER_TRANSITION_DURATION_MS,137 COMPOSER_TRANSITION_DURATION_MS,
138 ThreadComposer,138 ThreadComposer,
139} from "./ThreadComposer";139} from "./ThreadComposer";
140import { ComposerPopoverHost } from "./ComposerPopoverHost";
140import { ThreadFeed, type ThreadFeedHistoryControls } from "./ThreadFeed";141import { ThreadFeed, type ThreadFeedHistoryControls } from "./ThreadFeed";
141import { useThreadTurnSubagents } from "./ThreadAgentsSheet";142import { useThreadTurnSubagents } from "./ThreadAgentsSheet";
142import { ComposerQueuedEditBanner } from "./ComposerQueuedEdit";143import { ComposerQueuedEditBanner } from "./ComposerQueuedEdit";
...@@ -559,6 +560,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread...@@ -559,6 +560,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
559 const [collapsedUserInputRequestId, setCollapsedUserInputRequestId] =560 const [collapsedUserInputRequestId, setCollapsedUserInputRequestId] =
560 useState<RuntimeRequestId | null>(null);561 useState<RuntimeRequestId | null>(null);
561 const activeUserInputRequestId = props.activePendingUserInput?.requestId ?? null;562 const activeUserInputRequestId = props.activePendingUserInput?.requestId ?? null;
563 // A pending user-input request or a failed creation owns the composer slot.
564 const composerSlotHidden =
565 activeUserInputRequestId !== null || props.creationState?.kind === "failed";
562 // The open /usage-limits panel for this thread, model and turn. Only the open566 // The open /usage-limits panel for this thread, model and turn. Only the open
563 // moment is stored: the rows read live provider data, so a redeemed reset567 // moment is stored: the rows read live provider data, so a redeemed reset
564 // credit or refreshed probe shows through. Anything that spends quota closes568 // credit or refreshed probe shows through. Anything that spends quota closes
...@@ -1172,265 +1176,263 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread...@@ -1172,265 +1176,263 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
1172 style={{ position: "absolute", bottom: 0, left: 0, right: 0, top: 0 }}1176 style={{ position: "absolute", bottom: 0, left: 0, right: 0, top: 0 }}
1173 offset={{ closed: 0, opened: 0 }}1177 offset={{ closed: 0, opened: 0 }}
1174 >1178 >
1175 {/* The fixed sticky host gives this bottom-anchored child a stable1179 <ComposerPopoverHost hidden={composerSlotHidden}>
1180 {/* The fixed sticky host gives this bottom-anchored child a stable
1176 coordinate space. Its top and height can then animate together1181 coordinate space. Its top and height can then animate together
1177 instead of the auto-sized host jumping to Yoga's destination. */}1182 instead of the auto-sized host jumping to Yoga's destination. */}
1178 <Animated.View1183 <Animated.View
1179 layout={COMPOSER_LAYOUT_TRANSITION}1184 layout={COMPOSER_LAYOUT_TRANSITION}
1180 pointerEvents="box-none"1185 pointerEvents="box-none"
1181 style={[{ position: "absolute", bottom: 0, left: 0 }, composerWidthStyle]}1186 style={[{ position: "absolute", bottom: 0, left: 0 }, composerWidthStyle]}
1182 >1187 >
1183 {/* No paddingTop here: the overlay's measured height becomes the1188 {/* No paddingTop here: the overlay's measured height becomes the
1184 list's bottom inset, so any padding above the pill/composer1189 list's bottom inset, so any padding above the pill/composer
1185 pushes the resting content floor up by the same amount. */}1190 pushes the resting content floor up by the same amount. */}
1186 <View ref={composerOverlayRef} onLayout={onComposerLayout} className="w-full">1191 <View ref={composerOverlayRef} onLayout={onComposerLayout} className="w-full">
1187 <FloatingWorkingControl1192 <FloatingWorkingControl
1188 colorScheme={isDarkMode ? "dark" : "light"}1193 colorScheme={isDarkMode ? "dark" : "light"}
1189 status={floatingStatus}1194 status={floatingStatus}
1190 lift={floatingControlLift}1195 lift={floatingControlLift}
1191 devicePreview={1196 devicePreview={
1192 devicePreviews.length > 01197 devicePreviews.length > 0
1193 ? { count: devicePreviews.length, onPress: openDevicePreview }1198 ? { count: devicePreviews.length, onPress: openDevicePreview }
1194 : null1199 : null
1195 }1200 }
1196 browserPreview={1201 browserPreview={
1197 browserTabs.tabs.length > 01202 browserTabs.tabs.length > 0
1198 ? { count: browserTabs.tabs.length, onPress: () => openBrowserPreview() }1203 ? { count: browserTabs.tabs.length, onPress: () => openBrowserPreview() }
1199 : null1204 : null
1200 }1205 }
1201 showScrollToEnd={showScrollToEndButton}1206 showScrollToEnd={showScrollToEndButton}
1202 onScrollToEnd={handleScrollToEnd}1207 onScrollToEnd={handleScrollToEnd}
1203 agents={agentsSegment}1208 agents={agentsSegment}
1204 onOpenAgents={() => {1209 onOpenAgents={() => {
1205 Keyboard.dismiss();1210 Keyboard.dismiss();
1206 navigation.navigate("ThreadAgents", {1211 navigation.navigate("ThreadAgents", {
1207 environmentId: props.environmentId,1212 environmentId: props.environmentId,
1208 threadId: props.selectedThread.id,1213 threadId: props.selectedThread.id,
1209 });1214 });
1210 }}1215 }}
1211 queuedCount={queuedCount}1216 queuedCount={queuedCount}
1212 onOpenQueue={() => {1217 onOpenQueue={() => {
1213 Keyboard.dismiss();1218 Keyboard.dismiss();
1214 navigation.navigate("ThreadQueue", {1219 navigation.navigate("ThreadQueue", {
1215 environmentId: props.environmentId,1220 environmentId: props.environmentId,
1216 threadId: props.selectedThread.id,1221 threadId: props.selectedThread.id,
1217 });1222 });
1218 }}1223 }}
1219 />
1220 <View className="w-full self-center" style={{ maxWidth: contentMaxWidth }}>
1221 {props.queuedRunEdit !== null ? (
1222 <Animated.View
1223 className="shrink-0"
1224 entering={FadeInDown.duration(180)}
1225 exiting={FadeOut.duration(120)}
1226 >
1227 <ComposerQueuedEditBanner
1228 saving={props.isSavingQueuedEdit}
1229 onCancel={() => {
1230 voiceInputSession.cancel(props.composerDraftKey);
1231 props.onCancelQueuedRunEdit();
1232 }}
1233 />
1234 </Animated.View>
1235 ) : null}
1236 <UsageLimitRecoveryCard
1237 key={props.selectedThread.latestRun?.runId}
1238 thread={props.selectedThread}
1239 environmentId={props.environmentId}
1240 />1224 />
1241 {props.feedbackSubmissions.map((submission) => (1225 <View className="w-full self-center" style={{ maxWidth: contentMaxWidth }}>
1242 <ComposerFeedback1226 {props.queuedRunEdit !== null ? (
1243 key={submission.id}1227 <Animated.View
1244 submission={submission}1228 className="shrink-0"
1245 onDismiss={() => props.onDismissFeedback(submission.id)}1229 entering={FadeInDown.duration(180)}
1230 exiting={FadeOut.duration(120)}
1231 >
1232 <ComposerQueuedEditBanner
1233 saving={props.isSavingQueuedEdit}
1234 onCancel={() => {
1235 voiceInputSession.cancel(props.composerDraftKey);
1236 props.onCancelQueuedRunEdit();
1237 }}
1238 />
1239 </Animated.View>
1240 ) : null}
1241 <UsageLimitRecoveryCard
1242 key={props.selectedThread.latestRun?.runId}
1243 thread={props.selectedThread}
1244 environmentId={props.environmentId}
1246 />1245 />
1247 ))}1246 {props.feedbackSubmissions.map((submission) => (
1248 {composerError !== null ? (1247 <ComposerFeedback
1249 <Animated.View1248 key={submission.id}
1250 className="shrink-0"1249 submission={submission}
1251 entering={FadeInDown.duration(180)}1250 onDismiss={() => props.onDismissFeedback(submission.id)}
1252 exiting={FadeOut.duration(120)}
1253 >
1254 <ComposerErrorNotice
1255 message={composerError}
1256 onDismiss={() => clearThreadComposerError(selectedThreadKey)}
1257 />
1258 </Animated.View>
1259 ) : null}
1260 {usageLimitsReport && activeUserInputRequestId === null ? (
1261 <Animated.View
1262 className="shrink-0 px-4 pb-3"
1263 entering={FadeInDown.duration(220)}
1264 exiting={FadeOut.duration(140)}
1265 >
1266 <ComposerUsageLimits
1267 report={usageLimitsReport}
1268 environmentId={props.environmentId}
1269 onClose={dismissUsageLimits}
1270 />1251 />
1271 </Animated.View>1252 ))}
1272 ) : null}1253 {composerError !== null ? (
1273 {props.creationState?.kind === "failed" ? (1254 <Animated.View
1274 <Animated.View1255 className="shrink-0"
1275 className="shrink-0 px-4"1256 entering={FadeInDown.duration(180)}
1276 style={{ paddingBottom: composerBottomInset }}1257 exiting={FadeOut.duration(120)}
1277 entering={FadeInDown.duration(220)}1258 >
1278 exiting={FadeOut.duration(140)}1259 <ComposerErrorNotice
1279 >1260 message={composerError}
1280 <ThreadCreationFailedCard1261 onDismiss={() => clearThreadComposerError(selectedThreadKey)}
1281 reason={props.creationState.reason}
1282 onEditTask={props.creationState.onEditTask}
1283 />
1284 </Animated.View>
1285 ) : null}
1286 {props.activePendingApproval || props.activePendingUserInput ? (
1287 <Animated.View
1288 className="shrink-0 gap-3 px-4 pb-3"
1289 // The questionnaire replaces the composer, so it must pad
1290 // the home indicator the composer normally covers.
1291 style={
1292 activeUserInputRequestId !== null
1293 ? { paddingBottom: composerBottomInset }
1294 : undefined
1295 }
1296 entering={FadeInDown.duration(220)}
1297 exiting={FadeOut.duration(140)}
1298 >
1299 {props.activePendingApproval ? (
1300 <PendingApprovalCard
1301 canOperateThread={props.canOperateThread}
1302 approval={props.activePendingApproval}
1303 respondingApprovalId={props.respondingApprovalId}
1304 onRespond={props.onRespondToApproval}
1305 />1262 />
1306 ) : null}1263 </Animated.View>
1307 {props.activePendingUserInput ? (1264 ) : null}
1308 <PendingUserInputCard1265 {usageLimitsReport && activeUserInputRequestId === null ? (
1309 canOperateThread={props.canOperateThread}1266 <Animated.View
1310 pendingUserInput={props.activePendingUserInput}1267 className="shrink-0 px-4 pb-3"
1311 maxHeight={pendingUserInputMaxHeight}1268 entering={FadeInDown.duration(220)}
1312 collapsed={userInputCollapsed}1269 exiting={FadeOut.duration(140)}
1313 onToggleCollapsed={handleToggleUserInputCollapsed}1270 >
1314 onStopThread={props.onStopThread}1271 <ComposerUsageLimits
1315 cardProgress={userInputCardProgress}1272 report={usageLimitsReport}
1316 cardCoverage={userInputCardCoverage}1273 environmentId={props.environmentId}
1317 onInputFocusChange={handleOwnedInputFocusChange}1274 onClose={dismissUsageLimits}
1318 drafts={props.activePendingUserInputDrafts}
1319 answers={props.activePendingUserInputAnswers}
1320 respondingUserInputId={props.respondingUserInputId}
1321 onSelectOption={props.onSelectUserInputOption}
1322 onChangeCustomAnswer={props.onChangeUserInputCustomAnswer}
1323 onSubmit={props.onSubmitUserInput}
1324 onDismiss={props.onDismissUserInput}
1325 />1275 />
1326 ) : null}1276 </Animated.View>
1327 </Animated.View>1277 ) : null}
1328 ) : null}1278 {props.creationState?.kind === "failed" ? (
1329 </View>1279 <Animated.View
1280 className="shrink-0 px-4"
1281 style={{ paddingBottom: composerBottomInset }}
1282 entering={FadeInDown.duration(220)}
1283 exiting={FadeOut.duration(140)}
1284 >
1285 <ThreadCreationFailedCard
1286 reason={props.creationState.reason}
1287 onEditTask={props.creationState.onEditTask}
1288 />
1289 </Animated.View>
1290 ) : null}
1291 {props.activePendingApproval || props.activePendingUserInput ? (
1292 <Animated.View
1293 className="shrink-0 gap-3 px-4 pb-3"
1294 // The questionnaire replaces the composer, so it must pad
1295 // the home indicator the composer normally covers.
1296 style={
1297 activeUserInputRequestId !== null
1298 ? { paddingBottom: composerBottomInset }
1299 : undefined
1300 }
1301 entering={FadeInDown.duration(220)}
1302 exiting={FadeOut.duration(140)}
1303 >
1304 {props.activePendingApproval ? (
1305 <PendingApprovalCard
1306 canOperateThread={props.canOperateThread}
1307 approval={props.activePendingApproval}
1308 respondingApprovalId={props.respondingApprovalId}
1309 onRespond={props.onRespondToApproval}
1310 />
1311 ) : null}
1312 {props.activePendingUserInput ? (
1313 <PendingUserInputCard
1314 canOperateThread={props.canOperateThread}
1315 pendingUserInput={props.activePendingUserInput}
1316 maxHeight={pendingUserInputMaxHeight}
1317 collapsed={userInputCollapsed}
1318 onToggleCollapsed={handleToggleUserInputCollapsed}
1319 onStopThread={props.onStopThread}
1320 cardProgress={userInputCardProgress}
1321 cardCoverage={userInputCardCoverage}
1322 onInputFocusChange={handleOwnedInputFocusChange}
1323 drafts={props.activePendingUserInputDrafts}
1324 answers={props.activePendingUserInputAnswers}
1325 respondingUserInputId={props.respondingUserInputId}
1326 onSelectOption={props.onSelectUserInputOption}
1327 onChangeCustomAnswer={props.onChangeUserInputCustomAnswer}
1328 onSubmit={props.onSubmitUserInput}
1329 onDismiss={props.onDismissUserInput}
1330 />
1331 ) : null}
1332 </Animated.View>
1333 ) : null}
1334 </View>
13301335
1331 {/* Hidden (not unmounted) while a user-input request owns the1336 {/* Hidden (not unmounted) while a user-input request owns the
1332 composer slot, so composer drafts and editor state survive.1337 composer slot, so composer drafts and editor state survive.
1333 A rejected creation has no thread to send to; the failure card1338 A rejected creation has no thread to send to; the failure card
1334 owns the slot instead. */}1339 owns the slot instead. */}
1335 <View1340 <View style={composerSlotHidden ? { display: "none" } : undefined}>
1336 style={1341 {isProviderSubagent ? (
1337 activeUserInputRequestId !== null || props.creationState?.kind === "failed"1342 <View
1338 ? { display: "none" }1343 className="self-center px-3 pt-1.5"
1339 : undefined1344 style={{
1340 }1345 width: "100%",
1341 >1346 maxWidth: contentMaxWidth,
1342 {isProviderSubagent ? (1347 paddingBottom: composerBottomInset + 6,
1343 <View1348 }}
1344 className="self-center px-3 pt-1.5"1349 >
1345 style={{1350 <ProviderSubagentBar
1346 width: "100%",1351 provider={providerSubagentProvider ?? null}
1347 maxWidth: contentMaxWidth,1352 modelLabel={
1348 paddingBottom: composerBottomInset + 6,1353 providerSubagentCatalogModel?.name ??
1349 }}1354 formatModelSlugName(props.selectedThread.modelSelection.model)
1350 >1355 }
1351 <ProviderSubagentBar1356 effortLabel={formatModelSelectionEffort(
1352 provider={providerSubagentProvider ?? null}1357 props.selectedThread.modelSelection,
1353 modelLabel={1358 providerSubagentProvider?.models,
1354 providerSubagentCatalogModel?.name ??1359 reportedModelSelection,
1355 formatModelSlugName(props.selectedThread.modelSelection.model)1360 )}
1356 }1361 status={props.providerSubagentStatus ?? null}
1357 effortLabel={formatModelSelectionEffort(1362 onOpenParent={
1358 props.selectedThread.modelSelection,1363 props.selectedThread.lineage.parentThreadId === null
1359 providerSubagentProvider?.models,1364 ? null
1360 reportedModelSelection,1365 : () =>
1361 )}1366 navigation.navigate("Thread", {
1362 status={props.providerSubagentStatus ?? null}1367 environmentId: String(props.environmentId),
1363 onOpenParent={1368 threadId: String(props.selectedThread.lineage.parentThreadId),
1364 props.selectedThread.lineage.parentThreadId === null1369 })
1365 ? null1370 }
1366 : () =>1371 />
1367 navigation.navigate("Thread", {1372 </View>
1368 environmentId: String(props.environmentId),1373 ) : (
1369 threadId: String(props.selectedThread.lineage.parentThreadId),1374 <>
1370 })1375 <ThreadComposer
1371 }1376 canOperateThread={props.canOperateThread}
1372 />1377 reportedModelSelection={reportedModelSelection}
1373 </View>1378 editorRef={composerEditorRef}
1374 ) : (1379 draftMessage={props.draftMessage}
1375 <>1380 draftAttachments={props.draftAttachments}
1376 <ThreadComposer1381 placeholder="Ask the repo agent, or run a command…"
1377 canOperateThread={props.canOperateThread}1382 contentMaxWidth={contentMaxWidth}
1378 reportedModelSelection={reportedModelSelection}1383 connectionState={props.connectionStateLabel}
1379 editorRef={composerEditorRef}1384 environmentLabel={props.environmentLabel}
1380 draftMessage={props.draftMessage}1385 selectedThread={props.selectedThread}
1381 draftAttachments={props.draftAttachments}1386 hasCompactableConversation={
1382 placeholder="Ask the repo agent, or run a command…"1387 hasCompactableConversation && !props.isCompacting
1383 contentMaxWidth={contentMaxWidth}1388 }
1384 connectionState={props.connectionStateLabel}1389 serverConfig={props.serverConfig}
1385 environmentLabel={props.environmentLabel}1390 queueCount={props.selectedThreadQueueCount}
1386 selectedThread={props.selectedThread}1391 activeThreadBusy={props.activeThreadBusy}
1387 hasCompactableConversation={hasCompactableConversation && !props.isCompacting}1392 canStopThread={props.canStopThread}
1388 serverConfig={props.serverConfig}1393 environmentId={props.environmentId}
1389 queueCount={props.selectedThreadQueueCount}1394 projectCwd={props.threadCwd ?? props.projectWorkspaceRoot}
1390 activeThreadBusy={props.activeThreadBusy}1395 // Follow-ups typed during setup wait in the draft: queueing
1391 canStopThread={props.canStopThread}1396 // them against a thread id the server may still reject
1392 environmentId={props.environmentId}1397 // would strand them in the outbox.
1393 projectCwd={props.threadCwd ?? props.projectWorkspaceRoot}1398 sendBlockedReason={
1394 // Follow-ups typed during setup wait in the draft: queueing1399 props.creationState?.kind === "preparing" ? "Starting the task…" : null
1395 // them against a thread id the server may still reject1400 }
1396 // would strand them in the outbox.1401 draftKey={props.composerDraftKey ?? undefined}
1397 sendBlockedReason={1402 followUpBehavior={props.followUpBehavior}
1398 props.creationState?.kind === "preparing" ? "Starting the task…" : null1403 canSteerActiveTurn={props.canSteerActiveTurn}
1399 }1404 queuedEdit={
1400 draftKey={props.composerDraftKey ?? undefined}1405 props.queuedRunEdit === null
1401 followUpBehavior={props.followUpBehavior}1406 ? null
1402 canSteerActiveTurn={props.canSteerActiveTurn}1407 : {
1403 queuedEdit={1408 existingAttachments: props.queuedRunEdit.existingAttachments,
1404 props.queuedRunEdit === null1409 saving: props.isSavingQueuedEdit,
1405 ? null1410 onRemoveExistingAttachment: props.onRemoveQueuedEditAttachment,
1406 : {1411 }
1407 existingAttachments: props.queuedRunEdit.existingAttachments,1412 }
1408 saving: props.isSavingQueuedEdit,1413 bottomInset={composerBottomInset}
1409 onRemoveExistingAttachment: props.onRemoveQueuedEditAttachment,1414 onChangeDraftMessage={props.onChangeDraftMessage}
1410 }1415 onPickDraftMedia={props.onPickDraftMedia}
1411 }1416 onPickDraftFiles={props.onPickDraftFiles}
1412 bottomInset={composerBottomInset}1417 onNativePasteImages={props.onNativePasteImages}
1413 onChangeDraftMessage={props.onChangeDraftMessage}1418 onNativePasteText={props.onNativePasteText}
1414 onPickDraftMedia={props.onPickDraftMedia}1419 onRemoveDraftImage={props.onRemoveDraftImage}
1415 onPickDraftFiles={props.onPickDraftFiles}1420 onStopThread={props.onStopThread}
1416 onNativePasteImages={props.onNativePasteImages}1421 onSendMessage={handleSendMessage}
1417 onNativePasteText={props.onNativePasteText}1422 onShowUsageLimits={showUsageLimits}
1418 onRemoveDraftImage={props.onRemoveDraftImage}1423 canSwitchProvider={props.canSwitchThreadProvider}
1419 onStopThread={props.onStopThread}1424 onUpdateModelSelection={props.onUpdateThreadModelSelection}
1420 onSendMessage={handleSendMessage}1425 onUpdateRuntimeMode={props.onUpdateThreadRuntimeMode}
1421 onShowUsageLimits={showUsageLimits}1426 onUpdateInteractionMode={props.onUpdateThreadInteractionMode}
1422 canSwitchProvider={props.canSwitchThreadProvider}1427 onExpandedChange={setComposerExpanded}
1423 onUpdateModelSelection={props.onUpdateThreadModelSelection}1428 onEditorFocusChange={handleComposerFocusChange}
1424 onUpdateRuntimeMode={props.onUpdateThreadRuntimeMode}1429 />
1425 onUpdateInteractionMode={props.onUpdateThreadInteractionMode}1430 </>
1426 onExpandedChange={setComposerExpanded}1431 )}
1427 onEditorFocusChange={handleComposerFocusChange}1432 </View>
1428 />
1429 </>
1430 )}
1431 </View>1433 </View>
1432 </View>1434 </Animated.View>
1433 </Animated.View>1435 </ComposerPopoverHost>
1434 </KeyboardStickyView>1436 </KeyboardStickyView>
1435 ) : null}1437 ) : null}
1436 </View>1438 </View>