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 {
102102 type ComposerSendPresentation,
103103} from "./composerSendPresentation";
104104import { ComposerCommandPopover } from "./ComposerCommandPopover";
105import { ComposerPopoverAnchor } from "./ComposerPopoverAnchor";
105106import { ComposerQueuedEditAttachments } from "./ComposerQueuedEdit";
106107import { useComposerCommandMenu } from "./use-composer-command-menu";
107108import {
......@@ -773,7 +774,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
773774 {!voiceInput.isBusy &&
774775 composerMenu.trigger &&
775776 (composerMenu.items.length > 0 || composerMenu.trigger.kind === "pull-request") ? (
776 <View className="absolute inset-x-0 bottom-full z-10 mb-2">
777 <ComposerPopoverAnchor>
777778 <ComposerCommandPopover
778779 items={composerMenu.items}
779780 triggerKind={composerMenu.trigger.kind}
......@@ -781,7 +782,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
781782 error={composerMenu.error}
782783 onSelect={composerMenu.onSelect}
783784 />
784 </View>
785 </ComposerPopoverAnchor>
785786 ) : null}
786787
787788 {selectedProviderStatus?.compatibilityAdvisory?.message &&
apps/mobile/src/features/threads/ThreadDetailScreen.tsx+247-245
......@@ -137,6 +137,7 @@ import {
137137 COMPOSER_TRANSITION_DURATION_MS,
138138 ThreadComposer,
139139} from "./ThreadComposer";
140import { ComposerPopoverHost } from "./ComposerPopoverHost";
140141import { ThreadFeed, type ThreadFeedHistoryControls } from "./ThreadFeed";
141142import { useThreadTurnSubagents } from "./ThreadAgentsSheet";
142143import { ComposerQueuedEditBanner } from "./ComposerQueuedEdit";
......@@ -559,6 +560,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
559560 const [collapsedUserInputRequestId, setCollapsedUserInputRequestId] =
560561 useState<RuntimeRequestId | null>(null);
561562 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";
562566 // The open /usage-limits panel for this thread, model and turn. Only the open
563567 // moment is stored: the rows read live provider data, so a redeemed reset
564568 // credit or refreshed probe shows through. Anything that spends quota closes
......@@ -1172,265 +1176,263 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
11721176 style={{ position: "absolute", bottom: 0, left: 0, right: 0, top: 0 }}
11731177 offset={{ closed: 0, opened: 0 }}
11741178 >
1175 {/* The fixed sticky host gives this bottom-anchored child a stable
1179 <ComposerPopoverHost hidden={composerSlotHidden}>
1180 {/* The fixed sticky host gives this bottom-anchored child a stable
11761181 coordinate space. Its top and height can then animate together
11771182 instead of the auto-sized host jumping to Yoga's destination. */}
1178 <Animated.View
1179 layout={COMPOSER_LAYOUT_TRANSITION}
1180 pointerEvents="box-none"
1181 style={[{ position: "absolute", bottom: 0, left: 0 }, composerWidthStyle]}
1182 >
1183 {/* No paddingTop here: the overlay's measured height becomes the
1183 <Animated.View
1184 layout={COMPOSER_LAYOUT_TRANSITION}
1185 pointerEvents="box-none"
1186 style={[{ position: "absolute", bottom: 0, left: 0 }, composerWidthStyle]}
1187 >
1188 {/* No paddingTop here: the overlay's measured height becomes the
11841189 list's bottom inset, so any padding above the pill/composer
11851190 pushes the resting content floor up by the same amount. */}
1186 <View ref={composerOverlayRef} onLayout={onComposerLayout} className="w-full">
1187 <FloatingWorkingControl
1188 colorScheme={isDarkMode ? "dark" : "light"}
1189 status={floatingStatus}
1190 lift={floatingControlLift}
1191 devicePreview={
1192 devicePreviews.length > 0
1193 ? { count: devicePreviews.length, onPress: openDevicePreview }
1194 : null
1195 }
1196 browserPreview={
1197 browserTabs.tabs.length > 0
1198 ? { count: browserTabs.tabs.length, onPress: () => openBrowserPreview() }
1199 : null
1200 }
1201 showScrollToEnd={showScrollToEndButton}
1202 onScrollToEnd={handleScrollToEnd}
1203 agents={agentsSegment}
1204 onOpenAgents={() => {
1205 Keyboard.dismiss();
1206 navigation.navigate("ThreadAgents", {
1207 environmentId: props.environmentId,
1208 threadId: props.selectedThread.id,
1209 });
1210 }}
1211 queuedCount={queuedCount}
1212 onOpenQueue={() => {
1213 Keyboard.dismiss();
1214 navigation.navigate("ThreadQueue", {
1215 environmentId: props.environmentId,
1216 threadId: props.selectedThread.id,
1217 });
1218 }}
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}
1191 <View ref={composerOverlayRef} onLayout={onComposerLayout} className="w-full">
1192 <FloatingWorkingControl
1193 colorScheme={isDarkMode ? "dark" : "light"}
1194 status={floatingStatus}
1195 lift={floatingControlLift}
1196 devicePreview={
1197 devicePreviews.length > 0
1198 ? { count: devicePreviews.length, onPress: openDevicePreview }
1199 : null
1200 }
1201 browserPreview={
1202 browserTabs.tabs.length > 0
1203 ? { count: browserTabs.tabs.length, onPress: () => openBrowserPreview() }
1204 : null
1205 }
1206 showScrollToEnd={showScrollToEndButton}
1207 onScrollToEnd={handleScrollToEnd}
1208 agents={agentsSegment}
1209 onOpenAgents={() => {
1210 Keyboard.dismiss();
1211 navigation.navigate("ThreadAgents", {
1212 environmentId: props.environmentId,
1213 threadId: props.selectedThread.id,
1214 });
1215 }}
1216 queuedCount={queuedCount}
1217 onOpenQueue={() => {
1218 Keyboard.dismiss();
1219 navigation.navigate("ThreadQueue", {
1220 environmentId: props.environmentId,
1221 threadId: props.selectedThread.id,
1222 });
1223 }}
12401224 />
1241 {props.feedbackSubmissions.map((submission) => (
1242 <ComposerFeedback
1243 key={submission.id}
1244 submission={submission}
1245 onDismiss={() => props.onDismissFeedback(submission.id)}
1225 <View className="w-full self-center" style={{ maxWidth: contentMaxWidth }}>
1226 {props.queuedRunEdit !== null ? (
1227 <Animated.View
1228 className="shrink-0"
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}
12461245 />
1247 ))}
1248 {composerError !== null ? (
1249 <Animated.View
1250 className="shrink-0"
1251 entering={FadeInDown.duration(180)}
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}
1246 {props.feedbackSubmissions.map((submission) => (
1247 <ComposerFeedback
1248 key={submission.id}
1249 submission={submission}
1250 onDismiss={() => props.onDismissFeedback(submission.id)}
12701251 />
1271 </Animated.View>
1272 ) : null}
1273 {props.creationState?.kind === "failed" ? (
1274 <Animated.View
1275 className="shrink-0 px-4"
1276 style={{ paddingBottom: composerBottomInset }}
1277 entering={FadeInDown.duration(220)}
1278 exiting={FadeOut.duration(140)}
1279 >
1280 <ThreadCreationFailedCard
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}
1252 ))}
1253 {composerError !== null ? (
1254 <Animated.View
1255 className="shrink-0"
1256 entering={FadeInDown.duration(180)}
1257 exiting={FadeOut.duration(120)}
1258 >
1259 <ComposerErrorNotice
1260 message={composerError}
1261 onDismiss={() => clearThreadComposerError(selectedThreadKey)}
13051262 />
1306 ) : null}
1307 {props.activePendingUserInput ? (
1308 <PendingUserInputCard
1309 canOperateThread={props.canOperateThread}
1310 pendingUserInput={props.activePendingUserInput}
1311 maxHeight={pendingUserInputMaxHeight}
1312 collapsed={userInputCollapsed}
1313 onToggleCollapsed={handleToggleUserInputCollapsed}
1314 onStopThread={props.onStopThread}
1315 cardProgress={userInputCardProgress}
1316 cardCoverage={userInputCardCoverage}
1317 onInputFocusChange={handleOwnedInputFocusChange}
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}
1263 </Animated.View>
1264 ) : null}
1265 {usageLimitsReport && activeUserInputRequestId === null ? (
1266 <Animated.View
1267 className="shrink-0 px-4 pb-3"
1268 entering={FadeInDown.duration(220)}
1269 exiting={FadeOut.duration(140)}
1270 >
1271 <ComposerUsageLimits
1272 report={usageLimitsReport}
1273 environmentId={props.environmentId}
1274 onClose={dismissUsageLimits}
13251275 />
1326 ) : null}
1327 </Animated.View>
1328 ) : null}
1329 </View>
1276 </Animated.View>
1277 ) : null}
1278 {props.creationState?.kind === "failed" ? (
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 the
1336 {/* Hidden (not unmounted) while a user-input request owns the
13321337 composer slot, so composer drafts and editor state survive.
13331338 A rejected creation has no thread to send to; the failure card
13341339 owns the slot instead. */}
1335 <View
1336 style={
1337 activeUserInputRequestId !== null || props.creationState?.kind === "failed"
1338 ? { display: "none" }
1339 : undefined
1340 }
1341 >
1342 {isProviderSubagent ? (
1343 <View
1344 className="self-center px-3 pt-1.5"
1345 style={{
1346 width: "100%",
1347 maxWidth: contentMaxWidth,
1348 paddingBottom: composerBottomInset + 6,
1349 }}
1350 >
1351 <ProviderSubagentBar
1352 provider={providerSubagentProvider ?? null}
1353 modelLabel={
1354 providerSubagentCatalogModel?.name ??
1355 formatModelSlugName(props.selectedThread.modelSelection.model)
1356 }
1357 effortLabel={formatModelSelectionEffort(
1358 props.selectedThread.modelSelection,
1359 providerSubagentProvider?.models,
1360 reportedModelSelection,
1361 )}
1362 status={props.providerSubagentStatus ?? null}
1363 onOpenParent={
1364 props.selectedThread.lineage.parentThreadId === null
1365 ? null
1366 : () =>
1367 navigation.navigate("Thread", {
1368 environmentId: String(props.environmentId),
1369 threadId: String(props.selectedThread.lineage.parentThreadId),
1370 })
1371 }
1372 />
1373 </View>
1374 ) : (
1375 <>
1376 <ThreadComposer
1377 canOperateThread={props.canOperateThread}
1378 reportedModelSelection={reportedModelSelection}
1379 editorRef={composerEditorRef}
1380 draftMessage={props.draftMessage}
1381 draftAttachments={props.draftAttachments}
1382 placeholder="Ask the repo agent, or run a command…"
1383 contentMaxWidth={contentMaxWidth}
1384 connectionState={props.connectionStateLabel}
1385 environmentLabel={props.environmentLabel}
1386 selectedThread={props.selectedThread}
1387 hasCompactableConversation={hasCompactableConversation && !props.isCompacting}
1388 serverConfig={props.serverConfig}
1389 queueCount={props.selectedThreadQueueCount}
1390 activeThreadBusy={props.activeThreadBusy}
1391 canStopThread={props.canStopThread}
1392 environmentId={props.environmentId}
1393 projectCwd={props.threadCwd ?? props.projectWorkspaceRoot}
1394 // Follow-ups typed during setup wait in the draft: queueing
1395 // them against a thread id the server may still reject
1396 // would strand them in the outbox.
1397 sendBlockedReason={
1398 props.creationState?.kind === "preparing" ? "Starting the task…" : null
1399 }
1400 draftKey={props.composerDraftKey ?? undefined}
1401 followUpBehavior={props.followUpBehavior}
1402 canSteerActiveTurn={props.canSteerActiveTurn}
1403 queuedEdit={
1404 props.queuedRunEdit === null
1405 ? null
1406 : {
1407 existingAttachments: props.queuedRunEdit.existingAttachments,
1408 saving: props.isSavingQueuedEdit,
1409 onRemoveExistingAttachment: props.onRemoveQueuedEditAttachment,
1410 }
1411 }
1412 bottomInset={composerBottomInset}
1413 onChangeDraftMessage={props.onChangeDraftMessage}
1414 onPickDraftMedia={props.onPickDraftMedia}
1415 onPickDraftFiles={props.onPickDraftFiles}
1416 onNativePasteImages={props.onNativePasteImages}
1417 onNativePasteText={props.onNativePasteText}
1418 onRemoveDraftImage={props.onRemoveDraftImage}
1419 onStopThread={props.onStopThread}
1420 onSendMessage={handleSendMessage}
1421 onShowUsageLimits={showUsageLimits}
1422 canSwitchProvider={props.canSwitchThreadProvider}
1423 onUpdateModelSelection={props.onUpdateThreadModelSelection}
1424 onUpdateRuntimeMode={props.onUpdateThreadRuntimeMode}
1425 onUpdateInteractionMode={props.onUpdateThreadInteractionMode}
1426 onExpandedChange={setComposerExpanded}
1427 onEditorFocusChange={handleComposerFocusChange}
1428 />
1429 </>
1430 )}
1340 <View style={composerSlotHidden ? { display: "none" } : undefined}>
1341 {isProviderSubagent ? (
1342 <View
1343 className="self-center px-3 pt-1.5"
1344 style={{
1345 width: "100%",
1346 maxWidth: contentMaxWidth,
1347 paddingBottom: composerBottomInset + 6,
1348 }}
1349 >
1350 <ProviderSubagentBar
1351 provider={providerSubagentProvider ?? null}
1352 modelLabel={
1353 providerSubagentCatalogModel?.name ??
1354 formatModelSlugName(props.selectedThread.modelSelection.model)
1355 }
1356 effortLabel={formatModelSelectionEffort(
1357 props.selectedThread.modelSelection,
1358 providerSubagentProvider?.models,
1359 reportedModelSelection,
1360 )}
1361 status={props.providerSubagentStatus ?? null}
1362 onOpenParent={
1363 props.selectedThread.lineage.parentThreadId === null
1364 ? null
1365 : () =>
1366 navigation.navigate("Thread", {
1367 environmentId: String(props.environmentId),
1368 threadId: String(props.selectedThread.lineage.parentThreadId),
1369 })
1370 }
1371 />
1372 </View>
1373 ) : (
1374 <>
1375 <ThreadComposer
1376 canOperateThread={props.canOperateThread}
1377 reportedModelSelection={reportedModelSelection}
1378 editorRef={composerEditorRef}
1379 draftMessage={props.draftMessage}
1380 draftAttachments={props.draftAttachments}
1381 placeholder="Ask the repo agent, or run a command…"
1382 contentMaxWidth={contentMaxWidth}
1383 connectionState={props.connectionStateLabel}
1384 environmentLabel={props.environmentLabel}
1385 selectedThread={props.selectedThread}
1386 hasCompactableConversation={
1387 hasCompactableConversation && !props.isCompacting
1388 }
1389 serverConfig={props.serverConfig}
1390 queueCount={props.selectedThreadQueueCount}
1391 activeThreadBusy={props.activeThreadBusy}
1392 canStopThread={props.canStopThread}
1393 environmentId={props.environmentId}
1394 projectCwd={props.threadCwd ?? props.projectWorkspaceRoot}
1395 // Follow-ups typed during setup wait in the draft: queueing
1396 // them against a thread id the server may still reject
1397 // would strand them in the outbox.
1398 sendBlockedReason={
1399 props.creationState?.kind === "preparing" ? "Starting the task…" : null
1400 }
1401 draftKey={props.composerDraftKey ?? undefined}
1402 followUpBehavior={props.followUpBehavior}
1403 canSteerActiveTurn={props.canSteerActiveTurn}
1404 queuedEdit={
1405 props.queuedRunEdit === null
1406 ? null
1407 : {
1408 existingAttachments: props.queuedRunEdit.existingAttachments,
1409 saving: props.isSavingQueuedEdit,
1410 onRemoveExistingAttachment: props.onRemoveQueuedEditAttachment,
1411 }
1412 }
1413 bottomInset={composerBottomInset}
1414 onChangeDraftMessage={props.onChangeDraftMessage}
1415 onPickDraftMedia={props.onPickDraftMedia}
1416 onPickDraftFiles={props.onPickDraftFiles}
1417 onNativePasteImages={props.onNativePasteImages}
1418 onNativePasteText={props.onNativePasteText}
1419 onRemoveDraftImage={props.onRemoveDraftImage}
1420 onStopThread={props.onStopThread}
1421 onSendMessage={handleSendMessage}
1422 onShowUsageLimits={showUsageLimits}
1423 canSwitchProvider={props.canSwitchThreadProvider}
1424 onUpdateModelSelection={props.onUpdateThreadModelSelection}
1425 onUpdateRuntimeMode={props.onUpdateThreadRuntimeMode}
1426 onUpdateInteractionMode={props.onUpdateThreadInteractionMode}
1427 onExpandedChange={setComposerExpanded}
1428 onEditorFocusChange={handleComposerFocusChange}
1429 />
1430 </>
1431 )}
1432 </View>
14311433 </View>
1432 </View>
1433 </Animated.View>
1434 </Animated.View>
1435 </ComposerPopoverHost>
14341436 </KeyboardStickyView>
14351437 ) : null}
14361438 </View>