From a32f1b5e9ec333fa88ec054cc04581012fe36f2a Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 28 Sep 2026 15:59:57 +0200 Subject: [PATCH] ui : rework the selector around its providers The selector keeps working when a provider is down: the trigger shows the provider's mark and the org avatar, the banner only appears when no backend is enabled, and the model ids read through their settings. The model mark becomes MODEL_ICON everywhere. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- .../ChatFormActionModels.svelte | 35 ++- .../ChatFormPickerCommand.svelte | 4 +- .../ChatScreen/ChatScreenServerError.svelte | 13 +- tools/ui/src/lib/constants/icons.constants.ts | 3 - .../lib/constants/settings-keys.constants.ts | 4 +- .../src/lib/constants/settings.constants.ts | 30 +- .../lib/hooks/use-models-selector.svelte.ts | 264 ++++++++++++++---- .../lib/hooks/use-reasoning-menu.svelte.ts | 4 +- .../src/lib/stores/settings/index.svelte.ts | 27 +- .../a11y/ChatScreenForm.a11y.stories.svelte | 21 +- 10 files changed, 307 insertions(+), 98 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte index a4baa40bf9..434db0cf4f 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte @@ -1,6 +1,14 @@ -{#if hasError} +{#if hasError || isLoadingModel}
{#if isLoadingModel} diff --git a/tools/ui/src/lib/constants/icons.constants.ts b/tools/ui/src/lib/constants/icons.constants.ts index 71741c6957..38de4e0cdc 100644 --- a/tools/ui/src/lib/constants/icons.constants.ts +++ b/tools/ui/src/lib/constants/icons.constants.ts @@ -31,9 +31,6 @@ export const DEFAULT_FILE_ICON = FileIcon; /** The mark for a model, wherever one is listed. */ export const MODEL_ICON = ModelIcon; -/** Alias kept for the selector until it adopts MODEL_ICON. */ -export const MODEL_SELECTOR_ICON = ModelIcon; - export const MODALITY_ICONS = { [ModelModality.AUDIO]: AudioIcon, [ModelModality.VIDEO]: VideoIcon, diff --git a/tools/ui/src/lib/constants/settings-keys.constants.ts b/tools/ui/src/lib/constants/settings-keys.constants.ts index 7727370bde..574c906de2 100644 --- a/tools/ui/src/lib/constants/settings-keys.constants.ts +++ b/tools/ui/src/lib/constants/settings-keys.constants.ts @@ -41,6 +41,7 @@ export const SETTINGS_KEYS = { MCP_SERVERS: 'mcpServers', MENTION_SEARCH_MAX_DEPTH: 'mentionSearchMaxDepth', MIN_P: 'min_p', + // Display PASTE_LONG_TEXT_TO_FILE_LEN: 'pasteLongTextToFileLen', PDF_AS_IMAGE: 'pdfAsImage', // Performance @@ -58,7 +59,8 @@ export const SETTINGS_KEYS = { SHOW_FULL_PATH_IN_MENTIONS: 'showFullPathInMentions', // Display SHOW_MESSAGE_STATS: 'showMessageStats', - SHOW_MODEL_ORG_NAME_IN_TRIGGER: 'showModelOrgNameInTrigger', + SHOW_MODEL_CAPABILITIES_IN_SELECTOR: 'showModelCapabilitiesInSelector', + SHOW_MODEL_ORG_NAME: 'showModelOrgName', SHOW_MODEL_QUANTIZATION: 'showModelQuantization', SHOW_MODEL_TAGS: 'showModelTags', SHOW_RAW_MODEL_NAMES: 'showRawModelNames', diff --git a/tools/ui/src/lib/constants/settings.constants.ts b/tools/ui/src/lib/constants/settings.constants.ts index 27d20c9243..2d5cb4afec 100644 --- a/tools/ui/src/lib/constants/settings.constants.ts +++ b/tools/ui/src/lib/constants/settings.constants.ts @@ -28,6 +28,7 @@ import type { /** Settings sections — slug is the routing identity, title is the display label. */ export const SETTINGS_SECTIONS = { AGENTIC: { slug: 'agentic', title: 'Agentic' }, + BACKENDS: { slug: 'backends', title: 'Providers' }, DEVELOPER: { slug: 'developer', title: 'Developer' }, DISPLAY: { slug: 'display', title: 'Display' }, GENERAL: { slug: 'general', title: 'General' }, @@ -38,6 +39,7 @@ export const SETTINGS_SECTIONS = { export const SETTINGS_SECTION_SLUGS = { AGENTIC: SETTINGS_SECTIONS.AGENTIC.slug, + BACKENDS: SETTINGS_SECTIONS.BACKENDS.slug, DEVELOPER: SETTINGS_SECTIONS.DEVELOPER.slug, DISPLAY: SETTINGS_SECTIONS.DISPLAY.slug, GENERAL: SETTINGS_SECTIONS.GENERAL.slug, @@ -48,6 +50,7 @@ export const SETTINGS_SECTION_SLUGS = { export const SETTINGS_SECTION_TITLES = { AGENTIC: SETTINGS_SECTIONS.AGENTIC.title, + BACKENDS: SETTINGS_SECTIONS.BACKENDS.title, DEVELOPER: SETTINGS_SECTIONS.DEVELOPER.title, DISPLAY: SETTINGS_SECTIONS.DISPLAY.title, GENERAL: SETTINGS_SECTIONS.GENERAL.title, @@ -291,9 +294,16 @@ export const SETTINGS_REGISTRY: SettingsSectionEntry[] = [ }, { defaultValue: false, - help: 'Display the organization name in the model selector trigger button.', - key: SETTINGS_KEYS.SHOW_MODEL_ORG_NAME_IN_TRIGGER, - label: 'Show organization name in model selector trigger', + help: 'Show the modality and capability icons (vision, audio, reasoning, tools) on the models selector rows.', + key: SETTINGS_KEYS.SHOW_MODEL_CAPABILITIES_IN_SELECTOR, + label: 'Show model modalities & capabilities in models selector', + type: SettingsFieldType.CHECKBOX + }, + { + defaultValue: true, + help: 'Display the organization name alongside model names in the model selector, in the trigger and in the model rows.', + key: SETTINGS_KEYS.SHOW_MODEL_ORG_NAME, + label: 'Show organization name in model selector', type: SettingsFieldType.CHECKBOX }, { @@ -303,19 +313,19 @@ export const SETTINGS_REGISTRY: SettingsSectionEntry[] = [ label: 'Show build version information', type: SettingsFieldType.CHECKBOX }, - { - defaultValue: true, - help: 'Group models into families (e.g. "Qwen") in the model picker and the models manager. Turn it off to list every model on its own.', - key: SETTINGS_KEYS.GROUP_MODELS_BY_FAMILY, - label: 'Group models by family', - type: SettingsFieldType.CHECKBOX - }, { defaultValue: false, help: 'Display the full file system path inside file and folder @-mention badges instead of just the file or folder name.', key: SETTINGS_KEYS.SHOW_FULL_PATH_IN_MENTIONS, label: 'Show full path in mentions', type: SettingsFieldType.CHECKBOX + }, + { + defaultValue: true, + help: 'Group models into families (e.g. "Qwen") in the model picker and the models manager. Turn it off to list every model on its own.', + key: SETTINGS_KEYS.GROUP_MODELS_BY_FAMILY, + label: 'Group models by family', + type: SettingsFieldType.CHECKBOX } ], slug: SETTINGS_SECTION_SLUGS.DISPLAY, diff --git a/tools/ui/src/lib/hooks/use-models-selector.svelte.ts b/tools/ui/src/lib/hooks/use-models-selector.svelte.ts index 7d2770a261..38c5f3b42b 100644 --- a/tools/ui/src/lib/hooks/use-models-selector.svelte.ts +++ b/tools/ui/src/lib/hooks/use-models-selector.svelte.ts @@ -1,14 +1,35 @@ -import { filterModelOptions, groupModelOptions } from '$lib/components/app/models/utils'; -import { CHAT_INPUT_FOCUS_SELECTOR } from '$lib/constants'; -import { modelsStore, serverStore } from '$lib/stores'; +import type { ModelItem } from '$lib/components/app/navigation/utils'; +import { + filterModelOptions, + groupFavoriteOptions, + groupModelOptions, + groupProviderOptions +} from '$lib/components/app/navigation/utils'; +import { + CHAT_INPUT_FOCUS_SELECTOR, + LOCAL_BACKEND_ID, + REMOTE_PROVIDER_MODEL_LIMIT +} from '$lib/constants'; +import { backendsModelsStore, backendsStore, modelsStore, serverStore, uiStore } from '$lib/stores'; import type { ModelOption } from '$lib/types/models'; +import { getBackend } from '$lib/utils/api-base'; +import { getBackendCapabilities } from '$lib/utils/backend'; +import { rawModelId } from '$lib/utils/model-option-id'; import { onMount } from 'svelte'; +import { SvelteSet } from 'svelte/reactivity'; + +/** Groups of the favorites tab, which lists favorites only. */ +const EMPTY_GROUPS = { available: [], loaded: [], providers: [] }; export interface UseModelsSelectorOptions { currentModel: () => string | null; useGlobalSelection?: () => boolean; onModelChange?: () => - | ((modelId: string, modelName: string) => Promise | boolean | void) + | (( + modelId: string, + modelName: string, + backendId?: string + ) => Promise | boolean | void) | undefined; onOpenChange?: (open: boolean) => void; } @@ -18,19 +39,24 @@ export interface UseModelsSelectorReturn { readonly loading: boolean; readonly updating: boolean; readonly activeId: string | null; + readonly emptyMessage: string; + readonly isMultiModel: boolean; readonly isRouter: boolean; readonly serverModel: string | null; readonly isHighlightedCurrentModelActive: boolean; readonly isCurrentModelInCache: boolean; + readonly favoriteItems: ModelItem[]; + readonly loadedItems: ModelItem[]; readonly filteredOptions: ModelOption[]; + readonly isEmpty: boolean; + readonly isProviderView: boolean; readonly groupedFilteredOptions: ReturnType; readonly isLoadingModel: boolean; readonly searchTerm: string; - readonly showModelDialog: boolean; - readonly infoModelId: string | null; + closeProvider(): void; + openProvider(backendId: string): void; setSearchTerm(value: string): void; - setShowModelDialog(value: boolean): void; - handleInfoClick(modelName: string): void; + showBackendModels(backendId: string): Promise; handleSelect(modelId: string): Promise; handleOpenChange(open: boolean): void; isFavorite(model: string): boolean; @@ -45,16 +71,36 @@ export interface UseModelsSelectorReturn { * duplicating store derivations, selection handling, and model loading. */ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSelectorReturn { - const options = $derived( + /** + * Current view: the favorites of every backend, the local server's models, or + * the remote backends'. Favorites are the default while there is at least one. + */ + /** Remote backend drilled into from its section; null while browsing. */ + let providerViewId = $state(null); + + const isProviderView = $derived(providerViewId !== null); + const isLocalOption = (option: ModelOption) => option.backendId === LOCAL_BACKEND_ID; + // every enabled backend's models are one list: favorites, then the local + // server, then one section per remote provider + const allOptions = $derived( modelsStore.models.filter((option) => { const modelProps = modelsStore.props.getModelProps(option.model); return modelProps?.ui !== false; }) ); + const options = $derived( + providerViewId ? allOptions.filter((option) => option.backendId === providerViewId) : allOptions + ); const loading = $derived(modelsStore.loading); const updating = $derived(modelsStore.updating); const activeId = $derived(modelsStore.selectedModelId); + // Router mode and external backends both expose a selectable model list; only + // a lone llama.cpp server without a router has nothing to choose from. + const isMultiModel = $derived( + serverStore.isRouterMode || + backendsStore.enabled.some((backend) => backend.id !== LOCAL_BACKEND_ID) + ); const isRouter = $derived(serverStore.isRouterMode); const serverModel = $derived(modelsStore.singleModelName); const currentModel = $derived(opts.currentModel()); @@ -62,32 +108,95 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele const isHighlightedCurrentModelActive = $derived.by(() => { if (!isRouter || !currentModel) return false; - const currentOption = options.find((option) => option.model === currentModel); + const currentOption = allOptions.find((option) => option.model === currentModel); return currentOption ? currentOption.id === activeId : false; }); const isCurrentModelInCache = $derived.by(() => { if (!isRouter || !currentModel) return true; - return options.some((option) => option.model === currentModel); + return allOptions.some((option) => option.model === currentModel); }); let isLoadingModel = $state(false); let searchTerm = $state(''); - let showModelDialog = $state(false); - let infoModelId = $state(null); + const visibleOptions = $derived(allOptions.filter((option) => !modelsStore.isHidden(option.id))); const filteredOptions = $derived(filterModelOptions(options, searchTerm)); - const groupedFilteredOptions = $derived( - groupModelOptions(filteredOptions, modelsStore.favoriteModelIds, (m) => - modelsStore.isModelLoaded(m) + // favorites span every backend, so they come from the full option list + const favoriteItems = $derived( + groupFavoriteOptions( + filterModelOptions(visibleOptions, searchTerm), + modelsStore.favoriteModelIds ) ); + const remoteProviders = $derived( + backendsStore.enabled + .filter((backend) => backend.id !== LOCAL_BACKEND_ID) + .map((backend) => { + const state = backendsModelsStore.get(backend.id); - function handleInfoClick(modelName: string) { - infoModelId = modelName; - showModelDialog = true; - } + return { + backendId: backend.id, + catalog: state.models.length, + error: state.error, + loading: state.loading, + name: backend.name + }; + }) + ); + // loaded models lead the list, from any llama-compat backend + const isLoadedLlamaCompat = (option: ModelOption) => + modelsStore.isModelLoaded(option.model) && + getBackendCapabilities(getBackend(option.backendId)).loadUnload; + const loadedItems = $derived.by(() => { + if (isProviderView) return []; + + return filterModelOptions(visibleOptions, searchTerm) + .map((option, flatIndex) => ({ flatIndex, option })) + .filter(({ option }) => isLoadedLlamaCompat(option)); + }); + const loadedIds = $derived(new SvelteSet(loadedItems.map((item) => item.option.id))); + // loaded models and favorites are listed once, at the top: the sections skip both + const sectionOptions = $derived( + filteredOptions.filter( + (option) => !modelsStore.favoriteModelIds.has(option.model) && !loadedIds.has(option.id) + ) + ); + const providerSections = $derived( + groupProviderOptions( + sectionOptions, + remoteProviders, + // a drill-in or a search reaches every model, the sections stay short + providerViewId || searchTerm ? Infinity : REMOTE_PROVIDER_MODEL_LIMIT, + modelsStore.recentModelIds + ) + ); + const groupedFilteredOptions = $derived.by(() => { + if (isProviderView) { + const sections = providerSections.filter((section) => section.backendId === providerViewId); + + return { ...EMPTY_GROUPS, providers: sections }; + } + + const local = groupModelOptions(sectionOptions.filter(isLocalOption), (m) => + modelsStore.isModelLoaded(m) + ); + + return { ...local, providers: providerSections }; + }); + const isEmpty = $derived( + filteredOptions.length === 0 && favoriteItems.length === 0 && loadedItems.length === 0 + ); + const emptyMessage = $derived(searchTerm ? 'No models found.' : 'No models yet.'); + + // a row action can hand over to the models manager, which takes the focus; + // the selector closes so it does not sit open behind the dialog + $effect(() => { + if (!uiStore.manageModelsOpen) return; + + opts.onOpenChange?.(false); + }); onMount(() => { modelsStore.fetch().catch((error) => { @@ -98,36 +207,61 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele function handleOpenChange(open: boolean) { if (loading || updating) return; - if (isRouter) { - searchTerm = ''; + // a single-model llama.cpp server with no other backend has no list to show, + // so the trigger opens the manager, which holds the one model's configuration + if (!isMultiModel) { + if (open) uiStore.openModelsManager(); - if (open) { - modelsStore.fetchRouterModels().then(() => { - modelsStore.props.fetchModalitiesForLoadedModels(); - }); - } - - opts.onOpenChange?.(open); - } else { - showModelDialog = open; + return; } + + searchTerm = ''; + providerViewId = null; + + if (open && isRouter) { + modelsStore.props.fetchModalitiesForLoadedModels(); + } + + opts.onOpenChange?.(open); + } + + /** + * Switch the rendered view. Views are display only: the backend that serves + * requests follows the selected model, not the view. + */ + /** Drill into one remote backend's full model list. */ + function openProvider(backendId: string) { + providerViewId = backendId; + searchTerm = ''; + } + + function closeProvider() { + providerViewId = null; + searchTerm = ''; + } + + /** Refresh a backend's models, e.g. right after it was added. */ + async function showBackendModels(backendId: string): Promise { + await backendsModelsStore.ensureLoaded(backendId); } async function handleSelect(modelId: string) { - const option = options.find((opt) => opt.id === modelId); + // favorites live above the tabs and may belong to another backend, so the + // lookup spans every enabled backend + const option = allOptions.find((opt) => opt.id === modelId); if (!option) return; let shouldCloseMenu = true; if (onModelChange) { - const result = await onModelChange(option.id, option.model); + const result = await onModelChange(rawModelId(option.id), option.model, option.backendId); if (result === false) { shouldCloseMenu = false; } } else { - await modelsStore.selectModelById(option.id); + await modelsStore.selectModelById(option.id, { recordRecent: true }); } if (shouldCloseMenu) { @@ -140,7 +274,10 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele }); } - if (!onModelChange && isRouter && !modelsStore.isModelLoaded(option.model)) { + // only the built-in server loads on request, and only in router mode + const canLoadHere = option.backendId === LOCAL_BACKEND_ID && isRouter; + + if (!onModelChange && canLoadHere && !modelsStore.isModelLoaded(option.model)) { isLoadingModel = true; modelsStore.status @@ -152,6 +289,18 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele function getDisplayOption(): ModelOption | undefined { if (!isRouter) { + // External backend: the selection is backend-scoped, so it wins over + // the conversation's model, which may belong to another backend. + if (!serverStore.capabilities.props) { + const selected = activeId ? allOptions.find((option) => option.id === activeId) : undefined; + + if (selected) return selected; + + return currentModel + ? allOptions.find((option) => option.model === currentModel) + : undefined; + } + const displayModel = serverModel || currentModel; if (displayModel) { @@ -176,11 +325,11 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele }; } - return options.find((option) => option.model === currentModel); + return allOptions.find((option) => option.model === currentModel); } if (activeId) { - return options.find((option) => option.id === activeId); + return allOptions.find((option) => option.id === activeId); } return undefined; @@ -191,6 +340,15 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele return activeId; }, + closeProvider, + + get emptyMessage() { + return emptyMessage; + }, + + get favoriteItems() { + return favoriteItems; + }, get filteredOptions() { return filteredOptions; }, @@ -201,20 +359,18 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele return groupedFilteredOptions; }, - handleInfoClick, - handleOpenChange, handleSelect, - get infoModelId() { - return infoModelId; - }, - get isCurrentModelInCache() { return isCurrentModelInCache; }, + get isEmpty() { + return isEmpty; + }, + isFavorite(model: string) { return modelsStore.favoriteModelIds.has(model); }, @@ -227,14 +383,28 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele return isLoadingModel; }, + get isMultiModel() { + return isMultiModel; + }, + + get isProviderView() { + return isProviderView; + }, + get isRouter() { return isRouter; }, + get loadedItems() { + return loadedItems; + }, + get loading() { return loading; }, + openProvider, + get options() { return options; }, @@ -251,13 +421,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele searchTerm = value; }, - setShowModelDialog(value: boolean) { - showModelDialog = value; - }, - - get showModelDialog() { - return showModelDialog; - }, + showBackendModels, get updating() { return updating; diff --git a/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts b/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts index 3c52d7ab27..ab3012d9c6 100644 --- a/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts +++ b/tools/ui/src/lib/hooks/use-reasoning-menu.svelte.ts @@ -20,8 +20,8 @@ export interface UseReasoningMenuReturn { /** * Shared reactive state and helpers for the reasoning effort menu. * - * Used by both the desktop dropdown (`ChatFormActionAddReasoningSubmenu`) - * and the mobile sheet (`ChatFormActionAddSheet`) to avoid duplicating the + * Used by the models dropdown footer (`ModelsSelectorReasoningPanel`) and the + * mobile sheet (`ChatFormActionAddSheet`) to avoid duplicating the * thinking-support derivation and the effort selection logic. */ export function useReasoningMenu(): UseReasoningMenuReturn { diff --git a/tools/ui/src/lib/stores/settings/index.svelte.ts b/tools/ui/src/lib/stores/settings/index.svelte.ts index a583a1423f..fb98b03dcc 100644 --- a/tools/ui/src/lib/stores/settings/index.svelte.ts +++ b/tools/ui/src/lib/stores/settings/index.svelte.ts @@ -212,7 +212,7 @@ class SettingsStore { /** * Initialize the settings store by loading from localStorage. - * Called by initStores() after migrations have run. + * Called by hydrateStores() after migrations have run. */ initialize() { if (!browser) return; @@ -220,6 +220,7 @@ class SettingsStore { try { this.loadConfig(); this.migrateLegacyTheme(); + this.migrateModelOrgNameKey(); // Apply the persisted theme from config on initial load setMode(this.config[SETTINGS_KEYS.THEME] as ColorMode); this.isInitialized = true; @@ -475,6 +476,30 @@ class SettingsStore { } } + /** + * Move the trigger-only org name key onto the selector wide one. The setting + * grew from the trigger to every model id, so an explicit choice has to + * survive the rename. + */ + private migrateModelOrgNameKey() { + if (!browser) return; + + const legacyKey = 'showModelOrgNameInTrigger'; + const { config: saved } = SettingsService.loadConfig(); + + if (!(legacyKey in saved)) return; + + this.config[SETTINGS_KEYS.SHOW_MODEL_ORG_NAME] = Boolean(saved[legacyKey]); + + if (this.userOverrides.delete(legacyKey)) { + this.userOverrides.add(SETTINGS_KEYS.SHOW_MODEL_ORG_NAME); + } + + delete (this.config as Record)[legacyKey]; + + this.saveConfig(); + } + /** * Save the current configuration to localStorage via the persistence service. */ diff --git a/tools/ui/tests/stories/a11y/ChatScreenForm.a11y.stories.svelte b/tools/ui/tests/stories/a11y/ChatScreenForm.a11y.stories.svelte index 826e0d4aa1..c56bfd7678 100644 --- a/tools/ui/tests/stories/a11y/ChatScreenForm.a11y.stories.svelte +++ b/tools/ui/tests/stories/a11y/ChatScreenForm.a11y.stories.svelte @@ -2,25 +2,8 @@ import { defineMeta } from '@storybook/addon-svelte-csf'; import ChatScreenForm from '$lib/components/app/chat/ChatScreen/ChatScreenForm.svelte'; import { ATTACHMENT_TOOLTIP_TEXT } from '$lib/constants'; - import { ServerRole } from '$lib/enums'; - import { serverStore } from '$lib/stores'; - import type { ApiLlamaCppServerProps } from '$lib/types'; import { expect, screen, waitFor } from 'storybook/test'; - /** - * The add menu mounts the reasoning submenu only outside router mode, and the - * dev server proxies /props to whichever server happens to be running, so pin - * the mode this story asserts instead of inheriting it from the environment. - */ - function pinSingleModelMode(): void { - serverStore.props = { - ...(serverStore.props ?? {}), - role: ServerRole.MODEL - } as ApiLlamaCppServerProps; - - serverStore.role = ServerRole.MODEL; - } - const { Story } = defineMeta({ component: ChatScreenForm, parameters: { @@ -55,8 +38,6 @@ args={{ class: 'max-w-[56rem] w-[calc(100vw-2rem)]' }} name="AddDropdownFocusesFirstEnabled" play={async ({ canvas, userEvent }) => { - pinSingleModelMode(); - const trigger = await canvas.findByRole('button', { name: ATTACHMENT_TOOLTIP_TEXT }); trigger.focus(); @@ -64,7 +45,7 @@ await screen.findByRole('menu'); await waitFor(() => { - expect(document.activeElement).toHaveTextContent('Reasoning'); + expect(document.activeElement).toHaveTextContent('Add files'); }); }} />