Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 84 additions & 65 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ import {
hydrateImagesFromPersisted,
useComposerDraftStore,
useComposerThreadDraft,
useEffectiveComposerModelState,
deriveEffectiveComposerModelState,
useComposerDraftModelState,
} from "../../composerDraftStore";
import {
MAX_STASH_ENTRIES,
Expand Down Expand Up @@ -1764,48 +1765,98 @@ 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<Parameters<typeof resolveComposerProviderSelection>[0], "entries"> & {
readonly providerStatuses: ReadonlyArray<ServerProvider>;
readonly settings: UnifiedSettings;
readonly activeProjectId: ProjectId | null;
readonly threadRef: ScopedThreadRef | DraftId;
readonly threadModelSelection: ModelSelection | null | undefined;
readonly projectModelSelection: ModelSelection | null | undefined;
readonly promptInjectionState: ReturnType<typeof getComposerPromptInjectionState>;
},
) {
// 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<ProviderInstanceEntry> = sortProviderInstanceEntries(
applyProviderInstanceSettings(
deriveProviderInstanceEntries(providerStatuses),
settings,
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<typeof getPendingSnapShotAnimations>,
composerDraftTarget: ScopedThreadRef | DraftId,
composerImages: ReadonlyArray<ComposerImageAttachment>,
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,
Expand All @@ -1814,6 +1865,7 @@ function useComposerImageGroups(
composerPreviewAnnotations.map((annotation) => annotation.id),
);
return {
pendingSnapShotAnimations,
pendingSnapShotIds,
pendingSnapShotIdSet: new Set(pendingSnapShotIds),
uncommittedSnapShotIds: pendingSnapShotIds.filter(
Expand All @@ -1826,14 +1878,6 @@ function useComposerImageGroups(
const NO_PROVIDER_SKILLS: ServerProvider["skills"] = [];
const NO_PROVIDER_SLASH_COMMANDS: ServerProvider["slashCommands"] = [];

function useComposerProviderState(input: Parameters<typeof getComposerProviderState>[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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -2301,19 +2332,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
environmentId,
gitCwd,
selectedProviderEntry,
selectedProviderStatus,
});
const selectedProviderModels: ReadonlyArray<ServerProvider["models"][number]> =
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;
Expand Down
27 changes: 3 additions & 24 deletions apps/web/src/composerDraftStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -4256,29 +4258,6 @@ function useComposerDraftModelState(threadRef: ComposerThreadTarget): ComposerDr
);
}

export function useEffectiveComposerModelState(input: {
threadRef?: ComposerThreadTarget;
draftId?: DraftId;
providers: ReadonlyArray<ServerProvider>;
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)) {
Expand Down
Loading