diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 9cc804cccd0a..ddca044b0468 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -124,7 +124,8 @@ import { hydrateImagesFromPersisted, useComposerDraftStore, useComposerThreadDraft, - useEffectiveComposerModelState, + deriveEffectiveComposerModelState, + useComposerDraftModelState, } from "../../composerDraftStore"; import { MAX_STASH_ENTRIES, @@ -1764,24 +1765,37 @@ function useProviderWorkspaceSnapshotRefresh(input: { ); } +const NO_PROVIDER_MODELS: ServerProvider["models"] = []; + /** - * The provider instances the composer offers and which one is selected. - * A hook so its results reach the composer frozen: they are passed to plain - * functions across later hook calls, which would otherwise keep the compiler - * from caching them. + * The provider instances the composer offers, which one is selected, and the model and options + * the draft resolves to on it. The draft store is read first so every derivation follows the only + * hook here; in the composer body they would straddle its later hooks and lose their cache. */ -function useComposerProviderSelection( +function useComposerProviderModel( input: Omit[0], "entries"> & { readonly providerStatuses: ReadonlyArray; readonly settings: UnifiedSettings; readonly activeProjectId: ProjectId | null; + readonly threadRef: ScopedThreadRef | DraftId; + readonly threadModelSelection: ModelSelection | null | undefined; + readonly projectModelSelection: ModelSelection | null | undefined; + readonly promptInjectionState: ReturnType; }, ) { - // Kept: `providerInstanceEntries` is read by plain functions on both sides of - // `useEffectiveComposerModelState` and `useProviderWorkspaceSnapshotRefresh` (the - // `hasProviderSetup` lookup before, the model picker's `.find` after), so inline it loses its cache. - "use memo"; - const { providerStatuses, settings, activeProjectId, ...selection } = input; + const { + providerStatuses, + settings, + activeProjectId, + threadRef, + threadModelSelection, + projectModelSelection, + promptInjectionState, + candidateInstanceIds, + lockedProvider, + lockedInstanceId, + } = input; + const draftModelState = useComposerDraftModelState(threadRef); const providerInstanceEntries: ReadonlyArray = sortProviderInstanceEntries( applyProviderInstanceSettings( deriveProviderInstanceEntries(providerStatuses), @@ -1789,23 +1803,60 @@ function useComposerProviderSelection( activeProjectId, ), ); + const selection = resolveComposerProviderSelection({ + entries: providerInstanceEntries, + candidateInstanceIds, + lockedProvider, + lockedInstanceId, + }); + const selectedInstanceId = + selection.selectedProviderEntry?.instanceId ?? NO_PROVIDER_MODEL_SELECTION.instanceId; + const selectedProvider: ProviderDriverKind = + selection.selectedProviderEntry?.driverKind ?? selection.requestedDriverKind; + const modelState = deriveEffectiveComposerModelState({ + draft: draftModelState, + providers: providerStatuses, + selectedProvider, + selectedInstanceId, + threadModelSelection, + projectModelSelection, + settings, + }); + const selectedProviderModels = selection.selectedProviderEntry?.models ?? NO_PROVIDER_MODELS; + const composerProviderState = getComposerProviderState({ + provider: selectedProvider, + model: modelState.selectedModel, + models: selectedProviderModels, + promptInjectionState, + modelOptions: modelState.modelOptions?.[selectedInstanceId], + planModeEnabled: settings.planModeEnabled, + }); return { providerInstanceEntries, - ...resolveComposerProviderSelection({ ...selection, entries: providerInstanceEntries }), + ...selection, + selectedInstanceId, + selectedProvider, + selectedProviderModels, + composerModelOptions: modelState.modelOptions, + selectedModel: modelState.selectedModel, + composerProviderState, }; } -/** Splits the draft's images into pending SnapShots and standalone images. */ +/** + * Follows the SnapShots still animating into this draft and splits the draft's images into + * pending SnapShots and standalone images. + */ function useComposerImageGroups( - pendingSnapShotAnimations: ReturnType, composerDraftTarget: ScopedThreadRef | DraftId, composerImages: ReadonlyArray, composerPreviewAnnotations: ReadonlyArray<{ readonly id: string }>, ) { - // Kept: these arrays go through plain functions here and again ~2,700 lines later - // (`.some`, `.filter`, `.slice` in the resting layout), past dozens of hook calls. Inline, the - // compiler gives them no cache scope, and moving every later use above those hooks is not possible. - "use memo"; + const pendingSnapShotAnimations = useSyncExternalStore( + subscribeToPendingSnapShotAnimations, + getPendingSnapShotAnimations, + getPendingSnapShotAnimations, + ); const pendingSnapShotIds = pendingSnapShotAnimationIdsForTarget( pendingSnapShotAnimations, composerDraftTarget, @@ -1814,6 +1865,7 @@ function useComposerImageGroups( composerPreviewAnnotations.map((annotation) => annotation.id), ); return { + pendingSnapShotAnimations, pendingSnapShotIds, pendingSnapShotIdSet: new Set(pendingSnapShotIds), uncommittedSnapShotIds: pendingSnapShotIds.filter( @@ -1826,14 +1878,6 @@ function useComposerImageGroups( const NO_PROVIDER_SKILLS: ServerProvider["skills"] = []; const NO_PROVIDER_SLASH_COMMANDS: ServerProvider["slashCommands"] = []; -function useComposerProviderState(input: Parameters[0]) { - // Kept: inline, `composerProviderState`, `selectedModelSelection` and `providerInputInputs` lose - // their cache scope. The inputs come from `useEffectiveComposerModelState`, and the results are - // read past later hooks such as `useProviderWorkspaceSnapshotRefresh`. - "use memo"; - return getComposerProviderState(input); -} - export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) { const { composerDraftTarget, @@ -2015,22 +2059,13 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const composerPreviewAnnotations = composerDraft.previewAnnotations; const composerReviewComments = composerDraft.reviewComments; const composerThreadContexts = attachmentDraft.threadContexts; - const pendingSnapShotAnimations = useSyncExternalStore( - subscribeToPendingSnapShotAnimations, - getPendingSnapShotAnimations, - getPendingSnapShotAnimations, - ); const { + pendingSnapShotAnimations, pendingSnapShotIds, pendingSnapShotIdSet, uncommittedSnapShotIds, standaloneComposerImages, - } = useComposerImageGroups( - pendingSnapShotAnimations, - composerDraftTarget, - composerImages, - composerPreviewAnnotations, - ); + } = useComposerImageGroups(composerDraftTarget, composerImages, composerPreviewAnnotations); const nonPersistedComposerImageIds = attachmentDraft.nonPersistedImageIds; const uploadsByImageId = useAttachmentUploadStore((state) => state.uploadsByImageId); const openPrLink = useOpenPrLink(routeThreadRef); @@ -2224,13 +2259,22 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const { providerInstanceEntries, selectedProviderEntry, - requestedDriverKind, lockedContinuationGroupKey, unavailableProviderInstanceId, - } = useComposerProviderSelection({ + selectedInstanceId, + selectedProvider, + selectedProviderModels, + composerModelOptions, + selectedModel, + composerProviderState, + } = useComposerProviderModel({ providerStatuses, settings, activeProjectId, + threadRef: composerDraftTarget, + threadModelSelection: activeThreadModelSelection, + projectModelSelection: activeProjectDefaultModelSelection, + promptInjectionState: getComposerPromptInjectionState(prompt), candidateInstanceIds: [ composerDraft.activeProvider ?? null, activeThread?.runtime?.providerInstanceId, @@ -2241,8 +2285,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) lockedInstanceId: activeThread?.runtime?.providerInstanceId ?? activeThreadModelSelection?.instanceId, }); - const selectedInstanceId = - selectedProviderEntry?.instanceId ?? NO_PROVIDER_MODEL_SELECTION.instanceId; const noProviderAvailable = selectedProviderEntry === undefined && multipleModelSelections === null; const providerCatalogPending = noProviderAvailable && !providerCatalogKnown; @@ -2255,8 +2297,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) : undefined; const resolvedCompactDisabledReason = compactDisabledReason ?? (noProviderAvailable ? "Compacting is unavailable right now" : null); - const selectedProvider: ProviderDriverKind = - selectedProviderEntry?.driverKind ?? requestedDriverKind; const supportedRuntimeModes = selectedProviderEntry?.snapshot.supportedRuntimeModes; const compatibleRuntimeModeOptions = supportedRuntimeModes && supportedRuntimeModes.length > 0 @@ -2268,15 +2308,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ? runtimeMode : (compatibleRuntimeModeOptions[0]?.mode ?? runtimeMode); - const { modelOptions: composerModelOptions, selectedModel } = useEffectiveComposerModelState({ - threadRef: composerDraftTarget, - providers: providerStatuses, - selectedProvider, - selectedInstanceId, - threadModelSelection: activeThreadModelSelection, - projectModelSelection: activeProjectDefaultModelSelection, - settings, - }); const providerSendBlockReason = getAntigravitySendBlockReason( selectedProviderEntry?.snapshot, selectedModel, @@ -2301,19 +2332,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) environmentId, gitCwd, selectedProviderEntry, - selectedProviderStatus, - }); - const selectedProviderModels: ReadonlyArray = - selectedProviderEntry?.models ?? []; - - const composerPromptInjectionState = getComposerPromptInjectionState(prompt); - const composerProviderState = useComposerProviderState({ - provider: selectedProvider, - model: selectedModel, - models: selectedProviderModels, - promptInjectionState: composerPromptInjectionState, - modelOptions: composerModelOptions?.[selectedInstanceId], - planModeEnabled: settings.planModeEnabled, + selectedProviderStatus: selectedProviderStatus, }); const selectedPromptEffort = composerProviderState.promptEffort; diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 5ae941213898..76b86ae4cd46 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -4242,7 +4242,9 @@ export function useThreadHasUnsentDraft(threadRef: ScopedThreadRef): boolean { ); } -function useComposerDraftModelState(threadRef: ComposerThreadTarget): ComposerDraftModelState { +export function useComposerDraftModelState( + threadRef: ComposerThreadTarget, +): ComposerDraftModelState { return useComposerDraftStore( useShallow((state) => { const draft = getComposerDraftState(state, threadRef); @@ -4256,29 +4258,6 @@ function useComposerDraftModelState(threadRef: ComposerThreadTarget): ComposerDr ); } -export function useEffectiveComposerModelState(input: { - threadRef?: ComposerThreadTarget; - draftId?: DraftId; - providers: ReadonlyArray; - selectedProvider: ProviderDriverKind; - selectedInstanceId?: ProviderInstanceId | null | undefined; - threadModelSelection: ModelSelection | null | undefined; - projectModelSelection: ModelSelection | null | undefined; - settings: UnifiedSettings; -}): EffectiveComposerModelState { - const draft = useComposerDraftModelState(input.threadRef ?? input.draftId ?? DraftId.make("")); - - return deriveEffectiveComposerModelState({ - draft, - providers: input.providers, - selectedProvider: input.selectedProvider, - selectedInstanceId: input.selectedInstanceId, - threadModelSelection: input.threadModelSelection, - projectModelSelection: input.projectModelSelection, - settings: input.settings, - }); -} - export function markPromotedDraftThreadByRef(threadRef: ScopedThreadRef): void { const draftStore = useComposerDraftStore.getState(); for (const [draftId, draftThread] of Object.entries(draftStore.draftThreadsByThreadKey)) {