webui: fix tool selector toggle/counter, key tools by stable identity (#24065)

* webui: fix tool selector toggle/counter, key tools by stable identity

Key the disabled set, counts and toggles by a stable per-tool key
instead of bare function name, deduped from one canonical list. Per-tool
checkboxes become presentational (single row handler, no nested button),
category checkboxes drop the tristate (n/total carries partial). One
getEnabledToolsForLLM keeps normalized MCP schemas and dedupes by name.

* ui: use SvelteSet and SvelteMap for local tool collections to satisfy svelte/prefer-svelte-reactivity
This commit is contained in:
Pascal
2026-06-04 13:09:49 +02:00
committed by GitHub
parent 4d742877b2
commit 4586479852
7 changed files with 203 additions and 240 deletions
@@ -231,7 +231,7 @@
<Collapsible.Content>
<div class="flex flex-col gap-0.5 pl-4">
{#each toolsPanel.activeGroups as group (group.label)}
{@const { checked, indeterminate } = toolsPanel.getGroupCheckedState(group)}
{@const checked = toolsPanel.isGroupChecked(group)}
{@const enabledCount = toolsPanel.getEnabledToolCount(group)}
{@const favicon = toolsPanel.getFavicon(group)}
@@ -259,7 +259,6 @@
<Checkbox
{checked}
{indeterminate}
class="h-4 w-4 shrink-0"
onclick={(e) => e.stopPropagation()}
onCheckedChange={() => toolsPanel.toggleGroupByLabel(group.label)}
@@ -1,5 +1,5 @@
<script lang="ts">
import { PencilRuler, ChevronDown, ChevronRight, Loader2, Info } from '@lucide/svelte';
import { PencilRuler, ChevronDown, ChevronRight, Loader2, Info, Check } from '@lucide/svelte';
import { Checkbox } from '$lib/components/ui/checkbox';
import * as Collapsible from '$lib/components/ui/collapsible';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
@@ -65,7 +65,7 @@
<div class="max-h-80 overflow-y-auto p-2 pr-1">
{#each toolsPanel.activeGroups as group (group.label)}
{@const isExpanded = toolsPanel.expandedGroups.has(group.label)}
{@const { checked, indeterminate } = toolsPanel.getGroupCheckedState(group)}
{@const checked = toolsPanel.isGroupChecked(group)}
{@const favicon = toolsPanel.getFavicon(group)}
<Collapsible.Root
@@ -104,12 +104,14 @@
<Tooltip.Root>
<Tooltip.Trigger>
<Checkbox
{checked}
{indeterminate}
onCheckedChange={() => toolsPanel.toggleGroupByLabel(group.label)}
class="mr-2 h-4 w-4 shrink-0"
/>
{#snippet child({ props })}
<Checkbox
{...props}
{checked}
onCheckedChange={() => toolsPanel.toggleGroupByLabel(group.label)}
class="mr-2 h-4 w-4 shrink-0"
/>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content side="right">
@@ -123,20 +125,25 @@
<Collapsible.Content>
<div class="ml-4 flex flex-col gap-0.5 border-l border-border/50 pl-2">
{#each group.tools as tool (tool.function.name)}
{#each group.tools as entry (entry.key)}
{@const enabled = toolsStore.isToolEnabled(entry.key)}
<button
type="button"
class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted/50"
onclick={() => toolsStore.toggleTool(tool.function.name)}
onclick={() => toolsStore.toggleTool(entry.key)}
>
<Checkbox
checked={toolsStore.isToolEnabled(tool.function.name)}
onCheckedChange={() => toolsStore.toggleTool(tool.function.name)}
class="h-4 w-4 shrink-0"
/>
<span
data-slot="checkbox"
data-state={enabled ? 'checked' : 'unchecked'}
class="flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
>
{#if enabled}
<Check class="size-3.5" />
{/if}
</span>
<span class="min-w-0 flex-1 truncate font-mono text-[12px]">
{tool.function.name}
{entry.definition.function.name}
</span>
</button>
{/each}
@@ -62,13 +62,11 @@
<span class="w-20 shrink-0 text-center">Always allow</span>
</div>
{#each group.tools as tool (tool.function.name)}
{@const toolName = tool.function.name}
{@const isEnabled = toolsStore.isToolEnabled(toolName)}
{@const permissionKey = toolsStore.getPermissionKey(toolName)}
{@const isAlwaysAllowed = permissionKey
? permissionsStore.hasTool(permissionKey)
: false}
{#each group.tools as entry (entry.key)}
{@const toolName = entry.definition.function.name}
{@const isEnabled = toolsStore.isToolEnabled(entry.key)}
{@const permissionKey = entry.key}
{@const isAlwaysAllowed = permissionsStore.hasTool(permissionKey)}
<div class="flex items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-muted/50">
<TruncatedText text={toolName} class="flex-1" showTooltip={true} />
@@ -76,7 +74,7 @@
<div class="flex w-16 shrink-0 justify-center">
<Checkbox
checked={isEnabled}
onCheckedChange={() => toolsStore.toggleTool(toolName)}
onCheckedChange={() => toolsStore.toggleTool(entry.key)}
class="h-4 w-4"
/>
</div>
@@ -86,9 +84,9 @@
checked={isAlwaysAllowed}
onCheckedChange={() => {
if (isAlwaysAllowed) {
permissionsStore.revokeTool(permissionKey!);
permissionsStore.revokeTool(permissionKey);
} else {
permissionsStore.allowTool(permissionKey!);
permissionsStore.allowTool(permissionKey);
}
}}
class="h-4 w-4"