Skip to content

Commit 80fbe24

Browse files
fix(mobile): port main's composer stabilization into the v2 thread screens
Round-4 reconciliation of #5986/#5988 with the v2 cutover files: - PendingUserInputCard adopts main's collapsible overlay redesign with the v2 RuntimeRequestId/responseCapability plumbing (dead provider processes still read-only the card) - ThreadFeed adopts the thread-feed-live-follow transition model, the user-scroll settle window, momentum handoff, and env-scoped feed keys while keeping the v2 nearListEnd layout gating - ThreadDetailScreen hides (not unmounts) the composer while a user-input request owns the slot; multi-select answers flow as arrays end to end (threadActivity toggle/build helpers + tests, ThreadUserInputQuestion) - ThreadComposer keeps the v2 canStopThread stop gate under main's onEditorFocusChange rename; standalone stop reuses the shared renderer - Restored the branch's steer stop/send tests alongside main's composer test suite Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f6b1f91 commit 80fbe24

12 files changed

Lines changed: 1105 additions & 212 deletions

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

Lines changed: 313 additions & 72 deletions
Large diffs are not rendered by default.

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

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
serializeComposerFileLink,
1515
type ComposerTrigger,
1616
} from "@t3tools/shared/composerTrigger";
17+
import * as Haptics from "expo-haptics";
1718
import type { ReactNode } from "react";
1819
import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
1920
import {
@@ -51,7 +52,7 @@ import {
5152
ComposerToolbarScroller,
5253
ComposerToolbarTrigger,
5354
} from "../../components/ComposerToolbarTrigger";
54-
import { ControlPill } from "../../components/ControlPill";
55+
import { ControlPill, ControlPillMenu } from "../../components/ControlPill";
5556
import { ProviderIcon } from "../../components/ProviderIcon";
5657
import type {
5758
DraftComposerAttachment,
@@ -60,9 +61,18 @@ import type {
6061
import { buildModelOptions, groupByProvider } from "../../lib/modelOptions";
6162
import { useScaledTextRole } from "../settings/appearance/useScaledTextRole";
6263
import type { RemoteClientConnectionState } from "../../lib/connection";
63-
import { resolveProviderOptionDescriptors } from "../../lib/providerOptions";
64+
import {
65+
insertRankedSearchResult,
66+
normalizeSearchQuery,
67+
scoreQueryMatch,
68+
} from "@t3tools/shared/searchRanking";
69+
import {
70+
applyProviderOptionSelection,
71+
resolveProviderOptionDescriptors,
72+
} from "../../lib/providerOptions";
6473
import { useComposerPathSearch } from "../../state/use-composer-path-search";
6574
import { ComposerCommandPopover, type ComposerCommandItem } from "./ComposerCommandPopover";
75+
import { buildThreadSettingsMenu } from "./thread-settings-menu";
6676
import { ThreadSettingsSheet, threadSettingsSummaryLabel } from "./ThreadSettingsSheet";
6777
import { useThreadSettingsSheetPresentation } from "./use-thread-settings-sheet-presentation";
6878

@@ -113,6 +123,8 @@ export interface ThreadComposerProps {
113123
readonly onUpdateInteractionMode: (interactionMode: ProviderInteractionMode) => void;
114124
readonly onReconnectEnvironment: () => void;
115125
readonly onExpandedChange?: (expanded: boolean) => void;
126+
/** Fires on editor focus/blur; hosts use it to vet stale keyboard state. */
127+
readonly onEditorFocusChange?: (focused: boolean) => void;
116128
}
117129

118130
/**
@@ -308,14 +320,16 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
308320
}
309321
}, [inputRef]);
310322

323+
const onEditorFocusChange = props.onEditorFocusChange;
311324
const handleFocus = useCallback(() => {
312325
setIsFocused(true);
313-
}, []);
326+
onEditorFocusChange?.(true);
327+
}, [onEditorFocusChange]);
314328

315329
const handleBlur = useCallback(() => {
316330
setIsFocused(false);
317-
onExpandedChange?.(false);
318-
}, [onExpandedChange]);
331+
onEditorFocusChange?.(false);
332+
}, [onEditorFocusChange]);
319333
const showStopAction = props.canStopThread;
320334

321335
const sendLabel =
@@ -562,6 +576,61 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
562576
interactionMode: currentInteractionMode,
563577
});
564578

579+
// iOS gets a native menu on the trigger pill: the everyday adjustments
580+
// apply without resigning the keyboard, while "All Settings…" (and the
581+
// Android trigger) still route through the sheet, which must dismiss it.
582+
const settingsMenu = useMemo(
583+
() =>
584+
Platform.OS === "ios"
585+
? buildThreadSettingsMenu({
586+
providerGroups: threadProviderGroups,
587+
selectedModel: currentModelSelection,
588+
optionDescriptors: providerOptionDescriptors,
589+
runtimeMode: currentRuntimeMode,
590+
})
591+
: null,
592+
[threadProviderGroups, currentModelSelection, providerOptionDescriptors, currentRuntimeMode],
593+
);
594+
595+
const onUpdateModelSelection = props.onUpdateModelSelection;
596+
const onUpdateRuntimeMode = props.onUpdateRuntimeMode;
597+
const handleSettingsMenuAction = useCallback(
598+
(eventId: string) => {
599+
const event = settingsMenu?.events.get(eventId);
600+
if (!event) {
601+
return;
602+
}
603+
switch (event.type) {
604+
case "select-model":
605+
void Haptics.selectionAsync();
606+
onUpdateModelSelection(event.option.selection);
607+
return;
608+
case "set-option": {
609+
const options = applyProviderOptionSelection(providerOptionDescriptors, {
610+
id: event.optionId,
611+
value: event.value,
612+
});
613+
if (options) {
614+
void Haptics.selectionAsync();
615+
onUpdateModelSelection({ ...currentModelSelection, options });
616+
}
617+
return;
618+
}
619+
case "set-runtime":
620+
void Haptics.selectionAsync();
621+
onUpdateRuntimeMode(event.mode);
622+
return;
623+
}
624+
},
625+
[
626+
currentModelSelection,
627+
onUpdateModelSelection,
628+
onUpdateRuntimeMode,
629+
providerOptionDescriptors,
630+
settingsMenu,
631+
],
632+
);
633+
565634
return (
566635
<Animated.View
567636
className="px-4"
@@ -732,15 +801,31 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
732801
onPress={() => void props.onPickDraftImages()}
733802
showChevron={false}
734803
/>
735-
<ComposerToolbarTrigger
736-
accessibilityLabel="Thread settings"
737-
iconNode={
738-
<ProviderIcon provider={currentModelOption?.providerDriver} size={16} />
739-
}
740-
label={settingsSummaryLabel}
741-
maxWidth={320}
742-
onPress={settingsSheetPresentation.open}
743-
/>
804+
{settingsMenu ? (
805+
<ControlPillMenu
806+
actions={settingsMenu.actions}
807+
onPressAction={({ nativeEvent }) => handleSettingsMenuAction(nativeEvent.event)}
808+
>
809+
<ComposerToolbarTrigger
810+
accessibilityLabel="Thread settings"
811+
iconNode={
812+
<ProviderIcon provider={currentModelOption?.providerDriver} size={16} />
813+
}
814+
label={settingsSummaryLabel}
815+
maxWidth={320}
816+
/>
817+
</ControlPillMenu>
818+
) : (
819+
<ComposerToolbarTrigger
820+
accessibilityLabel="Thread settings"
821+
iconNode={
822+
<ProviderIcon provider={currentModelOption?.providerDriver} size={16} />
823+
}
824+
label={settingsSummaryLabel}
825+
maxWidth={320}
826+
onPress={settingsSheetPresentation.open}
827+
/>
828+
)}
744829
{showStopAction ? (
745830
<ComposerToolbarButton
746831
accessibilityLabel="Stop"

0 commit comments

Comments
 (0)