diff --git a/apps/web/src/components/chat/ModelPickerContent.tsx b/apps/web/src/components/chat/ModelPickerContent.tsx
index 176351bcc31c..abfef9ac6315 100644
--- a/apps/web/src/components/chat/ModelPickerContent.tsx
+++ b/apps/web/src/components/chat/ModelPickerContent.tsx
@@ -10,7 +10,7 @@ import { useAtomValue } from "@effect/atom-react";
import { formatProviderUpdateRequiredNotice } from "@t3tools/client-runtime/providerUpdateRequiredModels";
import { useNavigate } from "@tanstack/react-router";
import { LegendList, type LegendListRef } from "@legendapp/list/react";
-import { memo, useMemo, useState, useCallback, useEffect, useLayoutEffect, useRef } from "react";
+import { memo, useState, useEffect, useEffectEvent, useLayoutEffect, useRef } from "react";
import { ChevronRightIcon } from "lucide-react";
import { ModelListRow } from "./ModelListRow";
import { ModelPickerSidebar } from "./ModelPickerSidebar";
@@ -156,166 +156,46 @@ function ModelListSeparator() {
return
;
}
-export const ModelPickerContent = memo(function ModelPickerContent(props: {
- environmentId: EnvironmentId | null;
- activeInstanceId: ProviderInstanceId;
- model: string;
- selectedModels?: ReadonlyArray<{ instanceId: ProviderInstanceId; model: string }>;
- onToggleModel?: (instanceId: ProviderInstanceId, model: string) => void;
- lockedProvider: ProviderDriverKind | null;
- lockedContinuationGroupKey?: string | null;
- instanceEntries: ReadonlyArray;
- keybindings?: ResolvedKeybindingsConfig;
- modelOptionsByInstance: ReadonlyMap>;
- terminalOpen: boolean;
- onRequestClose?: () => void;
- onOpenProviderSetup?: (instanceId: ProviderInstanceId) => void;
- getModelDisabledReason?: (instanceId: ProviderInstanceId, model: string) => string | null;
- onInstanceModelChange: (instanceId: ProviderInstanceId, model: string) => void;
-}) {
- const {
- keybindings: providedKeybindings,
- modelOptionsByInstance,
- instanceEntries,
- getModelDisabledReason,
- onInstanceModelChange,
- onToggleModel,
- } = props;
- const [searchQuery, setSearchQuery] = useState("");
- const [showTopScrollFade, setShowTopScrollFade] = useState(false);
- const [showBottomScrollFade, setShowBottomScrollFade] = useState(false);
- const searchInputRef = useRef(null);
- const modelListRef = useRef(null);
- const pickerContentRef = useRef(null);
- const highlightedModelKeyRef = useRef(null);
- const { favorites } = useEnvironmentModelPreferences(props.environmentId);
- const activeEntry = props.instanceEntries.find(
- (entry) => entry.instanceId === props.activeInstanceId,
- );
- const activeModel = resolveModelPickerSelectedModel({
- driverKind: activeEntry?.driverKind,
- model: props.model,
- options: modelOptionsByInstance.get(props.activeInstanceId) ?? [],
- });
- const activeModelSlug =
- activeModel?.slug ?? (props.model === ANTIGRAVITY_DEFAULT_MODEL ? "" : props.model);
- const activeModelKey = activeModelSlug
- ? modelPickerModelKey(props.activeInstanceId, activeModelSlug)
- : null;
- const selectedModelKeys = useMemo(
- () =>
- props.selectedModels?.map((selection) => {
- const entry = instanceEntries.find((entry) => entry.instanceId === selection.instanceId);
- const model = resolveModelPickerSelectedModel({
- driverKind: entry?.driverKind,
- model: selection.model,
- options: modelOptionsByInstance.get(selection.instanceId) ?? [],
- });
- return modelPickerModelKey(selection.instanceId, model?.slug ?? selection.model);
- }),
- [instanceEntries, modelOptionsByInstance, props.selectedModels],
- );
- const selectedModelKeySet = useMemo(
- () => new Set(selectedModelKeys ?? (activeModelKey ? [activeModelKey] : [])),
- [selectedModelKeys, activeModelKey],
- );
- const activeInstanceHasSelectableUnavailableModel =
- activeEntry !== undefined &&
- (modelOptionsByInstance.get(props.activeInstanceId) ?? []).some((option) =>
- shouldIncludeModelPickerOption({
- entry: activeEntry,
- option,
- activeInstanceId: props.activeInstanceId,
- activeModel: activeModelSlug,
- }),
- ) &&
- !isProviderInstancePickerReady(activeEntry);
- const activeInstanceNeedsSetup =
- props.onOpenProviderSetup !== undefined &&
- activeEntry !== undefined &&
- shouldOfferModelPickerSetup(
- activeEntry,
- modelOptionsByInstance.get(props.activeInstanceId) ?? [],
- );
- const [selectedInstanceId, setSelectedInstanceId] = useState(
- () => {
- if (
- props.lockedProvider !== null ||
- activeInstanceHasSelectableUnavailableModel ||
- activeInstanceNeedsSetup
- ) {
- return props.activeInstanceId;
- }
- return favorites.length > 0 ? "favorites" : props.activeInstanceId;
- },
- );
- const [expandedLegacyInstances, setExpandedLegacyInstances] = useState(
- () =>
- new Set(
- modelOptionsByInstance
- .get(props.activeInstanceId)
- ?.some((model) => model.slug === activeModelSlug && model.isLegacy)
- ? [props.activeInstanceId]
- : [],
- ),
- );
- const serverKeybindings = useAtomValue(primaryServerKeybindingsAtom);
- const keybindings = providedKeybindings ?? serverKeybindings;
- const navigate = useNavigate();
- const toggleFavorite = useToggleEnvironmentModelFavorite(props.environmentId);
-
- const focusSearchInput = useCallback(() => {
- searchInputRef.current?.focus({ preventScroll: true });
- }, []);
-
- const handleSelectInstance = useCallback(
- (instanceId: ProviderInstanceId | "favorites") => {
- setSelectedInstanceId(instanceId);
- window.requestAnimationFrame(() => {
- focusSearchInput();
- });
- },
- [focusSearchInput],
- );
-
- useLayoutEffect(() => {
- focusSearchInput();
- const frame = window.requestAnimationFrame(() => {
- focusSearchInput();
- });
- const timeout = window.setTimeout(() => {
- focusSearchInput();
- }, 0);
- return () => {
- window.cancelAnimationFrame(frame);
- window.clearTimeout(timeout);
- };
- }, [focusSearchInput]);
-
- const favoritesSet = useMemo(() => {
+// Every list below keys the virtualized list, the combobox or the shortcut listener, so
+// they are derived together and only rebuilt when an input changes.
+function useModelPickerLists(
+ favorites: ReturnType["favorites"],
+ instanceEntries: ReadonlyArray,
+ modelOptionsByInstance: ReadonlyMap>,
+ activeInstanceId: ProviderInstanceId,
+ activeModelSlug: string,
+ activeInstanceHasSelectableUnavailableModel: boolean,
+ lockedProvider: ProviderDriverKind | null,
+ lockedContinuationGroupKey: string | null | undefined,
+ hasProviderSetup: boolean,
+ searchQuery: string,
+ selectedInstanceId: ProviderInstanceId | "favorites",
+ expandedLegacyInstances: ReadonlySet,
+ getModelDisabledReason:
+ | ((instanceId: ProviderInstanceId, model: string) => string | null)
+ | undefined,
+) {
+ "use memo";
+ const favoritesSet = (() => {
return new Set(favorites.map((fav) => providerModelKey(fav.provider, fav.model)));
- }, [favorites]);
+ })();
- const entryByInstanceId = useMemo(
- () => new Map(instanceEntries.map((entry) => [entry.instanceId, entry])),
- [instanceEntries],
- );
- const matchesLockedProvider = useCallback(
- (entry: Pick): boolean => {
- if (props.lockedProvider === null) return true;
- if (entry.driverKind !== props.lockedProvider) return false;
- if (!props.lockedContinuationGroupKey) return true;
- return entry.continuationGroupKey === props.lockedContinuationGroupKey;
- },
- [props.lockedContinuationGroupKey, props.lockedProvider],
- );
+ const entryByInstanceId = new Map(instanceEntries.map((entry) => [entry.instanceId, entry]));
+ const matchesLockedProvider = (
+ entry: Pick,
+ ): boolean => {
+ if (lockedProvider === null) return true;
+ if (entry.driverKind !== lockedProvider) return false;
+ if (!lockedContinuationGroupKey) return true;
+ return entry.continuationGroupKey === lockedContinuationGroupKey;
+ };
- const selectableUnavailableInstanceIds = useMemo(() => {
+ const selectableUnavailableInstanceIds = (() => {
const instanceIds = new Set();
if (activeInstanceHasSelectableUnavailableModel) {
- instanceIds.add(props.activeInstanceId);
+ instanceIds.add(activeInstanceId);
}
- if (props.onOpenProviderSetup) {
+ if (hasProviderSetup) {
for (const entry of instanceEntries) {
if (
shouldOfferModelPickerSetup(entry, modelOptionsByInstance.get(entry.instanceId) ?? [])
@@ -325,15 +205,9 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
}
}
return instanceIds.size > 0 ? instanceIds : undefined;
- }, [
- activeInstanceHasSelectableUnavailableModel,
- instanceEntries,
- modelOptionsByInstance,
- props.activeInstanceId,
- props.onOpenProviderSetup,
- ]);
+ })();
- const flatModels = useMemo(() => {
+ const flatModels = (() => {
const out: ModelPickerItem[] = [];
for (const [instanceId, models] of modelOptionsByInstance) {
const entry = entryByInstanceId.get(instanceId);
@@ -345,7 +219,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
!shouldIncludeModelPickerOption({
entry,
option: model,
- activeInstanceId: props.activeInstanceId,
+ activeInstanceId: activeInstanceId,
activeModel: activeModelSlug,
})
) {
@@ -372,11 +246,11 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
}
}
return out;
- }, [modelOptionsByInstance, entryByInstanceId, props.activeInstanceId, activeModelSlug]);
+ })();
- const isLocked = props.lockedProvider !== null;
+ const isLocked = lockedProvider !== null;
const isSearching = searchQuery.trim().length > 0;
- const lockedDisabledInstanceIds = useMemo(() => {
+ const lockedDisabledInstanceIds = (() => {
if (!isLocked) {
return undefined;
}
@@ -387,8 +261,8 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
}
}
return disabled;
- }, [instanceEntries, isLocked, matchesLockedProvider]);
- const sidebarInstanceEntries = useMemo(() => {
+ })();
+ const sidebarInstanceEntries = (() => {
const enabledEntries = instanceEntries.filter(isProviderInstancePickerVisible);
if (!isLocked) {
return enabledEntries;
@@ -403,19 +277,16 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
}
}
return [...available, ...disabled];
- }, [instanceEntries, isLocked, matchesLockedProvider]);
+ })();
const showSidebar = !isSearching && sidebarInstanceEntries.length > 0;
- const instanceOrder = useMemo(
- () => instanceEntries.map((entry) => entry.instanceId),
- [instanceEntries],
- );
+ const instanceOrder = instanceEntries.map((entry) => entry.instanceId);
- const filteredModels = useMemo(() => {
+ const filteredModels = (() => {
let result = flatModels;
if (searchQuery.trim()) {
const candidates =
- props.lockedProvider !== null ? result.filter((m) => matchesLockedProvider(m)) : result;
+ lockedProvider !== null ? result.filter((m) => matchesLockedProvider(m)) : result;
return rankModelPickerSearch(candidates, searchQuery, (model) => ({
name: model.name,
...(model.shortName ? { shortName: model.shortName } : {}),
@@ -426,7 +297,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
}));
}
- if (props.lockedProvider !== null) {
+ if (lockedProvider !== null) {
result = result.filter((m) => matchesLockedProvider(m));
if (selectedInstanceId === "favorites") {
result = result.filter((m) => favoritesSet.has(providerModelKey(m.instanceId, m.slug)));
@@ -444,17 +315,9 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
groupFavorites: selectedInstanceId !== "favorites",
instanceOrder: selectedInstanceId === "favorites" ? instanceOrder : [],
});
- }, [
- favoritesSet,
- flatModels,
- instanceOrder,
- matchesLockedProvider,
- props.lockedProvider,
- searchQuery,
- selectedInstanceId,
- ]);
+ })();
- const legacySection = useMemo(() => {
+ const legacySection = (() => {
if (isSearching || selectedInstanceId === "favorites") {
return null;
}
@@ -469,9 +332,9 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
legacyModels,
isExpanded: expandedLegacyInstances.has(selectedInstanceId),
};
- }, [expandedLegacyInstances, filteredModels, isSearching, selectedInstanceId]);
+ })();
- const visibleModels = useMemo(() => {
+ const visibleModels = (() => {
if (!legacySection) {
return filteredModels;
}
@@ -479,12 +342,12 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
...legacySection.currentModels,
...(legacySection.isExpanded ? legacySection.legacyModels : []),
];
- }, [filteredModels, legacySection]);
+ })();
const selectedEntry =
selectedInstanceId === "favorites" ? undefined : entryByInstanceId.get(selectedInstanceId);
const providerSetupEntries =
- !isSearching && props.onOpenProviderSetup
+ !isSearching && hasProviderSetup
? instanceEntries.filter(
(entry) =>
matchesLockedProvider(entry) &&
@@ -509,50 +372,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
return notice ? [{ instanceId: entry.instanceId, notice }] : [];
});
- const toggleLegacySection = useCallback((instanceId: ProviderInstanceId) => {
- setExpandedLegacyInstances((expanded) => {
- const next = new Set(expanded);
- if (next.has(instanceId)) {
- next.delete(instanceId);
- } else {
- next.add(instanceId);
- }
- return next;
- });
- }, []);
-
- const handleModelSelect = useCallback(
- (modelSlug: string, instanceId: ProviderInstanceId, additive = false) => {
- if (getModelDisabledReason?.(instanceId, modelSlug)) {
- return;
- }
- const options = modelOptionsByInstance.get(instanceId);
- if (!options) {
- return;
- }
- const entry = entryByInstanceId.get(instanceId);
- if (!entry) {
- return;
- }
- const resolvedModel = resolveSelectableModel(entry.driverKind, modelSlug, options);
- if (resolvedModel) {
- if (additive && onToggleModel) {
- onToggleModel(instanceId, resolvedModel);
- } else {
- onInstanceModelChange(instanceId, resolvedModel);
- }
- }
- },
- [
- entryByInstanceId,
- getModelDisabledReason,
- modelOptionsByInstance,
- onInstanceModelChange,
- onToggleModel,
- ],
- );
-
- const modelJumpCommandByKey = useMemo(() => {
+ const modelJumpCommandByKey = (() => {
const mapping = new Map<
string,
NonNullable>
@@ -570,23 +390,17 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
selectableModelIndex += 1;
}
return mapping;
- }, [getModelDisabledReason, visibleModels]);
- const modelJumpModelKeys = useMemo(
- () => [...modelJumpCommandByKey.keys()],
- [modelJumpCommandByKey],
- );
- const allItemKeys = useMemo(
- (): string[] => [
- ...flatModels.map((model) => modelPickerModelKey(model.instanceId, model.slug)),
- ...new Set(
- flatModels
- .filter((model) => model.isLegacy)
- .map((model) => modelPickerLegacySectionKey(model.instanceId)),
- ),
- ],
- [flatModels],
- );
- const filteredItemKeys = useMemo((): string[] => {
+ })();
+ const modelJumpModelKeys = [...modelJumpCommandByKey.keys()];
+ const allItemKeys = ((): string[] => [
+ ...flatModels.map((model) => modelPickerModelKey(model.instanceId, model.slug)),
+ ...new Set(
+ flatModels
+ .filter((model) => model.isLegacy)
+ .map((model) => modelPickerLegacySectionKey(model.instanceId)),
+ ),
+ ])();
+ const filteredItemKeys = ((): string[] => {
const modelKeys = visibleModels.map((model) =>
modelPickerModelKey(model.instanceId, model.slug),
);
@@ -595,16 +409,238 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
}
modelKeys.splice(legacySection.currentModels.length, 0, legacySection.key);
return modelKeys;
- }, [legacySection, visibleModels]);
- const filteredModelByKey = useMemo(
- (): ReadonlyMap =>
- new Map(
- visibleModels.map(
- (model) => [modelPickerModelKey(model.instanceId, model.slug), model] as const,
- ),
+ })();
+ const filteredModelByKey = ((): ReadonlyMap =>
+ new Map(
+ visibleModels.map(
+ (model) => [modelPickerModelKey(model.instanceId, model.slug), model] as const,
),
- [visibleModels],
+ ))();
+ return {
+ favoritesSet,
+ entryByInstanceId,
+ matchesLockedProvider,
+ selectableUnavailableInstanceIds,
+ flatModels,
+ isLocked,
+ isSearching,
+ lockedDisabledInstanceIds,
+ sidebarInstanceEntries,
+ showSidebar,
+ instanceOrder,
+ filteredModels,
+ legacySection,
+ visibleModels,
+ selectedEntry,
+ providerSetupEntries,
+ updateRequiredNotices,
+ modelJumpCommandByKey,
+ modelJumpModelKeys,
+ allItemKeys,
+ filteredItemKeys,
+ filteredModelByKey,
+ };
+}
+
+export const ModelPickerContent = memo(function ModelPickerContent(props: {
+ environmentId: EnvironmentId | null;
+ activeInstanceId: ProviderInstanceId;
+ model: string;
+ selectedModels?: ReadonlyArray<{ instanceId: ProviderInstanceId; model: string }>;
+ onToggleModel?: (instanceId: ProviderInstanceId, model: string) => void;
+ lockedProvider: ProviderDriverKind | null;
+ lockedContinuationGroupKey?: string | null;
+ instanceEntries: ReadonlyArray;
+ keybindings?: ResolvedKeybindingsConfig;
+ modelOptionsByInstance: ReadonlyMap>;
+ terminalOpen: boolean;
+ onRequestClose?: () => void;
+ onOpenProviderSetup?: (instanceId: ProviderInstanceId) => void;
+ getModelDisabledReason?: (instanceId: ProviderInstanceId, model: string) => string | null;
+ onInstanceModelChange: (instanceId: ProviderInstanceId, model: string) => void;
+}) {
+ const {
+ keybindings: providedKeybindings,
+ modelOptionsByInstance,
+ instanceEntries,
+ getModelDisabledReason,
+ onInstanceModelChange,
+ onToggleModel,
+ } = props;
+ const [searchQuery, setSearchQuery] = useState("");
+ const [showTopScrollFade, setShowTopScrollFade] = useState(false);
+ const [showBottomScrollFade, setShowBottomScrollFade] = useState(false);
+ const searchInputRef = useRef(null);
+ const modelListRef = useRef(null);
+ const pickerContentRef = useRef(null);
+ const highlightedModelKeyRef = useRef(null);
+ const { favorites } = useEnvironmentModelPreferences(props.environmentId);
+ const activeEntry = props.instanceEntries.find(
+ (entry) => entry.instanceId === props.activeInstanceId,
);
+ const activeModel = resolveModelPickerSelectedModel({
+ driverKind: activeEntry?.driverKind,
+ model: props.model,
+ options: modelOptionsByInstance.get(props.activeInstanceId) ?? [],
+ });
+ const activeModelSlug =
+ activeModel?.slug ?? (props.model === ANTIGRAVITY_DEFAULT_MODEL ? "" : props.model);
+ const activeModelKey = activeModelSlug
+ ? modelPickerModelKey(props.activeInstanceId, activeModelSlug)
+ : null;
+ const selectedModelKeys = props.selectedModels?.map((selection) => {
+ const entry = instanceEntries.find((entry) => entry.instanceId === selection.instanceId);
+ const model = resolveModelPickerSelectedModel({
+ driverKind: entry?.driverKind,
+ model: selection.model,
+ options: modelOptionsByInstance.get(selection.instanceId) ?? [],
+ });
+ return modelPickerModelKey(selection.instanceId, model?.slug ?? selection.model);
+ });
+ const selectedModelKeySet = new Set(
+ selectedModelKeys ?? (activeModelKey ? [activeModelKey] : []),
+ );
+ const activeInstanceHasSelectableUnavailableModel =
+ activeEntry !== undefined &&
+ (modelOptionsByInstance.get(props.activeInstanceId) ?? []).some((option) =>
+ shouldIncludeModelPickerOption({
+ entry: activeEntry,
+ option,
+ activeInstanceId: props.activeInstanceId,
+ activeModel: activeModelSlug,
+ }),
+ ) &&
+ !isProviderInstancePickerReady(activeEntry);
+ const activeInstanceNeedsSetup =
+ props.onOpenProviderSetup !== undefined &&
+ activeEntry !== undefined &&
+ shouldOfferModelPickerSetup(
+ activeEntry,
+ modelOptionsByInstance.get(props.activeInstanceId) ?? [],
+ );
+ const [selectedInstanceId, setSelectedInstanceId] = useState(
+ () => {
+ if (
+ props.lockedProvider !== null ||
+ activeInstanceHasSelectableUnavailableModel ||
+ activeInstanceNeedsSetup
+ ) {
+ return props.activeInstanceId;
+ }
+ return favorites.length > 0 ? "favorites" : props.activeInstanceId;
+ },
+ );
+ const [expandedLegacyInstances, setExpandedLegacyInstances] = useState(
+ () =>
+ new Set(
+ modelOptionsByInstance
+ .get(props.activeInstanceId)
+ ?.some((model) => model.slug === activeModelSlug && model.isLegacy)
+ ? [props.activeInstanceId]
+ : [],
+ ),
+ );
+ const serverKeybindings = useAtomValue(primaryServerKeybindingsAtom);
+ const keybindings = providedKeybindings ?? serverKeybindings;
+ const navigate = useNavigate();
+ const toggleFavorite = useToggleEnvironmentModelFavorite(props.environmentId);
+
+ const focusSearchInput = () => {
+ searchInputRef.current?.focus({ preventScroll: true });
+ };
+
+ const handleSelectInstance = (instanceId: ProviderInstanceId | "favorites") => {
+ setSelectedInstanceId(instanceId);
+ window.requestAnimationFrame(() => {
+ focusSearchInput();
+ });
+ };
+
+ useLayoutEffect(() => {
+ focusSearchInput();
+ const frame = window.requestAnimationFrame(() => {
+ focusSearchInput();
+ });
+ const timeout = window.setTimeout(() => {
+ focusSearchInput();
+ }, 0);
+ return () => {
+ window.cancelAnimationFrame(frame);
+ window.clearTimeout(timeout);
+ };
+ }, [focusSearchInput]);
+
+ const {
+ favoritesSet,
+ entryByInstanceId,
+ selectableUnavailableInstanceIds,
+ isLocked,
+ isSearching,
+ lockedDisabledInstanceIds,
+ sidebarInstanceEntries,
+ showSidebar,
+ legacySection,
+ providerSetupEntries,
+ updateRequiredNotices,
+ modelJumpCommandByKey,
+ modelJumpModelKeys,
+ allItemKeys,
+ filteredItemKeys,
+ filteredModelByKey,
+ } = useModelPickerLists(
+ favorites,
+ instanceEntries,
+ modelOptionsByInstance,
+ props.activeInstanceId,
+ activeModelSlug,
+ activeInstanceHasSelectableUnavailableModel,
+ props.lockedProvider,
+ props.lockedContinuationGroupKey,
+ props.onOpenProviderSetup !== undefined,
+ searchQuery,
+ selectedInstanceId,
+ expandedLegacyInstances,
+ getModelDisabledReason,
+ );
+
+ const toggleLegacySection = (instanceId: ProviderInstanceId) => {
+ setExpandedLegacyInstances((expanded) => {
+ const next = new Set(expanded);
+ if (next.has(instanceId)) {
+ next.delete(instanceId);
+ } else {
+ next.add(instanceId);
+ }
+ return next;
+ });
+ };
+
+ const handleModelSelect = (
+ modelSlug: string,
+ instanceId: ProviderInstanceId,
+ additive = false,
+ ) => {
+ if (getModelDisabledReason?.(instanceId, modelSlug)) {
+ return;
+ }
+ const options = modelOptionsByInstance.get(instanceId);
+ if (!options) {
+ return;
+ }
+ const entry = entryByInstanceId.get(instanceId);
+ if (!entry) {
+ return;
+ }
+ const resolvedModel = resolveSelectableModel(entry.driverKind, modelSlug, options);
+ if (resolvedModel) {
+ if (additive && onToggleModel) {
+ onToggleModel(instanceId, resolvedModel);
+ } else {
+ onInstanceModelChange(instanceId, resolvedModel);
+ }
+ }
+ };
+
const [modelListContentSize, setModelListContentSize] = useState(
() => filteredItemKeys.length * MODEL_LIST_ESTIMATED_ITEM_SIZE,
);
@@ -615,7 +651,7 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
);
const modelListHeight =
filteredItemKeys.length === 0 ? 0 : `calc(${modelListContentSize}px + var(--spacing) * 5)`;
- const updateModelListScrollFades = useCallback(() => {
+ const updateModelListScrollFades = () => {
const scrollElement = modelListRef.current?.getScrollableNode();
if (!(scrollElement instanceof HTMLElement)) {
return;
@@ -623,17 +659,13 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
const maxScrollOffset = Math.max(0, scrollElement.scrollHeight - scrollElement.clientHeight);
setShowTopScrollFade(scrollElement.scrollTop > 1);
setShowBottomScrollFade(maxScrollOffset - scrollElement.scrollTop > 1);
- }, []);
- const modelJumpShortcutContext = useMemo(
- () =>
- ({
- terminalFocus: false,
- terminalOpen: props.terminalOpen,
- modelPickerOpen: true,
- }) as const,
- [props.terminalOpen],
- );
- const modelJumpLabelByKey = useMemo((): ReadonlyMap => {
+ };
+ const modelJumpShortcutContext = {
+ terminalFocus: false,
+ terminalOpen: props.terminalOpen,
+ modelPickerOpen: true,
+ } as const;
+ const modelJumpLabelByKey = ((): ReadonlyMap => {
if (modelJumpCommandByKey.size === 0) {
return EMPTY_MODEL_JUMP_LABELS;
}
@@ -649,84 +681,84 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
}
}
return mapping.size > 0 ? mapping : EMPTY_MODEL_JUMP_LABELS;
- }, [keybindings, modelJumpCommandByKey, modelJumpShortcutContext]);
- const modelListExtraData = useMemo(
- () => ({ favoritesSet, modelJumpLabelByKey, activeModelKey, selectedModelKeySet }),
- [favoritesSet, modelJumpLabelByKey, activeModelKey, selectedModelKeySet],
- );
+ })();
+ const modelListExtraData = {
+ favoritesSet,
+ modelJumpLabelByKey,
+ activeModelKey,
+ selectedModelKeySet,
+ };
- useEffect(() => {
- const onWindowKeyDown = (event: globalThis.KeyboardEvent) => {
- if (event.defaultPrevented || event.repeat || isCommandPaletteOpen()) {
- return;
- }
+ const onWindowKeyDown = useEffectEvent((event: globalThis.KeyboardEvent) => {
+ if (event.defaultPrevented || event.repeat || isCommandPaletteOpen()) {
+ return;
+ }
- const command = resolveShortcutCommand(event, keybindings, {
- platform: navigator.platform,
- context: modelJumpShortcutContext,
- });
- if (command === "modelPicker.previousProvider" || command === "modelPicker.nextProvider") {
- event.preventDefault();
- event.stopPropagation();
- const next = adjacentModelPickerProvider({
- entries: sidebarInstanceEntries,
- selectedInstanceId,
- direction: command === "modelPicker.nextProvider" ? 1 : -1,
- disabledInstanceIds: lockedDisabledInstanceIds,
- selectableUnavailableInstanceIds,
- });
- setSearchQuery("");
- handleSelectInstance(next);
- return;
- }
- const jumpIndex = modelPickerJumpIndexFromCommand(command ?? "");
- if (jumpIndex === null) {
- return;
- }
+ const command = resolveShortcutCommand(event, keybindings, {
+ platform: navigator.platform,
+ context: modelJumpShortcutContext,
+ });
+ if (command === "modelPicker.previousProvider" || command === "modelPicker.nextProvider") {
event.preventDefault();
event.stopPropagation();
+ const next = adjacentModelPickerProvider({
+ entries: sidebarInstanceEntries,
+ selectedInstanceId,
+ direction: command === "modelPicker.nextProvider" ? 1 : -1,
+ disabledInstanceIds: lockedDisabledInstanceIds,
+ selectableUnavailableInstanceIds,
+ });
+ setSearchQuery("");
+ handleSelectInstance(next);
+ return;
+ }
+ const jumpIndex = modelPickerJumpIndexFromCommand(command ?? "");
+ if (jumpIndex === null) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
- const targetModelKey = modelJumpModelKeys[jumpIndex];
- if (!targetModelKey) {
- return;
- }
- const model = parseModelPickerModelKey(targetModelKey);
- if (!model) {
- return;
- }
- handleModelSelect(model.slug, model.instanceId);
- };
-
- window.addEventListener("keydown", onWindowKeyDown, true);
-
+ const targetModelKey = modelJumpModelKeys[jumpIndex];
+ if (!targetModelKey) {
+ return;
+ }
+ const model = parseModelPickerModelKey(targetModelKey);
+ if (!model) {
+ return;
+ }
+ handleModelSelect(model.slug, model.instanceId);
+ });
+ useEffect(() => {
+ const listener = (event: globalThis.KeyboardEvent) => onWindowKeyDown(event);
+ window.addEventListener("keydown", listener, true);
return () => {
- window.removeEventListener("keydown", onWindowKeyDown, true);
+ window.removeEventListener("keydown", listener, true);
};
- }, [
- handleModelSelect,
- handleSelectInstance,
- keybindings,
- lockedDisabledInstanceIds,
- modelJumpModelKeys,
- modelJumpShortcutContext,
- selectableUnavailableInstanceIds,
- selectedInstanceId,
- sidebarInstanceEntries,
- ]);
+ }, []);
- useLayoutEffect(() => {
+ // Keyed by content: a new disabled-reason closure rebuilds an identical key array.
+ const filteredItemListKey = JSON.stringify(filteredItemKeys);
+ // A new list starts from the top until the measurement below lands.
+ const [fadesResetFor, setFadesResetFor] = useState(filteredItemListKey);
+ if (fadesResetFor !== filteredItemListKey) {
+ setFadesResetFor(filteredItemListKey);
setShowTopScrollFade(false);
setShowBottomScrollFade(filteredItemKeys.length > 5);
+ }
+ const measureScrollFades = useEffectEvent(updateModelListScrollFades);
+ useLayoutEffect(() => {
let nestedFrame = 0;
const frame = window.requestAnimationFrame(() => {
- updateModelListScrollFades();
- nestedFrame = window.requestAnimationFrame(updateModelListScrollFades);
+ measureScrollFades();
+ nestedFrame = window.requestAnimationFrame(() => measureScrollFades());
});
return () => {
window.cancelAnimationFrame(frame);
window.cancelAnimationFrame(nestedFrame);
};
- }, [filteredItemKeys, updateModelListScrollFades]);
+ // oxlint-disable-next-line react/exhaustive-effect-dependencies -- A new list re-measures its scroll fades once it has laid out.
+ }, [filteredItemListKey]);
return (