Skip to content

Commit 54283a9

Browse files
perf(mobile): thread composer, route screen and queue control compile with the React Compiler
ThreadComposer, ThreadRouteScreen, ThreadQueueControl and use-thread-composer-state. try/finally became .finally() on the async body, the route screen's inspector-toggle ref is written in a layout effect, and the queue drag handle reads its latest props through effect events. Manual useMemo/useCallback/memo are gone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent d10932f commit 54283a9

5 files changed

Lines changed: 648 additions & 890 deletions

File tree

‎apps/mobile/src/features/threads/ThreadComposer.tsx‎

Lines changed: 100 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,7 @@ import {
2424
} from "@t3tools/shared/usageLimits";
2525
import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native";
2626
import type { ReactNode } from "react";
27-
import {
28-
memo,
29-
useCallback,
30-
useEffect,
31-
useLayoutEffect,
32-
useMemo,
33-
useRef,
34-
useState,
35-
type RefObject,
36-
} from "react";
27+
import { useEffect, useLayoutEffect, useRef, useState, type RefObject } from "react";
3728
import { Keyboard, Platform, Pressable, View, type ViewStyle } from "react-native";
3829
import { composerImageGallery } from "../../lib/composerImageGallery";
3930
import { filePreviewGallery } from "../../lib/filePreviewGallery";
@@ -332,7 +323,7 @@ export function ComposerSurface(props: {
332323
);
333324
}
334325

335-
export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposerProps) {
326+
export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps) {
336327
const project = useProject(scopeProjectRef(props.environmentId, props.selectedThread.projectId));
337328
const { themeVariables: materialTheme } = useAppearancePreferences();
338329
const composerPanel = materialTheme["--color-composer-panel"];
@@ -361,10 +352,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
361352
props.draftMessage.trim().length > 0 ||
362353
props.draftAttachments.length > 0 ||
363354
(queuedEdit?.existingAttachments.length ?? 0) > 0;
364-
const stripAttachments = useMemo(
365-
() => composerStripAttachments(props.draftAttachments),
366-
[props.draftAttachments],
367-
);
355+
const stripAttachments = composerStripAttachments(props.draftAttachments);
368356
const showStopAction = !hasContent && props.canStopThread && queuedEdit === null;
369357

370358
const uploadStates = useAtomValue(composerAttachmentUploadsAtom);
@@ -394,14 +382,14 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
394382
currentModelSelection,
395383
props.selectedThread.projectId,
396384
);
397-
const selectedProviderStatus = useMemo(() => {
385+
const selectedProviderStatus = (() => {
398386
if (!props.serverConfig) return null;
399387
return (
400388
props.serverConfig.providers.find(
401389
(p) => p.instanceId === props.selectedThread.modelSelection.instanceId,
402390
) ?? null
403391
);
404-
}, [props.serverConfig, props.selectedThread.modelSelection.instanceId]);
392+
})();
405393
const composerOwnerKey = scopedThreadKey(props.environmentId, props.selectedThread.id);
406394
const composerDraftKey = props.draftKey ?? composerOwnerKey;
407395
const openDraftDocument = (attachment: ComposerDocumentAttachment) => {
@@ -424,7 +412,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
424412
props.serverConfig?.providers ?? [],
425413
props.serverConfig?.usageLimitSources ?? [],
426414
);
427-
const openUsageLimits = useCallback(() => {
415+
const openUsageLimits = () => {
428416
const report = collectProviderUsageLimits(
429417
currentModelSelection.instanceId,
430418
props.serverConfig?.providers ?? [],
@@ -436,7 +424,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
436424
Dialog.alert("Usage limits unavailable", "This provider does not currently report limits.");
437425
}
438426
return report !== null;
439-
}, [currentModelSelection.instanceId, onShowUsageLimits, props.serverConfig]);
427+
};
440428

441429
const composerMenu = useComposerCommandMenu({
442430
draftMessage: props.draftMessage,
@@ -504,174 +492,126 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
504492
onExpandedChange?.(isExpanded);
505493
}, [isExpanded, onExpandedChange]);
506494

507-
const onPressPreview = useCallback(
508-
(source: FilePreviewSource) => {
509-
wasExpandedBeforePreviewRef.current = isFocused;
510-
setPreviewVideo(null);
511-
setPreviewFile(
512-
(current) =>
513-
current ?? {
514-
...source,
515-
gallery: filePreviewGallery(source, composerImageGallery(props.draftAttachments)),
516-
},
517-
);
518-
},
519-
[isFocused, props.draftAttachments],
520-
);
495+
const onPressPreview = (source: FilePreviewSource) => {
496+
wasExpandedBeforePreviewRef.current = isFocused;
497+
setPreviewVideo(null);
498+
setPreviewFile(
499+
(current) =>
500+
current ?? {
501+
...source,
502+
gallery: filePreviewGallery(source, composerImageGallery(props.draftAttachments)),
503+
},
504+
);
505+
};
521506

522-
const closePreview = useCallback(() => {
507+
const closePreview = () => {
523508
setPreviewFile(null);
524509
setPreviewVideo(null);
525510
if (wasExpandedBeforePreviewRef.current) {
526511
setTimeout(() => {
527512
if (navigation.isFocused()) inputRef.current?.focus();
528513
}, 100);
529514
}
530-
}, [inputRef, navigation]);
515+
};
531516

532-
const onPressVideo = useCallback(
533-
(attachment: DraftComposerFileAttachment, sourceIdentifier: string) => {
534-
wasExpandedBeforePreviewRef.current = isFocused;
535-
setPreviewFile(null);
536-
setPreviewVideo((current) => current ?? { type: "local", attachment, sourceIdentifier });
537-
},
538-
[isFocused],
539-
);
517+
const onPressVideo = (attachment: DraftComposerFileAttachment, sourceIdentifier: string) => {
518+
wasExpandedBeforePreviewRef.current = isFocused;
519+
setPreviewFile(null);
520+
setPreviewVideo((current) => current ?? { type: "local", attachment, sourceIdentifier });
521+
};
540522

541523
const onEditorFocusChange = props.onEditorFocusChange;
542-
const handleFocus = useCallback(() => {
524+
const handleFocus = () => {
543525
setIsFocused(true);
544526
onExpandedChange?.(true);
545527
onEditorFocusChange?.(true);
546-
}, [onEditorFocusChange, onExpandedChange]);
528+
};
547529

548-
const handleBlur = useCallback(() => {
530+
const handleBlur = () => {
549531
setIsFocused(false);
550532
if (!settingsSheetPresentation.keepsComposerExpanded) {
551533
onExpandedChange?.(false);
552534
}
553535
onEditorFocusChange?.(false);
554-
}, [onEditorFocusChange, onExpandedChange, settingsSheetPresentation.keepsComposerExpanded]);
555-
const handleSend = useCallback(
556-
async (followUp?: ActiveTurnComposerAction) => {
557-
if (voiceInput.blocksSubmission || pendingPastedTextAttachmentCountRef.current > 0) return;
558-
if (
559-
usageLimitsOffered &&
560-
isUsageLimitsCommand(props.draftMessage) &&
561-
props.draftAttachments.length === 0
562-
) {
563-
if (openUsageLimits()) onChangeDraftMessage("");
536+
};
537+
const handleSend = async (followUp?: ActiveTurnComposerAction) => {
538+
if (voiceInput.blocksSubmission || pendingPastedTextAttachmentCountRef.current > 0) return;
539+
if (
540+
usageLimitsOffered &&
541+
isUsageLimitsCommand(props.draftMessage) &&
542+
props.draftAttachments.length === 0
543+
) {
544+
if (openUsageLimits()) onChangeDraftMessage("");
545+
return;
546+
}
547+
const threadKey = scopedThreadKey(props.environmentId, props.selectedThread.id);
548+
if (inFlightThreadIdsRef.current.has(threadKey)) return;
549+
inFlightThreadIdsRef.current.add(threadKey);
550+
return await (async () => {
551+
const messageId = await onSendMessage(followUp);
552+
if (messageId === null) {
564553
return;
565554
}
566-
const threadKey = scopedThreadKey(props.environmentId, props.selectedThread.id);
567-
if (inFlightThreadIdsRef.current.has(threadKey)) return;
568-
inFlightThreadIdsRef.current.add(threadKey);
569-
try {
570-
const messageId = await onSendMessage(followUp);
571-
if (messageId === null) {
572-
return;
573-
}
574-
// Armed after the send so its preference read and native Activity start don't contend
575-
// with the queued-message feedback on the tap frame.
576-
armAgentAwarenessLiveActivityForLocalWork({
577-
environmentId: props.environmentId,
578-
threadTitle: props.selectedThread.title,
579-
projectTitle: props.environmentLabel ?? "T3 Code",
580-
});
581-
} finally {
582-
inFlightThreadIdsRef.current.delete(threadKey);
583-
}
584-
},
585-
[
586-
props.draftMessage,
587-
props.draftAttachments.length,
588-
onChangeDraftMessage,
589-
openUsageLimits,
590-
usageLimitsOffered,
591-
onSendMessage,
592-
props.environmentId,
593-
props.environmentLabel,
594-
props.selectedThread.id,
595-
props.selectedThread.title,
596-
voiceInput.blocksSubmission,
597-
],
598-
);
555+
// Armed after the send so its preference read and native Activity start don't contend
556+
// with the queued-message feedback on the tap frame.
557+
armAgentAwarenessLiveActivityForLocalWork({
558+
environmentId: props.environmentId,
559+
threadTitle: props.selectedThread.title,
560+
projectTitle: props.environmentLabel ?? "T3 Code",
561+
});
562+
})().finally(() => {
563+
inFlightThreadIdsRef.current.delete(threadKey);
564+
});
565+
};
599566

600567
const lockedProviderInstanceId = props.canSwitchProvider
601568
? undefined
602569
: currentModelSelection.instanceId;
603-
const modelOptions = useMemo(
604-
() =>
605-
buildModelOptions(
606-
props.serverConfig,
607-
currentModelSelection,
608-
lockedProviderInstanceId,
609-
props.selectedThread.projectId,
610-
),
611-
[
612-
props.serverConfig,
613-
currentModelSelection,
614-
lockedProviderInstanceId,
615-
props.selectedThread.projectId,
616-
],
570+
const modelOptions = buildModelOptions(
571+
props.serverConfig,
572+
currentModelSelection,
573+
lockedProviderInstanceId,
574+
props.selectedThread.projectId,
617575
);
618-
const threadProviderGroups = useMemo(() => groupByProvider(modelOptions), [modelOptions]);
576+
const threadProviderGroups = groupByProvider(modelOptions);
619577
const currentModelOption =
620578
modelOptions.find(
621579
(option) =>
622580
option.selection.instanceId === currentModelSelection.instanceId &&
623581
option.selection.model === currentModelSelection.model,
624582
) ?? null;
625-
const providerOptionDescriptors = useMemo(
626-
() =>
627-
resolveProviderOptionDescriptors({
628-
capabilities: currentModelOption?.capabilities,
629-
selections: currentModelSelection.options,
630-
}),
631-
[currentModelOption?.capabilities, currentModelSelection.options],
632-
);
583+
const providerOptionDescriptors = resolveProviderOptionDescriptors({
584+
capabilities: currentModelOption?.capabilities,
585+
selections: currentModelSelection.options,
586+
});
633587
const settingsOwnerId = composerOwnerKey;
634-
const settingsRouteSession = useMemo<ExistingThreadSettingsRouteSession>(
635-
() => ({
636-
ownerId: settingsOwnerId,
637-
environmentId: props.environmentId,
638-
projectId: props.selectedThread.projectId,
639-
providerInstanceId: currentModelSelection.instanceId,
640-
...(lockedProviderInstanceId ? { lockedProviderInstanceId } : {}),
641-
providerGroups: threadProviderGroups,
642-
selectedModel: currentModelSelection,
643-
reportedModelSelection: props.reportedModelSelection,
644-
onSelectModel: (option) =>
645-
props.onUpdateModelSelection(withRememberedModelOptions(option.selection)),
646-
optionDescriptors: providerOptionDescriptors,
647-
onUpdateOptionSelections: (options) => {
648-
rememberModelOptions(
649-
currentModelSelection.instanceId,
650-
currentModelSelection.model,
651-
options ?? [],
652-
);
653-
props.onUpdateModelSelection({ ...currentModelSelection, options });
654-
},
655-
runtimeMode: currentRuntimeMode,
656-
onUpdateRuntimeMode: props.onUpdateRuntimeMode,
657-
}),
658-
[
659-
currentModelSelection,
660-
props.reportedModelSelection,
661-
currentRuntimeMode,
662-
props.onUpdateModelSelection,
663-
props.onUpdateRuntimeMode,
664-
providerOptionDescriptors,
665-
settingsOwnerId,
666-
lockedProviderInstanceId,
667-
threadProviderGroups,
668-
props.selectedThread.projectId,
669-
],
670-
);
671-
const openSettings = useCallback(() => {
588+
const settingsRouteSession = {
589+
ownerId: settingsOwnerId,
590+
environmentId: props.environmentId,
591+
projectId: props.selectedThread.projectId,
592+
providerInstanceId: currentModelSelection.instanceId,
593+
...(lockedProviderInstanceId ? { lockedProviderInstanceId } : {}),
594+
providerGroups: threadProviderGroups,
595+
selectedModel: currentModelSelection,
596+
reportedModelSelection: props.reportedModelSelection,
597+
onSelectModel: (option) =>
598+
props.onUpdateModelSelection(withRememberedModelOptions(option.selection)),
599+
optionDescriptors: providerOptionDescriptors,
600+
onUpdateOptionSelections: (options) => {
601+
rememberModelOptions(
602+
currentModelSelection.instanceId,
603+
currentModelSelection.model,
604+
options ?? [],
605+
);
606+
props.onUpdateModelSelection({ ...currentModelSelection, options });
607+
},
608+
runtimeMode: currentRuntimeMode,
609+
onUpdateRuntimeMode: props.onUpdateRuntimeMode,
610+
} satisfies ExistingThreadSettingsRouteSession;
611+
const openSettings = () => {
672612
settingsRoutePresentation.present(settingsRouteSession);
673613
settingsSheetPresentation.open();
674-
}, [settingsRoutePresentation.present, settingsRouteSession, settingsSheetPresentation.open]);
614+
};
675615

676616
useEffect(() => {
677617
if (settingsSheetPresentation.isActive) {
@@ -688,17 +628,15 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
688628
navigation.dispatch(StackActions.push("ThreadSettingsSheet"));
689629
}, [navigation, settingsSheetPresentation.isVisible]);
690630

691-
useFocusEffect(
692-
useCallback(() => {
693-
if (!settingsRoutePresentedRef.current) {
694-
return;
695-
}
631+
useFocusEffect(() => {
632+
if (!settingsRoutePresentedRef.current) {
633+
return;
634+
}
696635

697-
settingsRoutePresentedRef.current = false;
698-
settingsSheetPresentation.onDismissed();
699-
settingsRoutePresentation.clear(settingsOwnerId);
700-
}, [settingsOwnerId, settingsRoutePresentation.clear, settingsSheetPresentation.onDismissed]),
701-
);
636+
settingsRoutePresentedRef.current = false;
637+
settingsSheetPresentation.onDismissed();
638+
settingsRoutePresentation.clear(settingsOwnerId);
639+
});
702640

703641
useEffect(
704642
() =>
@@ -1140,4 +1078,4 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
11401078
<FilePreviewModal source={previewFile} onRequestClose={closePreview} />
11411079
</Animated.View>
11421080
);
1143-
});
1081+
};

0 commit comments

Comments
 (0)