From c8d5a697670f23ba61e4202a4b44ac2936d32e04 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Sat, 22 Aug 2026 08:03:42 +0200 Subject: [PATCH 01/34] ui: replace per-conversation MCP overrides with per-conversation tool policy MCP server enabled state is now global (server.enabled); per-conversation control moves to disabled tool keys and categories seeded into each new conversation. Aligns the add sheet with the dropdown options and flattens MCP tool groups in the tools submenu. Assisted-by: pi --- .../app/chat/ChatForm/ChatForm.svelte | 7 +- .../ChatFormActionAddSheet.svelte | 269 ++++-------------- .../ChatFormActionAddToolsSubmenu.svelte | 187 ++++++------ .../ChatFormActions/ChatFormActions.svelte | 12 +- .../ChatFormPickerMcpPrompts.svelte | 5 +- .../dialogs/DialogMcpResourcesBrowser.svelte | 5 +- .../app/dialogs/DialogMcpServerAddNew.svelte | 4 +- .../app/mcp/McpActiveServersAvatars.svelte | 8 +- .../app/settings/SettingsMcpServers.svelte | 12 +- .../ui/src/lib/constants/storage.constants.ts | 3 + .../src/lib/hooks/use-tools-panel.svelte.ts | 74 +++-- .../ui/src/lib/services/migration.service.ts | 60 +++- .../ui/src/lib/stores/agentic/index.svelte.ts | 20 +- tools/ui/src/lib/stores/chat/index.svelte.ts | 9 +- .../lib/stores/conversations/index.svelte.ts | 11 +- .../conversations/preferences.svelte.ts | 262 ++++++++++------- tools/ui/src/lib/stores/mcp/index.svelte.ts | 75 ++--- tools/ui/src/lib/stores/tools.svelte.ts | 147 +++++++--- tools/ui/src/lib/types/agentic.d.ts | 10 +- tools/ui/src/lib/types/database.d.ts | 12 +- .../tests/unit/mcp-override-fallback.test.ts | 151 ---------- 21 files changed, 624 insertions(+), 719 deletions(-) delete mode 100644 tools/ui/tests/unit/mcp-override-fallback.test.ts diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte index 40819d6f1a1..006ec39ce95 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -152,9 +152,8 @@ getServerHome: () => toolsStore.serverHome ?? null, getShowModelSelector: () => showModelSelector, getValue: () => value, - hasCwdTools: () => toolsStore.hasEnabledCwdTools, - hasPrompts: () => - mcpStore.hasPromptsCapability(conversationsStore.preferences.getAllMcpServerOverrides()), + hasCwdTools: () => conversationsStore.preferences.hasEnabledCwdTools(), + hasPrompts: () => mcpStore.hasPromptsCapability(), openModelSelector: () => chatFormActionsRef?.openModelSelector(), setCaretOffset: (offset) => inputRef?.setCaretOffset(offset), setValue: (v) => { @@ -635,7 +634,7 @@ - {#if toolsStore.hasEnabledCwdTools} + {#if conversationsStore.preferences.hasEnabledCwdTools()} - import { File, FolderOpen, MessageSquare, Zap } from '@lucide/svelte'; - import { - Check, - ChevronDown, - ChevronRight, - Lightbulb, - LightbulbOff, - PencilRuler - } from '@lucide/svelte'; + import { File, MessageSquare } from '@lucide/svelte'; + import { ChevronDown, ChevronRight, PencilRuler } from '@lucide/svelte'; import { McpLogo } from '$lib/components/app'; import { Checkbox } from '$lib/components/ui/checkbox'; import * as Collapsible from '$lib/components/ui/collapsible'; import * as Sheet from '$lib/components/ui/sheet'; - import { Switch } from '$lib/components/ui/switch'; import * as Tooltip from '$lib/components/ui/tooltip'; import { ATTACHMENT_FILE_ITEMS, @@ -20,12 +12,10 @@ TOOLTIP_DELAY_DURATION } from '$lib/constants'; import { getChatFormActionsContext } from '$lib/contexts'; - import { HealthCheckStatus } from '$lib/enums'; import { AttachmentAction } from '$lib/enums/attachment.enums'; import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte'; - import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte'; import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte'; - import { conversationsStore, mcpStore } from '$lib/stores'; + import type { ToolGroup } from '$lib/types'; import type { Snippet } from 'svelte'; interface Props { @@ -38,10 +28,8 @@ const chatFormActions = getChatFormActionsContext(); let sheetOpen = $state(false); - let reasoningExpanded = $state(false); let filesExpanded = $state(true); let toolsExpanded = $state(false); - let mcpExpanded = $state(false); const attachmentMenu = useAttachmentMenu( () => ({ @@ -63,15 +51,12 @@ ); const toolsPanel = useToolsPanel(); - const reasoning = useReasoningMenu(); const sheetItemClass = 'flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left text-sm transition-colors hover:bg-accent active:bg-accent disabled:cursor-not-allowed disabled:opacity-50'; const sheetItemRowClass = 'flex w-full items-center justify-between gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent'; - - let mcpServers = $derived(mcpStore.getServers());
@@ -88,65 +73,6 @@
- {#if reasoning.modelSupportsThinking} - (reasoningExpanded = open)} - open={reasoningExpanded} - > - - {#if reasoningExpanded} - - {:else} - - {/if} - - {#if reasoning.thinkingEnabled} - - {:else if reasoning.isOff} - - {:else} - - {/if} - - Reasoning - - - {reasoning.currentEffort} - - - - -
- {#each reasoning.levels as level (level.value)} - {@const tokenLabel = reasoning.tokenLabel(level)} - - {/each} -
-
-
- {/if} - (filesExpanded = open)} open={filesExpanded}> {#if filesExpanded} @@ -194,80 +120,15 @@ - (mcpExpanded = open)} open={mcpExpanded}> - - {#if mcpExpanded} - - {:else} - - {/if} - - - - MCP Servers - - - {mcpServers.length} server{mcpServers.length !== 1 ? 's' : ''} - - - - -
- {#each mcpServers as server (server.id)} - {@const healthState = mcpStore.getHealthCheckState(server.id)} - {@const hasError = healthState.status === HealthCheckStatus.ERROR} - {@const displayName = mcpStore.getServerLabel(server)} - {@const faviconUrl = mcpStore.getServerFavicon(server.id)} - {@const isEnabled = conversationsStore.preferences.isMcpServerEnabledForChat( - server.id - )} - - - {/each} +
-
-
+ System Message + {#if toolsPanel.totalToolCount > 0} (toolsExpanded = open)} open={toolsExpanded}> @@ -289,40 +150,12 @@
- {#each toolsPanel.activeGroups as group (group.key)} - {@const checked = toolsPanel.isGroupChecked(group)} - {@const enabledCount = toolsPanel.getEnabledToolCount(group)} - {@const favicon = toolsPanel.getFavicon(group)} + {#each toolsPanel.categoryGroups as group (group.key)} + {@render sheetGroupRow(group)} + {/each} - + {#each toolsPanel.mcpGroups as group (group.key)} + {@render sheetGroupRow(group)} {/each}
@@ -331,38 +164,54 @@ - - {#if chatFormActions.hasMcpPromptsSupport} - - {/if} - - {#if chatFormActions.hasMcpResourcesSupport} - - {/if}
+ +{#snippet sheetGroupRow(group: ToolGroup)} + {@const checked = toolsPanel.isGroupChecked(group)} + {@const enabledCount = toolsPanel.getEnabledToolCount(group)} + {@const favicon = toolsPanel.getFavicon(group)} + {@const groupDisabled = toolsPanel.isGroupDisabled(group)} + + +{/snippet} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte index 40fed27c70a..79f533b22fc 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte @@ -7,6 +7,7 @@ import { CLI_FLAGS, ICON_CLASS_DEFAULT } from '$lib/constants'; import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte'; import { mcpStore, toolsStore } from '$lib/stores'; + import type { ToolGroup } from '$lib/types'; const toolsPanel = useToolsPanel(); const hasMcpServersAvailable = $derived(mcpStore.getServers().length > 0); @@ -62,95 +63,107 @@ {/if} {:else}
- {#each toolsPanel.activeGroups as group (group.key)} - {@const isExpanded = toolsPanel.expandedGroups.has(group.key)} - {@const checked = toolsPanel.isGroupChecked(group)} - {@const favicon = toolsPanel.getFavicon(group)} - - toolsPanel.toggleGroupExpanded(group.key)} - open={isExpanded} - > -
- - {#if isExpanded} - - {:else} - - {/if} - - - {#if favicon} - { - (e.currentTarget as HTMLImageElement).style.display = 'none'; - }} - src={favicon} - /> - {/if} - - {group.label} - - - - {toolsPanel.getEnabledToolCount(group)}/{group.tools.length} - - - - - - {#snippet child({ props })} - toolsPanel.toggleGroupByKey(group.key)} - /> - {/snippet} - - - -

- {checked ? 'Disable' : 'Enable'} - {group.tools.length} tool{group.tools.length !== 1 ? 's' : ''} -

-
-
-
- - -
- {#each group.tools as entry (entry.key)} - {@const enabled = toolsStore.isToolEnabled(entry.key)} - - {/each} -
-
-
+ {#each toolsPanel.categoryGroups as group (group.key)} + {@render groupRow(group)} + {/each} + + {#each toolsPanel.mcpGroups as group (group.key)} + {@render groupRow(group)} {/each}
{/if} + +{#snippet groupRow(group: ToolGroup)} + {@const isExpanded = toolsPanel.expandedGroups.has(group.key)} + {@const checked = toolsPanel.isGroupChecked(group)} + {@const favicon = toolsPanel.getFavicon(group)} + {@const groupDisabled = toolsPanel.isGroupDisabled(group)} + + toolsPanel.toggleGroupExpanded(group.key)} + open={isExpanded} + > +
+ + {#if isExpanded} + + {:else} + + {/if} + + + {#if favicon} + { + (e.currentTarget as HTMLImageElement).style.display = 'none'; + }} + src={favicon} + /> + {/if} + + {group.label} + + + + {toolsPanel.getEnabledToolCount(group)}/{group.tools.length} + + + + + + {#snippet child({ props })} + toolsPanel.toggleGroupByKey(group.key)} + /> + {/snippet} + + + +

+ {checked ? 'Disable' : 'Enable'} + {group.tools.length} tool{group.tools.length !== 1 ? 's' : ''} +

+
+
+
+ + +
+ {#each group.tools as entry (entry.key)} + {@const enabled = toolsPanel.isToolEnabled(entry)} + {@const parentDisabled = toolsPanel.isToolParentDisabled(entry)} + + {/each} +
+
+
+{/snippet} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte index f1aa743693f..2fcb1ed5f8e 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte @@ -58,17 +58,9 @@ let currentConfig = $derived(settingsStore.config); - let hasMcpPromptsSupport = $derived.by(() => { - const perChatOverrides = conversationsStore.preferences.getAllMcpServerOverrides(); + let hasMcpPromptsSupport = $derived.by(() => mcpStore.hasPromptsCapability()); - return mcpStore.hasPromptsCapability(perChatOverrides); - }); - - let hasMcpResourcesSupport = $derived.by(() => { - const perChatOverrides = conversationsStore.preferences.getAllMcpServerOverrides(); - - return mcpStore.hasResourcesCapability(perChatOverrides); - }); + let hasMcpResourcesSupport = $derived.by(() => mcpStore.hasResourcesCapability()); let hasAudioModality = $state(false); let hasVideoModality = $state(false); diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte index 353d6e7bafe..f6a3ee13479 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte @@ -9,7 +9,7 @@ } from '$lib/components/app/chat'; import Badge from '$lib/components/ui/badge/badge.svelte'; import { KeyboardKey } from '$lib/enums'; - import { conversationsStore, mcpStore } from '$lib/stores'; + import { mcpStore } from '$lib/stores'; import type { GetPromptResult, MCPPromptInfo, MCPServerSettingsEntry } from '$lib/types'; import { debounce, uuid } from '$lib/utils'; import { SvelteMap } from 'svelte/reactivity'; @@ -87,8 +87,7 @@ isLoading = true; try { - const perChatOverrides = conversationsStore.preferences.getAllMcpServerOverrides(); - const initialized = await mcpStore.ensureInitialized(perChatOverrides); + const initialized = await mcpStore.ensureInitialized(); if (!initialized) { prompts = []; diff --git a/tools/ui/src/lib/components/app/dialogs/DialogMcpResourcesBrowser.svelte b/tools/ui/src/lib/components/app/dialogs/DialogMcpResourcesBrowser.svelte index 6dfcdb856c2..82a07477d14 100644 --- a/tools/ui/src/lib/components/app/dialogs/DialogMcpResourcesBrowser.svelte +++ b/tools/ui/src/lib/components/app/dialogs/DialogMcpResourcesBrowser.svelte @@ -8,7 +8,7 @@ import { Button } from '$lib/components/ui/button'; import * as Dialog from '$lib/components/ui/dialog'; import { ICON_CLASS_DEFAULT } from '$lib/constants'; - import { conversationsStore, mcpStore } from '$lib/stores'; + import { mcpStore } from '$lib/stores'; import type { MCPResourceContent, MCPResourceInfo, MCPResourceTemplateInfo } from '$lib/types'; import { getResourceDisplayName } from '$lib/utils'; import { SvelteSet } from 'svelte/reactivity'; @@ -48,8 +48,7 @@ }); async function loadResources() { - const perChatOverrides = conversationsStore.preferences.getAllMcpServerOverrides(); - const initialized = await mcpStore.ensureInitialized(perChatOverrides); + const initialized = await mcpStore.ensureInitialized(); if (initialized) { await mcpStore.fetchAllResources(); diff --git a/tools/ui/src/lib/components/app/dialogs/DialogMcpServerAddNew.svelte b/tools/ui/src/lib/components/app/dialogs/DialogMcpServerAddNew.svelte index fab45aa9706..bc28a754c61 100644 --- a/tools/ui/src/lib/components/app/dialogs/DialogMcpServerAddNew.svelte +++ b/tools/ui/src/lib/components/app/dialogs/DialogMcpServerAddNew.svelte @@ -10,7 +10,7 @@ RECOMMENDED_MCP_SERVERS } from '$lib/constants'; import { BooleanString, HealthCheckStatus } from '$lib/enums'; - import { conversationsStore, mcpStore } from '$lib/stores'; + import { mcpStore } from '$lib/stores'; import { canonicalizeServerUrl, parseHeadersToArray, uuid } from '$lib/utils'; interface Props { @@ -234,8 +234,6 @@ useProxy: newServerUseProxy }); - conversationsStore.preferences.setMcpServerOverride(newServerId, true); - handleOpenChange(false); } diff --git a/tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte b/tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte index ea274d5aa79..5ac7a7fae6a 100644 --- a/tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte +++ b/tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte @@ -3,7 +3,7 @@ import * as Tooltip from '$lib/components/ui/tooltip'; import { ICON_CLASS_DEFAULT, MAX_DISPLAYED_MCP_AVATARS } from '$lib/constants'; import { HealthCheckStatus } from '$lib/enums'; - import { conversationsStore, mcpStore } from '$lib/stores'; + import { mcpStore } from '$lib/stores'; interface Props { class?: string; @@ -13,11 +13,7 @@ let { class: className = '', onclick }: Props = $props(); let mcpServers = $derived(mcpStore.getServers().filter((s) => s.enabled)); - let enabledMcpServersForChat = $derived( - mcpServers.filter( - (s) => conversationsStore.preferences.isMcpServerEnabledForChat(s.id) && s.url.trim() - ) - ); + let enabledMcpServersForChat = $derived(mcpServers.filter((s) => s.url.trim())); let healthyEnabledMcpServers = $derived( enabledMcpServersForChat.filter((s) => { const healthState = mcpStore.getHealthCheckState(s.id); diff --git a/tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte b/tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte index dd4b96c626f..127a035ad50 100644 --- a/tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte +++ b/tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte @@ -7,7 +7,7 @@ import { Button } from '$lib/components/ui/button'; import * as Empty from '$lib/components/ui/empty'; import { HealthCheckStatus } from '$lib/enums'; - import { conversationsStore, mcpStore, toolsStore } from '$lib/stores'; + import { mcpStore, toolsStore } from '$lib/stores'; import { onMount } from 'svelte'; import { fade } from 'svelte/transition'; @@ -86,15 +86,14 @@ {:else} (isResourcesDialogOpen = true)} onDelete={() => mcpStore.removeServer(server.id)} onToggle={async () => { - const wasEnabled = conversationsStore.preferences.isMcpServerEnabledForChat( - server.id - ); + const wasEnabled = server.enabled; - await conversationsStore.preferences.toggleMcpServerForChat(server.id); + mcpStore.updateServer(server.id, { enabled: !wasEnabled }); if (!wasEnabled) { // Promote the connection so tools/prompts/resources become @@ -104,7 +103,6 @@ } }} onUpdate={(updates) => mcpStore.updateServer(server.id, updates)} - {server} /> {/if} {/each} diff --git a/tools/ui/src/lib/constants/storage.constants.ts b/tools/ui/src/lib/constants/storage.constants.ts index 918ee450868..0aad7c77064 100644 --- a/tools/ui/src/lib/constants/storage.constants.ts +++ b/tools/ui/src/lib/constants/storage.constants.ts @@ -20,6 +20,9 @@ export const DISABLED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledTool /** Disabled tools keyed by stable selection identity, no migration from the name based key */ export const DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledToolKeys`; + +/** Default disabled tool categories, seeded into newly created conversations */ +export const DISABLED_TOOL_CATEGORIES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.disabledToolCategories`; export const FAVORITE_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.favoriteModels`; export const REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.reasoningEffortDefault`; export const CONVERSATION_TABS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.conversationTabs`; diff --git a/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts b/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts index e9dc0dcab69..2c287a4848a 100644 --- a/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts +++ b/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts @@ -1,22 +1,27 @@ import { CLI_FLAGS } from '$lib/constants'; import { ToolSource } from '$lib/enums'; import { conversationsStore, mcpStore, toolsStore } from '$lib/stores'; -import type { ToolGroup } from '$lib/types'; +import type { ToolEntry, ToolGroup } from '$lib/types'; import { SvelteSet } from 'svelte/reactivity'; export interface UseToolsPanelReturn { readonly expandedGroups: SvelteSet; - readonly groups: ToolGroup[]; - readonly activeGroups: ToolGroup[]; + readonly categoryGroups: ToolGroup[]; + readonly mcpGroups: ToolGroup[]; readonly totalToolCount: number; readonly noToolsInfoMessage: string | null; + readonly mcpCategoryEnabled: boolean; isGroupChecked(group: ToolGroup): boolean; getEnabledToolCount(group: ToolGroup): number; getFavicon(group: ToolGroup): string | null; isGroupDisabled(group: ToolGroup): boolean; + isToolEnabled(entry: ToolEntry): boolean; + isToolParentDisabled(entry: ToolEntry): boolean; + toggleTool(entry: ToolEntry): void; toggleGroupExpanded(key: string): void; /** Toggle all tools in a group by its stable key (avoids stale group object references). */ toggleGroupByKey(key: string): void; + toggleMcpCategory(): void; handleOpen(): void; } @@ -26,19 +31,21 @@ export interface UseToolsPanelReturn { * Used by both the desktop dropdown (`ChatFormActionAddToolsSubmenu`) * and the mobile sheet (`ChatFormActionAddSheet`) to avoid * duplicating group filtering, checked-state derivation, and favicon logic. + * + * All toggle state routes through `conversationsStore.preferences`: with an + * active conversation it edits that conversation's tool policy, on the + * new-chat screen it edits the global defaults seeded into new conversations. */ export function useToolsPanel(): UseToolsPanelReturn { const expandedGroups = new SvelteSet(); const groups = $derived(toolsStore.toolGroups); - const activeGroups = $derived( - groups.filter( - (g) => - g.source !== ToolSource.MCP || - !g.serverId || - conversationsStore.preferences.isMcpServerEnabledForChat(g.serverId) - ) + // non-MCP groups are 1:1 with tool categories; MCP tools group per server + const categoryGroups = $derived(groups.filter((g) => g.source !== ToolSource.MCP)); + const mcpGroups = $derived(groups.filter((g) => g.source === ToolSource.MCP)); + const totalToolCount = $derived(groups.reduce((n, g) => n + g.tools.length, 0)); + const mcpCategoryEnabled = $derived( + conversationsStore.preferences.isCategoryEnabled(ToolSource.MCP) ); - const totalToolCount = $derived(activeGroups.reduce((n, g) => n + g.tools.length, 0)); const noToolsInfoMessage = $derived.by(() => { if (toolsStore.loading) return null; @@ -56,11 +63,11 @@ export function useToolsPanel(): UseToolsPanelReturn { }); function isGroupChecked(group: ToolGroup): boolean { - return toolsStore.isGroupFullyEnabled(group); + return conversationsStore.preferences.isGroupChecked(group); } function getEnabledToolCount(group: ToolGroup): number { - return group.tools.filter((tool) => toolsStore.isToolEnabled(tool.key)).length; + return group.tools.filter((tool) => conversationsStore.preferences.isToolActive(tool)).length; } function getFavicon(group: ToolGroup): string | null { @@ -70,13 +77,25 @@ export function useToolsPanel(): UseToolsPanelReturn { } function isGroupDisabled(group: ToolGroup): boolean { + // MCP server groups gray out while the whole MCP category is off return ( group.source === ToolSource.MCP && - !!group.serverId && - !conversationsStore.preferences.isMcpServerEnabledForChat(group.serverId) + !conversationsStore.preferences.isCategoryEnabled(ToolSource.MCP) ); } + function isToolEnabled(entry: ToolEntry): boolean { + return conversationsStore.preferences.isToolEnabled(entry.key); + } + + function isToolParentDisabled(entry: ToolEntry): boolean { + return conversationsStore.preferences.isToolParentDisabled(entry); + } + + function toggleTool(entry: ToolEntry): void { + void conversationsStore.preferences.toggleTool(entry.key); + } + function toggleGroupExpanded(key: string): void { if (expandedGroups.has(key)) { expandedGroups.delete(key); @@ -87,11 +106,15 @@ export function useToolsPanel(): UseToolsPanelReturn { function toggleGroupByKey(key: string): void { // Find current group by key to get up-to-date tool references - const group = activeGroups.find((g) => g.key === key); + const group = groups.find((g) => g.key === key); if (!group) return; - toolsStore.toggleGroup(group); + void conversationsStore.preferences.toggleGroup(group); + } + + function toggleMcpCategory(): void { + void conversationsStore.preferences.toggleCategory(ToolSource.MCP); } function handleOpen(): void { @@ -103,23 +126,30 @@ export function useToolsPanel(): UseToolsPanelReturn { } return { - get activeGroups() { - return activeGroups; + get categoryGroups() { + return categoryGroups; }, expandedGroups, getEnabledToolCount, getFavicon, - get groups() { - return groups; - }, handleOpen, isGroupChecked, isGroupDisabled, + isToolEnabled, + isToolParentDisabled, + get mcpCategoryEnabled() { + return mcpCategoryEnabled; + }, + get mcpGroups() { + return mcpGroups; + }, get noToolsInfoMessage() { return noToolsInfoMessage; }, toggleGroupByKey, toggleGroupExpanded, + toggleMcpCategory, + toggleTool, get totalToolCount() { return totalToolCount; } diff --git a/tools/ui/src/lib/services/migration.service.ts b/tools/ui/src/lib/services/migration.service.ts index 5d321b3ba28..1b3ecd61209 100644 --- a/tools/ui/src/lib/services/migration.service.ts +++ b/tools/ui/src/lib/services/migration.service.ts @@ -11,6 +11,7 @@ import { CONFIG_LOCALSTORAGE_KEY, DB_APP_NAME_DEPRECATED, + DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY, IDXDB_STORES, IDXDB_TABLES, LEGACY_AGENTIC_REGEX, @@ -21,6 +22,7 @@ import { STORAGE_APP_NAME_DEPRECATED } from '$lib/constants'; import { BooleanString, MessageRole } from '$lib/enums'; +import type { McpServerOverride } from '$lib/types/database'; import Dexie from 'dexie'; // Types @@ -737,6 +739,61 @@ const mcpDefaultOverridesMergeMigration: Migration = { ); } }; +const MCP_SERVER_OVERRIDES_TO_TOOL_POLICY_MIGRATION_ID = 'mcp-server-overrides-to-tool-policy-v1'; +const mcpServerOverridesToToolPolicyMigration: Migration = { + description: + 'Seed per-conversation disabled tool keys from the global defaults and legacy per-conversation MCP server overrides (legacy field preserved)', + id: MCP_SERVER_OVERRIDES_TO_TOOL_POLICY_MIGRATION_ID, + + async run(): Promise { + // The global disabled set used to apply to every conversation; it is now + // the defaults seeded into newly created conversations, so existing rows + // are seeded with it to keep their behavior unchanged. + let defaults: string[] = []; + + try { + const raw = localStorage.getItem(DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY); + + if (raw) { + const parsed: unknown = JSON.parse(raw); + + if (Array.isArray(parsed)) { + defaults = parsed.filter((k): k is string => typeof k === 'string'); + } + } + } catch { + return; + } + + const db = await getDatabaseService(); + const conversations = await db.getAllConversations(); + + let migratedCount = 0; + + for (const conv of conversations) { + // re-run safety: a row that already has a policy is left alone + if (conv.disabledTools !== undefined) continue; + + // A legacy per-conversation server disable becomes a server-scoped tool + // key (same format as toolsStore.getMcpServerToolsKey). Per-conversation + // enables are dropped: the global server flag governs now. + const serverGroupKeys = (conv.mcpServerOverrides ?? []) + .filter((o: McpServerOverride) => !o.enabled) + .map((o: McpServerOverride) => `mcp:${o.serverId}`); + const disabledTools = [...new Set([...defaults, ...serverGroupKeys])]; + + if (disabledTools.length === 0) continue; + + await db.updateConversation(conv.id, { disabledTools }); + migratedCount++; + } + + if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) + console.log( + `[Migration] MCP server overrides -> tool policy: updated ${migratedCount} conversations` + ); + } +}; const migrations: Migration[] = [ localStorageMigration, idxdbMigration, @@ -746,7 +803,8 @@ const migrations: Migration[] = [ mcpDefaultEnabledMigration, mcpDefaultOverridesMergeMigration, configTypesMigration, - renderKeysMigration + renderKeysMigration, + mcpServerOverridesToToolPolicyMigration ]; export const MigrationService = { diff --git a/tools/ui/src/lib/stores/agentic/index.svelte.ts b/tools/ui/src/lib/stores/agentic/index.svelte.ts index a91e0ba46fa..4d94519db7b 100644 --- a/tools/ui/src/lib/stores/agentic/index.svelte.ts +++ b/tools/ui/src/lib/stores/agentic/index.svelte.ts @@ -44,7 +44,6 @@ import type { AgenticFlowParams, AgenticFlowResult, AgenticSession, - McpServerOverride, MCPToolCall, SettingsConfigType, ToolExecutionResult @@ -201,10 +200,10 @@ class AgenticStore { return active; } - getConfig(settings: SettingsConfigType, perChatOverrides?: McpServerOverride[]): AgenticConfig { + getConfig(settings: SettingsConfigType): AgenticConfig { const maxTurns = Number(settings.agenticMaxTurns) || DEFAULT_AGENTIC_CONFIG.maxTurns; const hasTools = - mcpStore.hasEnabledServers(perChatOverrides) || + mcpStore.hasEnabledServers() || toolsStore.serverTools.length > 0 || toolsStore.browserTools.length > 0 || toolsStore.customTools.length > 0; @@ -309,8 +308,8 @@ class AgenticStore { flowRootMessageId, messages, options = {}, - perChatOverrides, - signal + signal, + toolPolicy } = params; // Clear any pending permissions/continue requests for this conversation when starting a new flow @@ -321,21 +320,24 @@ class AgenticStore { await toolsStore.fetchServerTools(); } - const agenticConfig = this.getConfig(settingsStore.config, perChatOverrides); + const agenticConfig = this.getConfig(settingsStore.config); if (!agenticConfig.enabled) return { handled: false }; - const hasMcpServers = mcpStore.hasEnabledServers(perChatOverrides); + const hasMcpServers = mcpStore.hasEnabledServers(); if (hasMcpServers) { - const initialized = await mcpStore.ensureInitialized(perChatOverrides); + const initialized = await mcpStore.ensureInitialized(); if (!initialized) { console.log('[AgenticStore] MCP not initialized'); } } - const tools = toolsStore.getEnabledToolsForLLM(); + const tools = toolsStore.getEnabledToolsForLLM( + new Set(toolPolicy?.disabledTools ?? []), + new Set(toolPolicy?.disabledToolCategories ?? []) + ); if (tools.length === 0) { return { handled: false }; diff --git a/tools/ui/src/lib/stores/chat/index.svelte.ts b/tools/ui/src/lib/stores/chat/index.svelte.ts index aab824fd715..296c2cca582 100644 --- a/tools/ui/src/lib/stores/chat/index.svelte.ts +++ b/tools/ui/src/lib/stores/chat/index.svelte.ts @@ -1132,7 +1132,10 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost { await DatabaseService.updateMessage(messageId, updates); } }; - const perChatOverrides = conversationsStore.preferences.getAllMcpServerOverrides(); + const toolPolicy = { + disabledToolCategories: conversationsStore.preferences.getDisabledToolCategories(), + disabledTools: conversationsStore.preferences.getDisabledTools() + }; { const agenticResult = await agenticStore.runAgenticFlow({ @@ -1144,8 +1147,8 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost { ...this.getApiOptions(), ...(effectiveModel ? { model: effectiveModel } : {}) }, - perChatOverrides, - signal: abortController.signal + signal: abortController.signal, + toolPolicy }); if (agenticResult.handled) { diff --git a/tools/ui/src/lib/stores/conversations/index.svelte.ts b/tools/ui/src/lib/stores/conversations/index.svelte.ts index 98bf6a0310a..f2082ebefce 100644 --- a/tools/ui/src/lib/stores/conversations/index.svelte.ts +++ b/tools/ui/src/lib/stores/conversations/index.svelte.ts @@ -251,12 +251,15 @@ class ConversationsStore implements ConversationsPreferencesHost { */ async createConversation(name?: string): Promise { const conversationName = name || `Chat ${new Date().toLocaleString()}`; - // Working directory and reasoning effort picked on the new-chat screen - // get threaded into the new conversation here, then cleared so they - // don't bleed onto subsequent new chats. + // The tool policy is seeded from the current defaults: edits made inside + // the conversation afterwards live on its row and do not flow back into + // the defaults. Working directory picked on the new-chat screen gets + // threaded in here too, then cleared so it doesn't bleed onto subsequent + // new chats. const conversation = await DatabaseService.createConversation(conversationName, { cwd: this.preferences.pendingCwd ?? undefined, - reasoningEffort: this.preferences.pendingReasoningEffort + reasoningEffort: this.preferences.pendingReasoningEffort, + ...this.preferences.getToolPolicySnapshot() }); this.preferences.pendingCwd = null; diff --git a/tools/ui/src/lib/stores/conversations/preferences.svelte.ts b/tools/ui/src/lib/stores/conversations/preferences.svelte.ts index fea92860334..8106a0d85a4 100644 --- a/tools/ui/src/lib/stores/conversations/preferences.svelte.ts +++ b/tools/ui/src/lib/stores/conversations/preferences.svelte.ts @@ -1,21 +1,22 @@ /** * ConversationPreferences - Per-chat options with global fallback * - * Owns the options that resolve per conversation: MCP server overrides, - * reasoning effort, and the working directory. Cwd and reasoning effort are - * buffered as pending state and threaded into the next created conversation - * by the host; MCP server overrides edit the sparse `mcpServerOverrides` - * list on the active row (new-chat toggles edit the server's global flag). + * Owns the options that resolve per conversation: the tool policy (disabled + * categories and tool keys), reasoning effort, and the working directory. + * Tool picks made on the empty new-chat screen edit the global defaults + * directly (they seed every newly created conversation); cwd and reasoning + * effort are buffered as pending state and threaded into the next created + * conversation by the host. * Created and owned by conversationsStore; the host owns the conversation * rows these options persist onto. */ import { REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY } from '$lib/constants'; -import { ReasoningEffort } from '$lib/enums'; +import { ReasoningEffort, ToolSource } from '$lib/enums'; import { DatabaseService } from '$lib/services/database.service'; // direct imports between stores, not via the barrel, to avoid circular deps -import { mcpStore } from '$lib/stores/mcp/index.svelte'; -import type { McpServerOverride } from '$lib/types/database'; +import { toolsStore } from '$lib/stores/tools.svelte'; +import type { DatabaseConversation, ToolEntry, ToolGroup } from '$lib/types'; /** Load reasoning effort default from localStorage, DEFAULT defers to the server */ function loadReasoningEffortDefault(): ReasoningEffort { @@ -48,6 +49,18 @@ export interface ConversationsPreferencesHost { applyConversationUpdate(id: string, updates: Partial): void; } +/** Effective disabled tool keys: conversation row, falling back to defaults. */ +function buildDisabledTools(conv: DatabaseConversation | null): Set { + return new Set(conv ? (conv.disabledTools ?? []) : [...toolsStore.disabledTools]); +} + +/** Effective disabled tool categories: conversation row, falling back to defaults. */ +function buildDisabledToolCategories(conv: DatabaseConversation | null): Set { + return new Set( + conv ? (conv.disabledToolCategories ?? []) : [...toolsStore.disabledToolCategories] + ); +} + export class ConversationPreferences { /** * Working directory picked on the empty new-chat screen, before any @@ -61,36 +74,35 @@ export class ConversationPreferences { /** Global (non-conversation-specific) reasoning effort default */ pendingReasoningEffort = $state(loadReasoningEffortDefault()); - constructor(private host: ConversationsPreferencesHost) {} - - /** - * Gets the effective override list for the current conversation: - * one entry per configured server, resolved per server. The stored - * per-conversation list is sparse and only holds explicit toggles. - */ - getAllMcpServerOverrides(): McpServerOverride[] { - const overrides = this.host.activeConversation?.mcpServerOverrides; - - return mcpStore.getServers().map((s) => { - const override = overrides?.find((o: McpServerOverride) => o.serverId === s.id); + private get _disabledToolCategories(): Set { + return buildDisabledToolCategories(this.host.activeConversation); + } - return { enabled: override?.enabled ?? s.enabled, serverId: s.id }; - }); + // getters, not $derived fields: lazy evaluation keeps them off the class + // field initialization order (host is assigned by the constructor), and + // reads of the underlying $state stay tracked in reactive contexts + private get _disabledTools(): Set { + return buildDisabledTools(this.host.activeConversation); } /** - * Gets the effective MCP server override for a specific server. - * A per-conversation override wins when present; a server without one - * resolves to its `mcpServers[i].enabled` default. + * + * + * Tool Policy + * + * */ - getMcpServerOverride(serverId: string): McpServerOverride | undefined { - const override = this.host.activeConversation?.mcpServerOverrides?.find( - (o: McpServerOverride) => o.serverId === serverId - ); - if (override) return override; + constructor(private host: ConversationsPreferencesHost) {} - return this.getDefaultOverride(serverId); + /** Effective disabled tool categories for the current context, captured at flow start. */ + getDisabledToolCategories(): ToolSource[] { + return [...this._disabledToolCategories]; + } + + /** Effective disabled tool keys for the current context, captured at flow start. */ + getDisabledTools(): string[] { + return [...this._disabledTools]; } /** @@ -114,16 +126,56 @@ export class ConversationPreferences { return this.pendingReasoningEffort; } - /** Checks if an MCP server is enabled for the active conversation. */ - isMcpServerEnabledForChat(serverId: string): boolean { - const override = this.getMcpServerOverride(serverId); + /** Defaults snapshot for seeding a newly created conversation. */ + getToolPolicySnapshot(): { disabledTools?: string[]; disabledToolCategories?: ToolSource[] } { + const disabledTools = [...toolsStore.disabledTools]; + const disabledToolCategories = [...toolsStore.disabledToolCategories]; + + return { + disabledToolCategories: disabledToolCategories.length ? disabledToolCategories : undefined, + disabledTools: disabledTools.length ? disabledTools : undefined + }; + } + + hasEnabledCwdTools(): boolean { + return toolsStore.hasEnabledCwdTools(this._disabledTools, this._disabledToolCategories); + } + + isCategoryEnabled(source: ToolSource): boolean { + return !this._disabledToolCategories.has(source); + } + + /** Group checkbox state: the category flag, or the server key for MCP groups. */ + isGroupChecked(group: ToolGroup): boolean { + return group.source === ToolSource.MCP && group.serverId + ? this.isServerToolsEnabled(group.serverId) + : this.isCategoryEnabled(group.source); + } + + /** Server-scoped MCP group state: one key disables all of that server's tools. */ + isServerToolsEnabled(serverId: string): boolean { + return this.isToolEnabled(toolsStore.getMcpServerToolsKey(serverId)); + } + + /** Effective state: own key, MCP server group key, and category all on. */ + isToolActive(entry: ToolEntry): boolean { + return toolsStore.isEntryEnabled(entry, this._disabledTools, this._disabledToolCategories); + } - return override?.enabled ?? false; + /** Own-level state: the tool key itself, ignoring category and server group. */ + isToolEnabled(key: string): boolean { + return !this._disabledTools.has(key); } - /** Removes MCP server override for the active conversation. */ - async removeMcpServerOverride(serverId: string): Promise { - await this.setMcpServerOverride(serverId, undefined); + /** True when a parent level (category or MCP server group) disables this entry. */ + isToolParentDisabled(entry: ToolEntry): boolean { + if (!this.isCategoryEnabled(entry.source)) return true; + + return ( + entry.source === ToolSource.MCP && + !!entry.serverId && + !this.isServerToolsEnabled(entry.serverId) + ); } /** Reload persisted defaults, e.g. when the active conversation is cleared. */ @@ -166,96 +218,110 @@ export class ConversationPreferences { } /** - * Sets or removes MCP server override for the active conversation. - * If no conversation exists, persists `enabled` onto `mcpServers[i].enabled` - * (the single source of truth for new-chat defaults). + * Sets the reasoning effort for the active conversation. + * If no conversation exists, stores the global default. + * @param effort - The effort level ('default' | 'off' | 'low' | 'medium' | 'high' | 'max') */ - async setMcpServerOverride(serverId: string, enabled: boolean | undefined): Promise { + async setReasoningEffort(effort: ReasoningEffort): Promise { if (!this.host.activeConversation) { - if (enabled !== undefined) { - mcpStore.updateServer(serverId, { enabled }); - } + this.pendingReasoningEffort = effort; + saveReasoningEffortDefault(effort); return; } - // Clone to plain objects to avoid Proxy serialization issues with IndexedDB - const currentOverrides = (this.host.activeConversation.mcpServerOverrides || []).map( - (o: McpServerOverride) => ({ - enabled: o.enabled, - serverId: o.serverId - }) - ); + this.host.applyConversationUpdate(this.host.activeConversation.id, { + reasoningEffort: effort + }); + + await DatabaseService.updateConversation(this.host.activeConversation.id, { + reasoningEffort: effort + }); + } - let newOverrides: McpServerOverride[]; + async toggleCategory(source: ToolSource): Promise { + const conv: DatabaseConversation | null = this.host.activeConversation; - if (enabled === undefined) { - newOverrides = currentOverrides.filter((o: McpServerOverride) => o.serverId !== serverId); - } else { - const existingIndex = currentOverrides.findIndex( - (o: McpServerOverride) => o.serverId === serverId - ); - - if (existingIndex >= 0) { - newOverrides = [...currentOverrides]; - newOverrides[existingIndex] = { enabled, serverId }; - } else { - newOverrides = [...currentOverrides, { enabled, serverId }]; - } + if (!conv) { + toolsStore.toggleCategory(source); + + return; } - const overrides = newOverrides.length > 0 ? newOverrides : undefined; - const id = this.host.activeConversation.id; + const next = buildDisabledToolCategories(conv); - this.host.applyConversationUpdate(id, { - mcpServerOverrides: overrides - }); + if (next.has(source)) next.delete(source); + else next.add(source); - await DatabaseService.updateConversation(id, { - mcpServerOverrides: overrides - }); + await this.persistDisabledToolCategories(next); + } + + async toggleGroup(group: ToolGroup): Promise { + if (group.source === ToolSource.MCP && group.serverId) { + await this.toggleServerTools(group.serverId); + } else { + await this.toggleCategory(group.source); + } + } + + async toggleServerTools(serverId: string): Promise { + await this.toggleTool(toolsStore.getMcpServerToolsKey(serverId)); } /** - * Sets the reasoning effort for the active conversation. - * If no conversation exists, stores the global default. - * @param effort - The effort level ('default' | 'off' | 'low' | 'medium' | 'high' | 'max') + * + * + * Reasoning Effort + * + * */ - async setReasoningEffort(effort: ReasoningEffort): Promise { - if (!this.host.activeConversation) { - this.pendingReasoningEffort = effort; - saveReasoningEffortDefault(effort); + + async toggleTool(key: string): Promise { + const conv: DatabaseConversation | null = this.host.activeConversation; + + if (!conv) { + toolsStore.toggleTool(key); return; } - const id = this.host.activeConversation.id; + const next = buildDisabledTools(conv); - this.host.applyConversationUpdate(id, { - reasoningEffort: effort - }); + if (next.has(key)) next.delete(key); + else next.add(key); - await DatabaseService.updateConversation(id, { - reasoningEffort: effort - }); + await this.persistDisabledTools(next); } - /** Toggles MCP server enabled state for the active conversation. */ - async toggleMcpServerForChat(serverId: string): Promise { - const currentEnabled = this.isMcpServerEnabledForChat(serverId); + private async persistDisabledToolCategories(disabled: Set): Promise { + const conv = this.host.activeConversation; + + if (!conv) return; + + const disabledToolCategories = disabled.size ? [...disabled] : undefined; - await this.setMcpServerOverride(serverId, !currentEnabled); + this.host.applyConversationUpdate(conv.id, { disabledToolCategories }); + + await DatabaseService.updateConversation(conv.id, { disabledToolCategories }); } /** - * Resolve the default enabled value for a server: its own `enabled` - * flag in `mcpServers`, so the global on/off state lives in one place. + * + * + * Working Directory + * + * */ - private getDefaultOverride(serverId: string): McpServerOverride | undefined { - const server = mcpStore.getServers().find((s) => s.id === serverId); - if (!server) return undefined; + private async persistDisabledTools(disabled: Set): Promise { + const conv = this.host.activeConversation; + + if (!conv) return; + + const disabledTools = disabled.size ? [...disabled] : undefined; + + this.host.applyConversationUpdate(conv.id, { disabledTools }); - return { enabled: server.enabled, serverId }; + await DatabaseService.updateConversation(conv.id, { disabledTools }); } } diff --git a/tools/ui/src/lib/stores/mcp/index.svelte.ts b/tools/ui/src/lib/stores/mcp/index.svelte.ts index ccd53bc9d2f..ce9c4fbe3a0 100644 --- a/tools/ui/src/lib/stores/mcp/index.svelte.ts +++ b/tools/ui/src/lib/stores/mcp/index.svelte.ts @@ -37,7 +37,7 @@ import type { Tool, ToolExecutionResult } from '$lib/types'; -import type { DatabaseMessageExtraMcpResource, McpServerOverride } from '$lib/types/database'; +import type { DatabaseMessageExtraMcpResource } from '$lib/types/database'; import type { SettingsConfigType } from '$lib/types/settings'; import { detectMcpTransportFromUrl, @@ -306,12 +306,12 @@ class MCPStore implements McpHealthHost { return extras; } - async ensureInitialized(perChatOverrides?: McpServerOverride[]): Promise { + async ensureInitialized(): Promise { if (!browser) { return false; } - const mcpConfig = this.buildMcpClientConfig(settingsStore.config, perChatOverrides); + const mcpConfig = this.buildMcpClientConfig(settingsStore.config); const signature = mcpConfig ? JSON.stringify(mcpConfig) : null; if (!signature) { @@ -512,14 +512,6 @@ class MCPStore implements McpHealthHost { return this.connections; } - getEnabledServersForConversation( - perChatOverrides?: McpServerOverride[] - ): MCPServerSettingsEntry[] { - return this.getServers().filter((server) => { - return this.checkServerEnabled(server, perChatOverrides); - }); - } - /** * Check if a server already has an active connection that can be reused. * Returns the existing connection if available. @@ -811,8 +803,8 @@ class MCPStore implements McpHealthHost { ); } - hasEnabledServers(perChatOverrides?: McpServerOverride[]): boolean { - return Boolean(this.buildMcpClientConfig(settingsStore.config, perChatOverrides)); + hasEnabledServers(): boolean { + return Boolean(this.buildMcpClientConfig(settingsStore.config)); } /** @@ -820,18 +812,12 @@ class MCPStore implements McpHealthHost { * Uses health check state since servers may not have active connections until * the user actually sends a message or uses prompts. */ - hasPromptsCapability(perChatOverrides?: McpServerOverride[]): boolean { - let enabledServerIds: Set; - - if (perChatOverrides !== undefined) { - enabledServerIds = new Set(perChatOverrides.filter((o) => o.enabled).map((o) => o.serverId)); - } else { - enabledServerIds = new Set( - this.getServers() - .filter((s) => s.enabled) - .map((s) => s.id) - ); - } + hasPromptsCapability(): boolean { + const enabledServerIds = new Set( + this.getServers() + .filter((s) => s.enabled) + .map((s) => s.id) + ); if (enabledServerIds.size === 0) { return false; @@ -874,18 +860,12 @@ class MCPStore implements McpHealthHost { * Uses health check state since servers may not have active connections until * the user actually sends a message or uses prompts. */ - hasResourcesCapability(perChatOverrides?: McpServerOverride[]): boolean { - let enabledServerIds: Set; - - if (perChatOverrides !== undefined) { - enabledServerIds = new Set(perChatOverrides.filter((o) => o.enabled).map((o) => o.serverId)); - } else { - enabledServerIds = new Set( - this.getServers() - .filter((s) => s.enabled) - .map((s) => s.id) - ); - } + hasResourcesCapability(): boolean { + const enabledServerIds = new Set( + this.getServers() + .filter((s) => s.enabled) + .map((s) => s.id) + ); if (enabledServerIds.size === 0) { return false; @@ -1185,10 +1165,7 @@ class MCPStore implements McpHealthHost { /** * Builds MCP client configuration from settings. */ - private buildMcpClientConfig( - cfg: SettingsConfigType, - perChatOverrides?: McpServerOverride[] - ): MCPClientConfig | undefined { + private buildMcpClientConfig(cfg: SettingsConfigType): MCPClientConfig | undefined { const rawServers = parseMcpServerSettings(cfg.mcpServers); if (!rawServers.length) { @@ -1198,7 +1175,7 @@ class MCPStore implements McpHealthHost { const servers: Record = {}; for (const [index, entry] of rawServers.entries()) { - if (!this.checkServerEnabled(entry, perChatOverrides)) continue; + if (!entry.enabled) continue; const normalized = this.buildServerConfig(entry); @@ -1252,20 +1229,6 @@ class MCPStore implements McpHealthHost { }; } - /** - * Checks if a server is enabled for a given chat. - * A per-chat override wins when present; a server without one resolves - * to its own `enabled` flag in `mcpServers`. - */ - private checkServerEnabled( - server: MCPServerSettingsEntry, - perChatOverrides?: McpServerOverride[] - ): boolean { - const override = perChatOverrides?.find((o) => o.serverId === server.id); - - return override?.enabled ?? server.enabled; - } - private createListChangedHandlers(serverName: string): ListChangedHandlers { return { prompts: { diff --git a/tools/ui/src/lib/stores/tools.svelte.ts b/tools/ui/src/lib/stores/tools.svelte.ts index e255b8a43ec..dbbaee05a1d 100644 --- a/tools/ui/src/lib/stores/tools.svelte.ts +++ b/tools/ui/src/lib/stores/tools.svelte.ts @@ -12,6 +12,7 @@ import { buildBrowserInfoToolDefinition, buildGetDatetimeToolDefinition, buildReadMediaToolDefinition, + DISABLED_TOOL_CATEGORIES_LOCALSTORAGE_KEY, DISABLED_TOOL_KEYS_LOCALSTORAGE_KEY, HOME_TILDE, TOOL_GROUP_LABELS, @@ -37,6 +38,9 @@ import { SvelteMap, SvelteSet } from 'svelte/reactivity'; /** Stable selection identity for a tool, shared by the disabled set and the permission store */ class ToolsStore { + // default disabled tool categories, seeded into newly created conversations; + // the per-conversation policy lives on the conversation row + private _disabledToolCategories = $state(new SvelteSet()); private _disabledTools = $state(new SvelteSet()); private _error = $state(null); private _loading = $state(false); @@ -150,6 +154,10 @@ class ToolsStore { } } + get disabledToolCategories(): ReadonlySet { + return this._disabledToolCategories; + } + get disabledTools(): SvelteSet { return this._disabledTools; } @@ -158,26 +166,6 @@ class ToolsStore { return this._error; } - /** - * Check if a working directory is worth setting: at least one server tool - * that reads it is both served and left enabled by the user. - */ - get hasEnabledCwdTools(): boolean { - return this._serverTools.some((def) => { - const name = def.function.name; - - return ( - this.cwdAwareTools.has(name) && - !this._disabledTools.has(this.toolKey(ToolSource.SERVER, name)) - ); - }); - } - - /** Check if there are any enabled tools available (server, MCP, or custom) */ - get hasEnabledTools(): boolean { - return this.getEnabledToolsForLLM().length > 0; - } - get isToolsEndpointUnreachable(): boolean { return this._toolsEndpointUnreachable; } @@ -271,17 +259,14 @@ class ToolsStore { } } - /** - * Enabled tool definitions for sending to the LLM. - * MCP tool schemas are normalized here so the wire payload is consistent - * across all four sources (server, browser/sandbox, MCP, custom JSON). - * The API identifies tools by name, so a name is sent at most once. - */ - getEnabledToolsForLLM(): OpenAIToolDefinition[] { + getEnabledToolsForLLM( + disabledTools: ReadonlySet = this._disabledTools, + disabledCategories: ReadonlySet = this._disabledToolCategories + ): OpenAIToolDefinition[] { const enabledNames = new SvelteSet(); for (const entry of this.allTools) { - if (!this._disabledTools.has(entry.key)) { + if (this.isEntryEnabled(entry, disabledTools, disabledCategories)) { enabledNames.add(entry.definition.function.name); } } @@ -306,6 +291,11 @@ class ToolsStore { return result; } + /** Server-scoped tool key: disabling it disables all of that server's tools. */ + getMcpServerToolsKey(serverId: string): string { + return `mcp:${serverId}`; + } + /** Permission key for a tool name, identical to the selection key */ getPermissionKey(toolName: string): string | null { return this.findEntryByName(toolName)?.key ?? null; @@ -333,6 +323,26 @@ class ToolsStore { return this.findEntryByName(toolName)?.source ?? null; } + /** + * Check if a working directory is worth setting: at least one server tool + * that reads it is both served and left enabled by the given policy + * (defaults to the global defaults). + */ + hasEnabledCwdTools( + disabledTools: ReadonlySet = this._disabledTools, + disabledCategories: ReadonlySet = this._disabledToolCategories + ): boolean { + if (disabledCategories.has(ToolSource.SERVER)) return false; + + return this._serverTools.some((def) => { + const name = def.function.name; + + return ( + this.cwdAwareTools.has(name) && !disabledTools.has(this.toolKey(ToolSource.SERVER, name)) + ); + }); + } + /** * Load persisted disabled tools and fetch the builtin tool list. * Called by initStores() after migrations have run. @@ -357,11 +367,56 @@ class ToolsStore { console.error('[ToolsStore] Failed to load disabled tools from localStorage:', err); } + try { + const stored = localStorage.getItem(DISABLED_TOOL_CATEGORIES_LOCALSTORAGE_KEY); + + if (stored) { + const parsed = JSON.parse(stored); + + if (Array.isArray(parsed)) { + for (const key of parsed) { + if (Object.values(ToolSource).includes(key)) { + this._disabledToolCategories.add(key as ToolSource); + } + } + } + } + } catch (err) { + console.error('[ToolsStore] Failed to load disabled tool categories from localStorage:', err); + } + this.fetchServerTools(); } - isGroupFullyEnabled(group: ToolGroup): boolean { - return group.tools.length > 0 && group.tools.every((t) => this.isToolEnabled(t.key)); + isCategoryEnabled(source: ToolSource): boolean { + return !this._disabledToolCategories.has(source); + } + + /** + * Enabled tool definitions for sending to the LLM. + * MCP tool schemas are normalized here so the wire payload is consistent + * across all four sources (server, browser/sandbox, MCP, custom JSON). + * The API identifies tools by name, so a name is sent at most once. + */ + /** + * The single enable rule for a tool entry: its category must be on, its + * own key must not be disabled, and for MCP tools the server-scoped group + * key must not be disabled either. + */ + isEntryEnabled( + entry: ToolEntry, + disabledTools: ReadonlySet, + disabledCategories: ReadonlySet + ): boolean { + if (disabledCategories.has(entry.source)) return false; + + if (disabledTools.has(entry.key)) return false; + + if (entry.source === ToolSource.MCP && entry.serverId) { + return !disabledTools.has(this.getMcpServerToolsKey(entry.serverId)); + } + + return true; } isToolEnabled(key: string): boolean { @@ -394,6 +449,16 @@ class ToolsStore { return this._serverHome; } + setCategoryEnabled(source: ToolSource, enabled: boolean): void { + if (enabled) { + this._disabledToolCategories.delete(source); + } else { + this._disabledToolCategories.add(source); + } + + this.persistDisabledToolCategories(); + } + setToolEnabled(key: string, enabled: boolean): void { if (enabled) { this._disabledTools.delete(key); @@ -402,15 +467,8 @@ class ToolsStore { } } - toggleGroup(group: ToolGroup): void { - const allEnabled = group.tools.every((t) => this.isToolEnabled(t.key)); - const target = !allEnabled; - - for (const tool of group.tools) { - if (target) this._disabledTools.delete(tool.key); - else this._disabledTools.add(tool.key); - } - this.persistDisabledTools(); + toggleCategory(source: ToolSource): void { + this.setCategoryEnabled(source, !this.isCategoryEnabled(source)); } toggleTool(key: string): void { @@ -602,6 +660,17 @@ class ToolsStore { return normalized; } + private persistDisabledToolCategories(): void { + try { + localStorage.setItem( + DISABLED_TOOL_CATEGORIES_LOCALSTORAGE_KEY, + JSON.stringify([...this._disabledToolCategories]) + ); + } catch { + // ignore storage errors + } + } + private persistDisabledTools(): void { try { localStorage.setItem( diff --git a/tools/ui/src/lib/types/agentic.d.ts b/tools/ui/src/lib/types/agentic.d.ts index 1a604476bf9..c60e283fb6f 100644 --- a/tools/ui/src/lib/types/agentic.d.ts +++ b/tools/ui/src/lib/types/agentic.d.ts @@ -15,7 +15,7 @@ import type { DatabaseMessageExtraAudioFile, DatabaseMessageExtraImageFile } from './database'; -import type { MessageRole } from '$lib/enums'; +import type { MessageRole, ToolSource } from '$lib/enums'; import { AgenticSectionType, ContinueIntentKind, ToolCallType } from '$lib/enums'; /** @@ -162,6 +162,12 @@ export interface AgenticFlowOptions { /** * Parameters for starting an agentic flow */ +/** Per-conversation tool policy, captured at flow start */ +export interface AgenticToolPolicy { + disabledToolCategories: ToolSource[]; + disabledTools: string[]; +} + export interface AgenticFlowParams { conversationId: string; /** ID of the flow's first assistant message, used to keep its stats live */ @@ -170,7 +176,7 @@ export interface AgenticFlowParams { options?: AgenticFlowOptions; callbacks: AgenticFlowCallbacks; signal?: AbortSignal; - perChatOverrides?: McpServerOverride[]; + toolPolicy?: AgenticToolPolicy; } /** diff --git a/tools/ui/src/lib/types/database.d.ts b/tools/ui/src/lib/types/database.d.ts index b239aa0251b..57b77f06cd2 100644 --- a/tools/ui/src/lib/types/database.d.ts +++ b/tools/ui/src/lib/types/database.d.ts @@ -1,6 +1,11 @@ -import { AttachmentType, ReasoningEffort } from '$lib/enums'; +import { AttachmentType, ReasoningEffort, ToolSource } from '$lib/enums'; import type { ChatMessageTimings, ChatMessageType, ChatRole } from '$lib/types/chat'; +/** + * @deprecated Legacy per-conversation MCP server flags. MCP server enabled + * state is global now; per-conversation tool policy lives in + * `disabledTools` / `disabledToolCategories`. Read by the migration only. + */ export interface McpServerOverride { serverId: string; enabled: boolean; @@ -11,10 +16,15 @@ export interface DatabaseConversation { id: string; lastModified: number; name: string; + /** @deprecated See {@link McpServerOverride}. Kept on rows for downgrade compatibility. */ mcpServerOverrides?: McpServerOverride[]; thinkingEnabled?: boolean; reasoningEffort?: ReasoningEffort; cwd?: string; + /** Tool keys disabled for this conversation, incl. server-scoped MCP group keys (`mcp:`) */ + disabledTools?: string[]; + /** Tool categories disabled for this conversation */ + disabledToolCategories?: ToolSource[]; forkedFromConversationId?: string; pinned?: boolean; } diff --git a/tools/ui/tests/unit/mcp-override-fallback.test.ts b/tools/ui/tests/unit/mcp-override-fallback.test.ts deleted file mode 100644 index 12ed6e4c4b4..00000000000 --- a/tools/ui/tests/unit/mcp-override-fallback.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { CONFIG_LOCALSTORAGE_KEY, SETTINGS_KEYS } from '$lib/constants'; -import type { DatabaseConversation } from '$lib/types/database'; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; - -// node env unit project has no DOM, install a minimal localStorage backed by a Map -beforeAll(() => { - const store = new Map(); - const polyfill: Storage = { - clear: () => store.clear(), - getItem: (k) => (store.has(k) ? store.get(k)! : null), - key: (i) => Array.from(store.keys())[i] ?? null, - get length() { - return store.size; - }, - removeItem: (k) => { - store.delete(k); - }, - setItem: (k, v) => { - store.set(k, String(v)); - } - }; - - (globalThis as unknown as { localStorage: Storage }).localStorage = polyfill; -}); - -/** - * Regression coverage for the bug where MCP servers flipped to "disabled" - * after sending the first message on a fresh chat (see comment in - * `MCPStore.createConversation`: empty `mcpServerOverrides` should inherit - * `mcpServers[i].enabled`, not be treated as all-off). - */ -describe('conversationsStore MCP override resolution', () => { - beforeEach(async () => { - localStorage.clear(); - // Two configured servers: alpha is globally disabled, bravo enabled. - localStorage.setItem( - CONFIG_LOCALSTORAGE_KEY, - JSON.stringify({ - [SETTINGS_KEYS.MCP_SERVERS]: JSON.stringify([ - { enabled: false, id: 'alpha', url: 'https://alpha.example.com/mcp' }, - { enabled: true, id: 'bravo', url: 'https://bravo.example.com/mcp' } - ]) - }) - ); - - // The settings store constructor bails in node env (no `browser`), - // so seed the config directly. The shape mirrors what `loadConfig` - // would build from localStorage. - const { settingsStore } = await import('$lib/stores/settings/index.svelte'); - const raw = localStorage.getItem(CONFIG_LOCALSTORAGE_KEY) ?? '{}'; - const saved = JSON.parse(raw) as Record; - - settingsStore.config = { - ...settingsStore.config, - [SETTINGS_KEYS.MCP_SERVERS]: saved[SETTINGS_KEYS.MCP_SERVERS] - }; - }); - - afterEach(() => { - localStorage.clear(); - }); - - function makeConversation( - overrides?: { serverId: string; enabled: boolean }[] - ): DatabaseConversation { - return { - currNode: null, - id: 'conv-1', - lastModified: 0, - mcpServerOverrides: overrides, - name: 'Test chat' - }; - } - - it('inherits server.enabled when no conversation is active', async () => { - const { conversationsStore } = await import('$lib/stores/conversations/index.svelte'); - - conversationsStore.activeConversation = null; - - expect(conversationsStore.preferences.isMcpServerEnabledForChat('alpha')).toBe(false); - expect(conversationsStore.preferences.isMcpServerEnabledForChat('bravo')).toBe(true); - }); - - it('inherits server.enabled on a newly created chat with no overrides', async () => { - const { conversationsStore } = await import('$lib/stores/conversations/index.svelte'); - - conversationsStore.activeConversation = makeConversation(); - - // Empty override list: must fall back to global server.enabled, not all-off. - expect(conversationsStore.preferences.isMcpServerEnabledForChat('alpha')).toBe(false); - expect(conversationsStore.preferences.isMcpServerEnabledForChat('bravo')).toBe(true); - }); - - it('inherits server.enabled on a newly created chat when overrides is undefined', async () => { - const { conversationsStore } = await import('$lib/stores/conversations/index.svelte'); - - conversationsStore.activeConversation = makeConversation(undefined); - - expect(conversationsStore.preferences.isMcpServerEnabledForChat('alpha')).toBe(false); - expect(conversationsStore.preferences.isMcpServerEnabledForChat('bravo')).toBe(true); - }); - - it('uses explicit per-chat overrides, with defaults for non-overridden servers', async () => { - const { conversationsStore } = await import('$lib/stores/conversations/index.svelte'); - - // Override flips bravo off for this chat, alpha keeps its global default. - conversationsStore.activeConversation = makeConversation([ - { enabled: false, serverId: 'bravo' } - ]); - - expect(conversationsStore.preferences.isMcpServerEnabledForChat('alpha')).toBe(false); - expect(conversationsStore.preferences.isMcpServerEnabledForChat('bravo')).toBe(false); - }); - - it('getAllMcpServerOverrides returns a complete list merged from defaults', async () => { - const { conversationsStore } = await import('$lib/stores/conversations/index.svelte'); - - conversationsStore.activeConversation = makeConversation([ - { enabled: true, serverId: 'alpha' } - ]); - - expect(conversationsStore.preferences.getAllMcpServerOverrides()).toEqual([ - { enabled: true, serverId: 'alpha' }, - { enabled: true, serverId: 'bravo' } - ]); - }); - - it('getAllMcpServerOverrides falls back to defaults when there are no explicit overrides', async () => { - const { conversationsStore } = await import('$lib/stores/conversations/index.svelte'); - - conversationsStore.activeConversation = makeConversation(); - - expect(conversationsStore.preferences.getAllMcpServerOverrides()).toEqual([ - { enabled: false, serverId: 'alpha' }, - { enabled: true, serverId: 'bravo' } - ]); - }); - - it('getMcpServerOverride returns the global default when the server has no explicit override', async () => { - const { conversationsStore } = await import('$lib/stores/conversations/index.svelte'); - - conversationsStore.activeConversation = makeConversation([ - { enabled: true, serverId: 'alpha' } - ]); - - expect(conversationsStore.preferences.getMcpServerOverride('bravo')).toEqual({ - enabled: true, - serverId: 'bravo' - }); - }); -}); From 38c4721fbd3e857a401c71d0d705b7853a3a4188 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 26 Aug 2026 20:34:00 +0200 Subject: [PATCH 02/34] ui: keep tool policy migration running when defaults parse fails A corrupt disabledToolKeys localStorage entry no longer aborts the migration; it falls through with empty defaults so legacy MCP server overrides still get converted. Assisted-by: pi --- tools/ui/src/lib/services/migration.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ui/src/lib/services/migration.service.ts b/tools/ui/src/lib/services/migration.service.ts index 1b3ecd61209..f78f3be6262 100644 --- a/tools/ui/src/lib/services/migration.service.ts +++ b/tools/ui/src/lib/services/migration.service.ts @@ -762,7 +762,7 @@ const mcpServerOverridesToToolPolicyMigration: Migration = { } } } catch { - return; + // fall through with empty defaults so legacy overrides still migrate } const db = await getDatabaseService(); From 40d590623e19626bbafd428a8252c04c90537802 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 26 Aug 2026 20:34:06 +0200 Subject: [PATCH 03/34] ui: fall back to global defaults when agentic flow has no tool policy Passing empty disabled sets bypassed the global defaults and could enable tools for callers that do not pass a policy yet. Assisted-by: pi --- tools/ui/src/lib/stores/agentic/index.svelte.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/ui/src/lib/stores/agentic/index.svelte.ts b/tools/ui/src/lib/stores/agentic/index.svelte.ts index 4d94519db7b..b58cba15b40 100644 --- a/tools/ui/src/lib/stores/agentic/index.svelte.ts +++ b/tools/ui/src/lib/stores/agentic/index.svelte.ts @@ -334,9 +334,10 @@ class AgenticStore { } } + // callers without an explicit policy fall back to the global defaults const tools = toolsStore.getEnabledToolsForLLM( - new Set(toolPolicy?.disabledTools ?? []), - new Set(toolPolicy?.disabledToolCategories ?? []) + new Set(toolPolicy?.disabledTools ?? toolsStore.disabledTools), + new Set(toolPolicy?.disabledToolCategories ?? toolsStore.disabledToolCategories) ); if (tools.length === 0) { From c7dd200b3749f98ecd01c6766cf7b24688326e1d Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 26 Aug 2026 20:34:23 +0200 Subject: [PATCH 04/34] ui: align preferences section headers with their methods The Reasoning Effort and Working Directory headers sat above tool policy methods; move them above setCwd and setReasoningEffort. Also clarify the disabled tools JSDoc: existing rows with an unset field have an empty policy, defaults apply only when there is no active conversation. Assisted-by: pi --- .../conversations/preferences.svelte.ts | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/tools/ui/src/lib/stores/conversations/preferences.svelte.ts b/tools/ui/src/lib/stores/conversations/preferences.svelte.ts index 8106a0d85a4..5e75f1d7c69 100644 --- a/tools/ui/src/lib/stores/conversations/preferences.svelte.ts +++ b/tools/ui/src/lib/stores/conversations/preferences.svelte.ts @@ -49,12 +49,20 @@ export interface ConversationsPreferencesHost { applyConversationUpdate(id: string, updates: Partial): void; } -/** Effective disabled tool keys: conversation row, falling back to defaults. */ +/** + * Effective disabled tool keys: the active conversation row, or the global + * defaults when there is no conversation. An existing row with an unset + * field has an empty policy, not a fallback to defaults. + */ function buildDisabledTools(conv: DatabaseConversation | null): Set { return new Set(conv ? (conv.disabledTools ?? []) : [...toolsStore.disabledTools]); } -/** Effective disabled tool categories: conversation row, falling back to defaults. */ +/** + * Effective disabled tool categories: the active conversation row, or the + * global defaults when there is no conversation. An existing row with an + * unset field has an empty policy, not a fallback to defaults. + */ function buildDisabledToolCategories(conv: DatabaseConversation | null): Set { return new Set( conv ? (conv.disabledToolCategories ?? []) : [...toolsStore.disabledToolCategories] @@ -184,6 +192,14 @@ export class ConversationPreferences { this.pendingCwd = null; } + /** + * + * + * Working Directory + * + * + */ + /** * Sets the working directory for the active conversation. Pass `null` or * an empty string to clear it, which restores the picker's empty state. @@ -217,6 +233,14 @@ export class ConversationPreferences { this.pendingCwd = null; } + /** + * + * + * Reasoning Effort + * + * + */ + /** * Sets the reasoning effort for the active conversation. * If no conversation exists, stores the global default. @@ -268,14 +292,6 @@ export class ConversationPreferences { await this.toggleTool(toolsStore.getMcpServerToolsKey(serverId)); } - /** - * - * - * Reasoning Effort - * - * - */ - async toggleTool(key: string): Promise { const conv: DatabaseConversation | null = this.host.activeConversation; @@ -305,14 +321,6 @@ export class ConversationPreferences { await DatabaseService.updateConversation(conv.id, { disabledToolCategories }); } - /** - * - * - * Working Directory - * - * - */ - private async persistDisabledTools(disabled: Set): Promise { const conv = this.host.activeConversation; From d018525528eb55d2287c50252a69df8f28a20d63 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 26 Aug 2026 20:34:28 +0200 Subject: [PATCH 05/34] ui: gate MCP server avatars on conversation tool policy Servers whose tools are disabled for the current conversation (MCP category or server-scoped key) no longer show as enabled for the chat. Assisted-by: pi --- .../app/mcp/McpActiveServersAvatars.svelte | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte b/tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte index 5ac7a7fae6a..3319dcf09f1 100644 --- a/tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte +++ b/tools/ui/src/lib/components/app/mcp/McpActiveServersAvatars.svelte @@ -2,8 +2,8 @@ import McpLogo from './McpLogo.svelte'; import * as Tooltip from '$lib/components/ui/tooltip'; import { ICON_CLASS_DEFAULT, MAX_DISPLAYED_MCP_AVATARS } from '$lib/constants'; - import { HealthCheckStatus } from '$lib/enums'; - import { mcpStore } from '$lib/stores'; + import { HealthCheckStatus, ToolSource } from '$lib/enums'; + import { conversationsStore, mcpStore } from '$lib/stores'; interface Props { class?: string; @@ -13,7 +13,15 @@ let { class: className = '', onclick }: Props = $props(); let mcpServers = $derived(mcpStore.getServers().filter((s) => s.enabled)); - let enabledMcpServersForChat = $derived(mcpServers.filter((s) => s.url.trim())); + // respect the active conversation's tool policy, not just global enablement + let enabledMcpServersForChat = $derived( + mcpServers.filter( + (s) => + s.url.trim() && + conversationsStore.preferences.isCategoryEnabled(ToolSource.MCP) && + conversationsStore.preferences.isServerToolsEnabled(s.id) + ) + ); let healthyEnabledMcpServers = $derived( enabledMcpServersForChat.filter((s) => { const healthState = mcpStore.getHealthCheckState(s.id); From 9eb3f7df95d35c296e14851f9a321dd770f72ca1 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 26 Aug 2026 20:48:28 +0200 Subject: [PATCH 06/34] ui: drop unused MCP category toggle from tools panel hook Per-conversation MCP control is server-granular; no component renders a whole-category toggle, so remove the dead API. Assisted-by: pi --- tools/ui/src/lib/hooks/use-tools-panel.svelte.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts b/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts index 2c287a4848a..dac3ba827c2 100644 --- a/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts +++ b/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts @@ -10,7 +10,6 @@ export interface UseToolsPanelReturn { readonly mcpGroups: ToolGroup[]; readonly totalToolCount: number; readonly noToolsInfoMessage: string | null; - readonly mcpCategoryEnabled: boolean; isGroupChecked(group: ToolGroup): boolean; getEnabledToolCount(group: ToolGroup): number; getFavicon(group: ToolGroup): string | null; @@ -21,7 +20,6 @@ export interface UseToolsPanelReturn { toggleGroupExpanded(key: string): void; /** Toggle all tools in a group by its stable key (avoids stale group object references). */ toggleGroupByKey(key: string): void; - toggleMcpCategory(): void; handleOpen(): void; } @@ -43,9 +41,6 @@ export function useToolsPanel(): UseToolsPanelReturn { const categoryGroups = $derived(groups.filter((g) => g.source !== ToolSource.MCP)); const mcpGroups = $derived(groups.filter((g) => g.source === ToolSource.MCP)); const totalToolCount = $derived(groups.reduce((n, g) => n + g.tools.length, 0)); - const mcpCategoryEnabled = $derived( - conversationsStore.preferences.isCategoryEnabled(ToolSource.MCP) - ); const noToolsInfoMessage = $derived.by(() => { if (toolsStore.loading) return null; @@ -113,10 +108,6 @@ export function useToolsPanel(): UseToolsPanelReturn { void conversationsStore.preferences.toggleGroup(group); } - function toggleMcpCategory(): void { - void conversationsStore.preferences.toggleCategory(ToolSource.MCP); - } - function handleOpen(): void { if (toolsStore.serverTools.length === 0 && !toolsStore.loading) { toolsStore.fetchServerTools(); @@ -137,9 +128,6 @@ export function useToolsPanel(): UseToolsPanelReturn { isGroupDisabled, isToolEnabled, isToolParentDisabled, - get mcpCategoryEnabled() { - return mcpCategoryEnabled; - }, get mcpGroups() { return mcpGroups; }, @@ -148,7 +136,6 @@ export function useToolsPanel(): UseToolsPanelReturn { }, toggleGroupByKey, toggleGroupExpanded, - toggleMcpCategory, toggleTool, get totalToolCount() { return totalToolCount; From 617d9aed96464d0a172827cb91e0c50c8051c5d5 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 26 Aug 2026 20:48:34 +0200 Subject: [PATCH 07/34] ui: skip MCP init when flow policy disables the MCP category Resolve the effective tool policy before deciding whether to initialize MCP so flows that will not send any MCP tools skip the init work. Callers without a policy keep falling back to global defaults. Assisted-by: pi --- tools/ui/src/lib/stores/agentic/index.svelte.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/ui/src/lib/stores/agentic/index.svelte.ts b/tools/ui/src/lib/stores/agentic/index.svelte.ts index b58cba15b40..3d47c50d865 100644 --- a/tools/ui/src/lib/stores/agentic/index.svelte.ts +++ b/tools/ui/src/lib/stores/agentic/index.svelte.ts @@ -324,7 +324,14 @@ class AgenticStore { if (!agenticConfig.enabled) return { handled: false }; - const hasMcpServers = mcpStore.hasEnabledServers(); + // callers without an explicit policy fall back to the global defaults + const disabledTools = new Set(toolPolicy?.disabledTools ?? toolsStore.disabledTools); + const disabledToolCategories = new Set( + toolPolicy?.disabledToolCategories ?? toolsStore.disabledToolCategories + ); + // skip MCP init when the policy disables the whole MCP category + const hasMcpServers = + mcpStore.hasEnabledServers() && !disabledToolCategories.has(ToolSource.MCP); if (hasMcpServers) { const initialized = await mcpStore.ensureInitialized(); @@ -334,11 +341,7 @@ class AgenticStore { } } - // callers without an explicit policy fall back to the global defaults - const tools = toolsStore.getEnabledToolsForLLM( - new Set(toolPolicy?.disabledTools ?? toolsStore.disabledTools), - new Set(toolPolicy?.disabledToolCategories ?? toolsStore.disabledToolCategories) - ); + const tools = toolsStore.getEnabledToolsForLLM(disabledTools, disabledToolCategories); if (tools.length === 0) { return { handled: false }; From f1a9607d0cec6391bbfd79dd2e8e5be471cb13bc Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 26 Aug 2026 21:00:18 +0200 Subject: [PATCH 08/34] chore: format --- .../src/lib/components/app/settings/SettingsMcpServers.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte b/tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte index 127a035ad50..c0cf0000d3f 100644 --- a/tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte +++ b/tools/ui/src/lib/components/app/settings/SettingsMcpServers.svelte @@ -86,7 +86,6 @@ {:else} (isResourcesDialogOpen = true)} onDelete={() => mcpStore.removeServer(server.id)} @@ -103,6 +102,7 @@ } }} onUpdate={(updates) => mcpStore.updateServer(server.id, updates)} + {server} /> {/if} {/each} From f51b3cd95cf7c17aa8457837f3ee4b6492d53881 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 06:35:01 +0200 Subject: [PATCH 09/34] ui: restore reasoning section in mobile add sheet The sheet rewrite dropped it; the desktop dropdown still has it. MCP Prompts and Resources stay out of the sheet on purpose. Assisted-by: pi --- .../ChatFormActionAddSheet.svelte | 74 ++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte index e8da0af6b9e..761ce8052cc 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte @@ -1,6 +1,14 @@ - - - - - - MCP - - - - - - - Servers - - - {#if chatFormActions.hasMcpPromptsSupport} - - - - Prompts - - {/if} - - {#if chatFormActions.hasMcpResourcesSupport} - - - - Resources - - {/if} - - diff --git a/tools/ui/src/lib/components/app/chat/index.ts b/tools/ui/src/lib/components/app/chat/index.ts index 61ec242e907..d7d7745df1f 100644 --- a/tools/ui/src/lib/components/app/chat/index.ts +++ b/tools/ui/src/lib/components/app/chat/index.ts @@ -220,19 +220,6 @@ export { default as ChatFormActionModels } from './ChatForm/ChatFormActions/Chat */ export { default as ChatFormActionAddToolsSubmenu } from './ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte'; -/** - * Dropdown submenu for MCP prompts and resources in the chat form. - * - * Shows an "MCP" sub-menu item with entries for MCP Prompts and MCP - * Resources. Only visible when the server supports them. - * - * @example - * ```svelte - * - * ``` - */ -export { default as ChatFormActionAddMcpSubmenu } from './ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddMcpSubmenu.svelte'; - /** * Dropdown submenu for selecting reasoning effort level. * From a72d3f12c88103dbce9c63e798468a692affd428 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 08:06:25 +0200 Subject: [PATCH 18/34] ui: fix model information dialog width on all screen sizes The dialog sets container-type: inline-size, so auto width ignores its contents and collapses to padding. Give it an explicit viewport width on mobile and cap at 60rem on desktop. Assisted-by: pi --- .../lib/components/app/dialogs/DialogModelInformation.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ui/src/lib/components/app/dialogs/DialogModelInformation.svelte b/tools/ui/src/lib/components/app/dialogs/DialogModelInformation.svelte index 811c24d6b79..c962e7613af 100644 --- a/tools/ui/src/lib/components/app/dialogs/DialogModelInformation.svelte +++ b/tools/ui/src/lib/components/app/dialogs/DialogModelInformation.svelte @@ -76,7 +76,7 @@ - + - + Model Information Current model details and capabilities From 20b0733ceed6b8ac40952ec54adb7399a5e88bb4 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 09:50:52 +0200 Subject: [PATCH 24/34] ui: replace literal comment text in sheet group snippet A // line inside the Svelte snippet rendered as visible text; use an HTML comment. Assisted-by: pi --- .../ChatFormActionAdd/ChatFormActionAddSheet.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte index 762cc1386fd..c78b42ea882 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte @@ -252,7 +252,7 @@ {#snippet sheetGroupRow(group: ToolGroup)} {@const checked = toolsPanel.isGroupChecked(group)} {@const enabledCount = toolsPanel.getEnabledToolCount(group)} - // parent on but nothing under it enabled, or partially enabled: show mixed state + {@const indeterminate = group.tools.length > 0 && (enabledCount === 0 ? checked : enabledCount < group.tools.length)} {@const favicon = toolsPanel.getFavicon(group)} From b520daf9a719123745de7b5558f0828deffbdbf9 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 09:50:58 +0200 Subject: [PATCH 25/34] ui: let indeterminate state win over checked in group checkboxes The checkbox indicator snippet renders the check icon whenever checked, so the mixed state never showed. Pass the checked prop as false while indeterminate. Assisted-by: pi --- .../ChatFormActionAdd/ChatFormActionAddSheet.svelte | 2 +- .../ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte index c78b42ea882..6b4f12376f6 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte @@ -281,7 +281,7 @@ toolsPanel.toggleGroupByKey(group.key)} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte index b6e539b3450..6f32ad09204 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte @@ -79,7 +79,7 @@ {@const isExpanded = toolsPanel.expandedGroups.has(group.key)} {@const checked = toolsPanel.isGroupChecked(group)} {@const enabledCount = toolsPanel.getEnabledToolCount(group)} - // parent on but nothing under it enabled, or partially enabled: show mixed state + {@const indeterminate = group.tools.length > 0 && (enabledCount === 0 ? checked : enabledCount < group.tools.length)} {@const favicon = toolsPanel.getFavicon(group)} @@ -124,7 +124,7 @@ {#snippet child({ props })} toolsPanel.toggleGroupByKey(group.key)} From cbace8af70abcdddb374aa81bd6b500ea3dc1024 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 09:51:08 +0200 Subject: [PATCH 26/34] ui: initialize only policy-enabled MCP servers for a flow ensureInitialized accepts an optional server id set; the agentic flow passes the servers its tool policy leaves usable, so servers disabled for the conversation no longer get connected. Callers without arguments keep the global behavior. Assisted-by: pi --- .../ui/src/lib/stores/agentic/index.svelte.ts | 22 ++++++++++--------- tools/ui/src/lib/stores/mcp/index.svelte.ts | 17 +++++++++++--- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/tools/ui/src/lib/stores/agentic/index.svelte.ts b/tools/ui/src/lib/stores/agentic/index.svelte.ts index bbf2526ce0c..bf39b5e34f7 100644 --- a/tools/ui/src/lib/stores/agentic/index.svelte.ts +++ b/tools/ui/src/lib/stores/agentic/index.svelte.ts @@ -329,18 +329,20 @@ class AgenticStore { const disabledToolCategories = new Set( toolPolicy?.disabledToolCategories ?? toolsStore.disabledToolCategories ); - // skip MCP init when the policy leaves no usable server: either the whole - // MCP category is off, or every globally-enabled server has its group key - // disabled for this flow - const hasMcpServers = - mcpStore.hasEnabledServers() && - !disabledToolCategories.has(ToolSource.MCP) && - mcpStore - .getServers() - .some((s) => s.enabled && !disabledTools.has(toolsStore.getMcpServerToolsKey(s.id))); + // servers usable under this flow's policy: globally enabled, category on, + // and their server-scoped group key not disabled + const policyEnabledServerIds = new Set( + disabledToolCategories.has(ToolSource.MCP) + ? [] + : mcpStore + .getServers() + .filter((s) => s.enabled && !disabledTools.has(toolsStore.getMcpServerToolsKey(s.id))) + .map((s) => s.id) + ); + const hasMcpServers = policyEnabledServerIds.size > 0; if (hasMcpServers) { - const initialized = await mcpStore.ensureInitialized(); + const initialized = await mcpStore.ensureInitialized(policyEnabledServerIds); if (!initialized) { console.log('[AgenticStore] MCP not initialized'); diff --git a/tools/ui/src/lib/stores/mcp/index.svelte.ts b/tools/ui/src/lib/stores/mcp/index.svelte.ts index 0b9eddf6bf8..cb8be06c82c 100644 --- a/tools/ui/src/lib/stores/mcp/index.svelte.ts +++ b/tools/ui/src/lib/stores/mcp/index.svelte.ts @@ -306,12 +306,18 @@ class MCPStore implements McpHealthHost { return extras; } - async ensureInitialized(): Promise { + /** + * Initialize connections. Callers can restrict which servers connect by + * passing their ids (e.g. a conversation's tool policy); a different set + * changes the config signature and re-initializes, same as a settings + * change. + */ + async ensureInitialized(serverIds?: ReadonlySet): Promise { if (!browser) { return false; } - const mcpConfig = this.buildMcpClientConfig(settingsStore.config); + const mcpConfig = this.buildMcpClientConfig(settingsStore.config, serverIds); const signature = mcpConfig ? JSON.stringify(mcpConfig) : null; if (!signature) { @@ -1157,7 +1163,10 @@ class MCPStore implements McpHealthHost { /** * Builds MCP client configuration from settings. */ - private buildMcpClientConfig(cfg: SettingsConfigType): MCPClientConfig | undefined { + private buildMcpClientConfig( + cfg: SettingsConfigType, + serverIds?: ReadonlySet + ): MCPClientConfig | undefined { const rawServers = parseMcpServerSettings(cfg.mcpServers); if (!rawServers.length) { @@ -1169,6 +1178,8 @@ class MCPStore implements McpHealthHost { for (const [index, entry] of rawServers.entries()) { if (!entry.enabled) continue; + if (serverIds && !serverIds.has(entry.id)) continue; + const normalized = this.buildServerConfig(entry); if (normalized) servers[this.generateServerId(entry.id, index)] = normalized; From e415366eba8c6ae54ac6c7c9d495c57214e69906 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 09:54:09 +0200 Subject: [PATCH 27/34] ui: derive group checkbox state in useToolsPanel Moves the mixed-state derivation out of the submenu and sheet snippets into one getGroupCheckState accessor; the snippets just consume checked and indeterminate. Assisted-by: pi --- .../ChatFormActionAddSheet.svelte | 9 +++------ .../ChatFormActionAddToolsSubmenu.svelte | 12 ++++-------- .../ui/src/lib/hooks/use-tools-panel.svelte.ts | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte index 6b4f12376f6..2dbc7fe7747 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte @@ -250,11 +250,8 @@ {#snippet sheetGroupRow(group: ToolGroup)} - {@const checked = toolsPanel.isGroupChecked(group)} + {@const checkState = toolsPanel.getGroupCheckState(group)} {@const enabledCount = toolsPanel.getEnabledToolCount(group)} - - {@const indeterminate = - group.tools.length > 0 && (enabledCount === 0 ? checked : enabledCount < group.tools.length)} {@const favicon = toolsPanel.getFavicon(group)} {@const groupDisabled = toolsPanel.isGroupDisabled(group)} @@ -281,9 +278,9 @@ toolsPanel.toggleGroupByKey(group.key)} onclick={(e) => e.stopPropagation()} /> diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte index 6f32ad09204..f4954417148 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte @@ -77,11 +77,7 @@ {#snippet groupRow(group: ToolGroup)} {@const isExpanded = toolsPanel.expandedGroups.has(group.key)} - {@const checked = toolsPanel.isGroupChecked(group)} - {@const enabledCount = toolsPanel.getEnabledToolCount(group)} - - {@const indeterminate = - group.tools.length > 0 && (enabledCount === 0 ? checked : enabledCount < group.tools.length)} + {@const checkState = toolsPanel.getGroupCheckState(group)} {@const favicon = toolsPanel.getFavicon(group)} {@const groupDisabled = toolsPanel.isGroupDisabled(group)} @@ -124,9 +120,9 @@ {#snippet child({ props })} toolsPanel.toggleGroupByKey(group.key)} /> {/snippet} @@ -134,7 +130,7 @@

- {checked ? 'Disable' : 'Enable'} + {checkState.checked ? 'Disable' : 'Enable'} {group.tools.length} tool{group.tools.length !== 1 ? 's' : ''}

diff --git a/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts b/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts index dac3ba827c2..21deed32d30 100644 --- a/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts +++ b/tools/ui/src/lib/hooks/use-tools-panel.svelte.ts @@ -12,6 +12,7 @@ export interface UseToolsPanelReturn { readonly noToolsInfoMessage: string | null; isGroupChecked(group: ToolGroup): boolean; getEnabledToolCount(group: ToolGroup): number; + getGroupCheckState(group: ToolGroup): { checked: boolean; indeterminate: boolean }; getFavicon(group: ToolGroup): string | null; isGroupDisabled(group: ToolGroup): boolean; isToolEnabled(entry: ToolEntry): boolean; @@ -65,6 +66,22 @@ export function useToolsPanel(): UseToolsPanelReturn { return group.tools.filter((tool) => conversationsStore.preferences.isToolActive(tool)).length; } + /** + * Group checkbox state: checked is the parent flag (category on, or the + * server key on for MCP groups); indeterminate marks the mixed case where + * the parent is on but nothing or only part of the group is enabled. + * isToolActive folds the parent gates into the count, so a disabled parent + * always yields plain unchecked. + */ + function getGroupCheckState(group: ToolGroup): { checked: boolean; indeterminate: boolean } { + const checked = isGroupChecked(group); + const enabledCount = getEnabledToolCount(group); + const indeterminate = + group.tools.length > 0 && (enabledCount === 0 ? checked : enabledCount < group.tools.length); + + return { checked, indeterminate }; + } + function getFavicon(group: ToolGroup): string | null { if (group.source !== ToolSource.MCP || !group.serverId) return null; @@ -123,6 +140,7 @@ export function useToolsPanel(): UseToolsPanelReturn { expandedGroups, getEnabledToolCount, getFavicon, + getGroupCheckState, handleOpen, isGroupChecked, isGroupDisabled, From 2e8cce869e9eea7d4b60b89c9d12c61e756aead5 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 10:04:55 +0200 Subject: [PATCH 28/34] ui: gate /prompt command on the conversation tool policy The slash command's availability now follows the same rule as the agentic flow instead of the global capability check, so it disables itself when the conversation's policy leaves no usable MCP server. Assisted-by: pi --- .../components/app/chat/ChatForm/ChatForm.svelte | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte index 006ec39ce95..75efa7aa0d6 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -23,7 +23,8 @@ FileExtensionText, KeyboardKey, MimeTypeText, - SpecialFileType + SpecialFileType, + ToolSource } from '$lib/enums'; import { useChatFormPickers } from '$lib/hooks/use-chat-form-pickers.svelte'; import { @@ -153,7 +154,17 @@ getShowModelSelector: () => showModelSelector, getValue: () => value, hasCwdTools: () => conversationsStore.preferences.hasEnabledCwdTools(), - hasPrompts: () => mcpStore.hasPromptsCapability(), + // policy-aware, same rule as the agentic flow: MCP category on and at + // least one globally-enabled server whose group key is not disabled + hasPrompts: () => { + const prefs = conversationsStore.preferences; + + if (!prefs.isCategoryEnabled(ToolSource.MCP)) return false; + + return mcpStore + .getServers() + .some((s) => s.enabled && prefs.isServerToolsEnabled(s.id) && s.url.trim()); + }, openModelSelector: () => chatFormActionsRef?.openModelSelector(), setCaretOffset: (offset) => inputRef?.setCaretOffset(offset), setValue: (v) => { From dc2dd9913c02986ad63727c968a31564789b344c Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 10:15:42 +0200 Subject: [PATCH 29/34] ui: remove dead MCP prompt menu trigger chain The /prompt slash command is the surviving trigger; the menu-button path (onMcpPromptClick, hasMcpPromptsSupport, showMcpPromptButton, the MCP_PROMPT attachment item and its unrendered item arrays) has no consumer left. Message display for inserted prompts is untouched. Assisted-by: pi --- .../app/chat/ChatForm/ChatForm.svelte | 3 -- .../ChatFormActionAddDropdown.svelte | 2 - .../ChatFormActionAddSheet.svelte | 2 - .../ChatFormActions/ChatFormActions.svelte | 10 ----- .../ChatMessages/ChatMessageEditForm.svelte | 1 - .../app/chat/ChatScreen/ChatScreenForm.svelte | 1 - .../constants/attachment-menu.constants.ts | 40 +---------------- tools/ui/src/lib/enums/attachment.enums.ts | 3 -- .../lib/hooks/use-attachment-menu.svelte.ts | 3 -- tools/ui/src/lib/stores/mcp/index.svelte.ts | 44 ------------------- tools/ui/src/lib/types/chat.d.ts | 2 - 11 files changed, 1 insertion(+), 110 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte index 75efa7aa0d6..8572f7ccb39 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -74,7 +74,6 @@ disabled?: boolean; isLoading?: boolean; placeholder?: string; - showMcpPromptButton?: boolean; showAddButton?: boolean; showModelSelector?: boolean; @@ -104,7 +103,6 @@ onValueChange, placeholder = 'Type a message...', showAddButton = true, - showMcpPromptButton = false, showModelSelector = true, uploadedFiles = $bindable([]), value = $bindable('') @@ -630,7 +628,6 @@ isReasoning={chatStore.isReasoning} {isRecording} onFileUpload={handleFileUpload} - onMcpPromptClick={showMcpPromptButton ? () => pickers.openPromptPicker() : undefined} onMcpResourcesClick={() => (isResourceDialogOpen = true)} onMcpSettingsClick={() => (isMcpServersDialogOpen = true)} onMicClick={handleMicClick} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte index 5ff3c6a9085..8e00deee056 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte @@ -30,14 +30,12 @@ const attachmentMenu = useAttachmentMenu( () => ({ hasAudioModality: chatFormActions.hasAudioModality, - hasMcpPromptsSupport: chatFormActions.hasMcpPromptsSupport, hasMcpResourcesSupport: chatFormActions.hasMcpResourcesSupport, hasVideoModality: chatFormActions.hasVideoModality, hasVisionModality: chatFormActions.hasVisionModality }), () => ({ onFileUpload: chatFormActions.onFileUpload, - onMcpPromptClick: chatFormActions.onMcpPromptClick, onMcpResourcesClick: chatFormActions.onMcpResourcesClick, onSystemPromptClick: chatFormActions.onSystemPromptClick }), diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte index 2dbc7fe7747..3b665143d78 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte @@ -44,14 +44,12 @@ const attachmentMenu = useAttachmentMenu( () => ({ hasAudioModality: chatFormActions.hasAudioModality, - hasMcpPromptsSupport: chatFormActions.hasMcpPromptsSupport, hasMcpResourcesSupport: chatFormActions.hasMcpResourcesSupport, hasVideoModality: chatFormActions.hasVideoModality, hasVisionModality: chatFormActions.hasVisionModality }), () => ({ onFileUpload: chatFormActions.onFileUpload, - onMcpPromptClick: chatFormActions.onMcpPromptClick, onMcpResourcesClick: chatFormActions.onMcpResourcesClick, onSystemPromptClick: chatFormActions.onSystemPromptClick }), diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte index 33b129f83ef..5ab973c9a4c 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte @@ -31,7 +31,6 @@ onMicClick?: () => void; onStop?: () => void; onSystemPromptClick?: () => void; - onMcpPromptClick?: () => void; onMcpResourcesClick?: () => void; onMcpSettingsClick?: () => void; } @@ -45,7 +44,6 @@ isReasoning = false, isRecording = false, onFileUpload, - onMcpPromptClick, onMcpResourcesClick, onMcpSettingsClick, onMicClick, @@ -73,8 +71,6 @@ ); }); - let hasMcpPromptsSupport = $derived(mcpStore.hasPromptsCapability(policyEnabledMcpServerIds)); - let hasMcpResourcesSupport = $derived(mcpStore.hasResourcesCapability(policyEnabledMcpServerIds)); let hasAudioModality = $state(false); @@ -149,9 +145,6 @@ get hasAudioModality() { return hasAudioModality; }, - get hasMcpPromptsSupport() { - return hasMcpPromptsSupport; - }, get hasMcpResourcesSupport() { return hasMcpResourcesSupport; }, @@ -164,9 +157,6 @@ get onFileUpload() { return onFileUpload; }, - get onMcpPromptClick() { - return onMcpPromptClick; - }, get onMcpResourcesClick() { return onMcpResourcesClick; }, diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte index 41d79387b6b..6e30cebec6b 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte @@ -111,7 +111,6 @@ onValueChange={editCtx.setContent} placeholder="Edit your message..." showAddButton={editCtx.messageRole === MessageRole.USER} - showMcpPromptButton showModelSelector={editCtx.messageRole === MessageRole.USER} value={editCtx.editedContent} /> diff --git a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenForm.svelte b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenForm.svelte index 9825b4b90b3..962b6774fcf 100644 --- a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenForm.svelte @@ -160,6 +160,5 @@ onSubmit={handleSubmit} onSystemPromptClick={handleSystemPromptClick} onUploadedFileRemove={handleUploadedFileRemove} - showMcpPromptButton /> diff --git a/tools/ui/src/lib/constants/attachment-menu.constants.ts b/tools/ui/src/lib/constants/attachment-menu.constants.ts index 07ca17fad15..143450a7524 100644 --- a/tools/ui/src/lib/constants/attachment-menu.constants.ts +++ b/tools/ui/src/lib/constants/attachment-menu.constants.ts @@ -1,11 +1,5 @@ -import { FolderOpen, MessageSquare, Zap } from '@lucide/svelte'; import { FILE_TYPE_ICONS } from '$lib/constants'; -import { - AttachmentAction, - AttachmentItemEnabledWhen, - AttachmentItemVisibleWhen, - AttachmentMenuItemId -} from '$lib/enums'; +import { AttachmentAction, AttachmentItemEnabledWhen, AttachmentMenuItemId } from '$lib/enums'; import type { AttachmentMenuItem } from '$lib/types'; /** @@ -58,36 +52,4 @@ export const ATTACHMENT_FILE_ITEMS: AttachmentMenuItem[] = [ } ]; -export const ATTACHMENT_EXTRA_ITEMS: AttachmentMenuItem[] = []; - -export const ATTACHMENT_PROMPT_ITEMS: AttachmentMenuItem[] = [ - { - action: AttachmentAction.SYSTEM_PROMPT_CLICK, - enabledWhen: AttachmentItemEnabledWhen.ALWAYS, - hasEnabledTooltip: true, - icon: MessageSquare, - id: AttachmentMenuItemId.SYSTEM_MESSAGE, - label: 'System Message' - }, - { - action: AttachmentAction.MCP_PROMPT_CLICK, - enabledWhen: AttachmentItemEnabledWhen.ALWAYS, - icon: Zap, - id: AttachmentMenuItemId.MCP_PROMPT, - label: 'MCP Prompts', - visibleWhen: AttachmentItemVisibleWhen.HAS_MCP_PROMPTS_SUPPORT - } -]; - -export const ATTACHMENT_MCP_ITEMS: AttachmentMenuItem[] = [ - { - action: AttachmentAction.MCP_RESOURCES_CLICK, - enabledWhen: AttachmentItemEnabledWhen.ALWAYS, - icon: FolderOpen, - id: AttachmentMenuItemId.MCP_RESOURCES, - label: 'MCP Resources', - visibleWhen: AttachmentItemVisibleWhen.HAS_MCP_RESOURCES_SUPPORT - } -]; - export const ATTACHMENT_TOOLTIP_TEXT = 'Add files, prompts, tools or MCP Servers'; diff --git a/tools/ui/src/lib/enums/attachment.enums.ts b/tools/ui/src/lib/enums/attachment.enums.ts index 70ed36d89f9..3b7443eed3c 100644 --- a/tools/ui/src/lib/enums/attachment.enums.ts +++ b/tools/ui/src/lib/enums/attachment.enums.ts @@ -19,7 +19,6 @@ export enum AttachmentType { export enum AttachmentMenuItemId { AUDIO = 'audio', IMAGES = 'images', - MCP_PROMPT = 'mcp-prompt', MCP_RESOURCES = 'mcp-resources', PDF = 'pdf', SYSTEM_MESSAGE = 'system-message', @@ -42,7 +41,6 @@ export enum AttachmentItemEnabledWhen { */ export enum AttachmentAction { FILE_UPLOAD = 'onFileUpload', - MCP_PROMPT_CLICK = 'onMcpPromptClick', MCP_RESOURCES_CLICK = 'onMcpResourcesClick', SYSTEM_PROMPT_CLICK = 'onSystemPromptClick' } @@ -61,6 +59,5 @@ export enum AttachmentLabel { * Visibility conditions for attachment menu items. */ export enum AttachmentItemVisibleWhen { - HAS_MCP_PROMPTS_SUPPORT = 'hasMcpPromptsSupport', HAS_MCP_RESOURCES_SUPPORT = 'hasMcpResourcesSupport' } diff --git a/tools/ui/src/lib/hooks/use-attachment-menu.svelte.ts b/tools/ui/src/lib/hooks/use-attachment-menu.svelte.ts index 98ecc9ace0a..3940d7dbb0d 100644 --- a/tools/ui/src/lib/hooks/use-attachment-menu.svelte.ts +++ b/tools/ui/src/lib/hooks/use-attachment-menu.svelte.ts @@ -5,14 +5,12 @@ export interface AttachmentModalityFlags { hasVisionModality: boolean; hasAudioModality: boolean; hasVideoModality: boolean; - hasMcpPromptsSupport: boolean; hasMcpResourcesSupport: boolean; } export interface AttachmentActionCallbacks { onFileUpload?: () => void; onSystemPromptClick?: () => void; - onMcpPromptClick?: () => void; onMcpResourcesClick?: () => void; } @@ -49,7 +47,6 @@ export function useAttachmentMenu( return { [AttachmentAction.FILE_UPLOAD]: wrap(cbs.onFileUpload), - [AttachmentAction.MCP_PROMPT_CLICK]: wrap(cbs.onMcpPromptClick), [AttachmentAction.MCP_RESOURCES_CLICK]: wrap(cbs.onMcpResourcesClick), [AttachmentAction.SYSTEM_PROMPT_CLICK]: wrap(cbs.onSystemPromptClick) }; diff --git a/tools/ui/src/lib/stores/mcp/index.svelte.ts b/tools/ui/src/lib/stores/mcp/index.svelte.ts index cb8be06c82c..6a952294a37 100644 --- a/tools/ui/src/lib/stores/mcp/index.svelte.ts +++ b/tools/ui/src/lib/stores/mcp/index.svelte.ts @@ -813,50 +813,6 @@ class MCPStore implements McpHealthHost { return Boolean(this.buildMcpClientConfig(settingsStore.config)); } - /** - * Check if any enabled server with successful health check supports prompts. - * Uses health check state since servers may not have active connections until - * the user actually sends a message or uses prompts. - */ - hasPromptsCapability(enabledServerIds?: ReadonlySet): boolean { - const ids = enabledServerIds ?? this.globalEnabledServerIds(); - - if (ids.size === 0) { - return false; - } - - for (const [serverId, state] of Object.entries(this.health.checks)) { - if (!ids.has(serverId)) continue; - - if ( - state.status === HealthCheckStatus.SUCCESS && - state.capabilities?.server?.prompts !== undefined - ) { - return true; - } - } - - for (const [serverName, connection] of this.connections) { - if (!ids.has(serverName)) continue; - - if (connection.serverCapabilities?.prompts) { - return true; - } - } - - return false; - } - - hasPromptsSupport(): boolean { - for (const connection of this.connections.values()) { - if (connection.serverCapabilities?.prompts) { - return true; - } - } - - return false; - } - /** * Check if any enabled server with successful health check supports resources. * Uses health check state since servers may not have active connections until diff --git a/tools/ui/src/lib/types/chat.d.ts b/tools/ui/src/lib/types/chat.d.ts index f0f3a297e8e..e41a1df0c4d 100644 --- a/tools/ui/src/lib/types/chat.d.ts +++ b/tools/ui/src/lib/types/chat.d.ts @@ -336,11 +336,9 @@ export interface ChatFormActionsContext { readonly hasAudioModality: boolean; readonly hasVideoModality: boolean; readonly hasVisionModality: boolean; - readonly hasMcpPromptsSupport: boolean; readonly hasMcpResourcesSupport: boolean; onFileUpload?: () => void; onSystemPromptClick?: () => void; - onMcpPromptClick?: () => void; onMcpResourcesClick?: () => void; onMcpSettingsClick?: () => void; } From cf39a070adbf5ad015d071406d5eace00445ff8a Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 10:18:01 +0200 Subject: [PATCH 30/34] ui: render dash for mixed-state group checkboxes The accessor refactor dropped the checked-and-not-indeterminate guard, so the category-on flag won and the dash never showed. The tooltip keeps using the raw parent flag since clicking a mixed group still disables it. Assisted-by: pi --- .../ChatFormActionAdd/ChatFormActionAddSheet.svelte | 2 +- .../ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte index 3b665143d78..5fcacf4b854 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte @@ -276,7 +276,7 @@ toolsPanel.toggleGroupByKey(group.key)} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte index f4954417148..89c05d17a8f 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte @@ -120,7 +120,7 @@ {#snippet child({ props })} toolsPanel.toggleGroupByKey(group.key)} From 85acaa0626dc096432a7db2e6831be42d2816f79 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 10:38:13 +0200 Subject: [PATCH 31/34] ui: fix group checkbox sticking checked after disable Clicking a mixed-state group box let bits-ui optimistically flip its internal checked flag; the derived checked prop did not change across the transition (both mixed and off map to checked=false), so Svelte never applied the settled value and the check icon stuck while the count already read 0/7. Pass the parent flag as checked and the mix as indeterminate, so every group toggle changes checked; render the dash on top of a checked box for the mixed state. Assisted-by: pi --- .../ChatFormActionAdd/ChatFormActionAddSheet.svelte | 2 +- .../ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte | 2 +- tools/ui/src/lib/components/ui/checkbox/checkbox.svelte | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte index 5fcacf4b854..3b665143d78 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte @@ -276,7 +276,7 @@ toolsPanel.toggleGroupByKey(group.key)} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte index 89c05d17a8f..f4954417148 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddToolsSubmenu.svelte @@ -120,7 +120,7 @@ {#snippet child({ props })} toolsPanel.toggleGroupByKey(group.key)} diff --git a/tools/ui/src/lib/components/ui/checkbox/checkbox.svelte b/tools/ui/src/lib/components/ui/checkbox/checkbox.svelte index ec6d28826e5..60ef646fc41 100644 --- a/tools/ui/src/lib/components/ui/checkbox/checkbox.svelte +++ b/tools/ui/src/lib/components/ui/checkbox/checkbox.svelte @@ -26,10 +26,10 @@ > {#snippet children({ checked, indeterminate })}
- {#if checked} - - {:else if indeterminate} + {#if indeterminate} + {:else if checked} + {/if}
{/snippet} From 9b521a7cafec2c6b57237f554558ef3d1cd82a0c Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Aug 2026 11:00:27 +0200 Subject: [PATCH 32/34] fix: UI for Model Information dialog --- .../app/dialogs/DialogModelInformation.svelte | 155 ++++++++++++++---- 1 file changed, 123 insertions(+), 32 deletions(-) diff --git a/tools/ui/src/lib/components/app/dialogs/DialogModelInformation.svelte b/tools/ui/src/lib/components/app/dialogs/DialogModelInformation.svelte index c289baa665a..e200c004e58 100644 --- a/tools/ui/src/lib/components/app/dialogs/DialogModelInformation.svelte +++ b/tools/ui/src/lib/components/app/dialogs/DialogModelInformation.svelte @@ -77,23 +77,18 @@ - + + - - Model Information +
+
+ Model Information - Current model details and capabilities - + Current model details and capabilities +
-
{#if isLoadingModels || isLoadingRouterProps}
Loading model information...
@@ -102,17 +97,15 @@ {@const modelMeta = firstModel.meta} {#if serverProps} - + +