Files
open-webui/src/lib/components/workspace/Models/BuiltinTools.svelte
T
Classic298 2725ae6d6c fix: expose Checkbox as a checkbox with a name and state (WCAG 4.1.2) (#27494)
On latest `dev`, `common/Checkbox.svelte` renders a `<button type="button">` containing only `aria-hidden="true"` SVGs. It has no `role`, no `aria-checked` and no accessible name, and the component has no `$$restProps` spread, so a caller cannot supply a name either.

Assistive technology announces every one of these as an unnamed "button". A screen reader user cannot tell that the control is a checkbox, cannot tell whether it is on or off, and cannot tell what it toggles. The visible label is always an unassociated sibling element, for example `Capabilities.svelte` puts it in a preceding `<div>` with no `id`, and `Groups/Users.svelte` puts it in a different table cell from the checkbox.

Breaks WCAG 4.1.2 Name, Role, Value (Level A) on all three counts at once.

Fix: expose `role="checkbox"` and `aria-checked` on the control, add an `ariaLabel` prop, and pass the label text that is already in scope at each call site. `aria-checked` mirrors the component's existing icon logic exactly, so the indeterminate dash reports `mixed` rather than `false`. The `ariaLabel={ariaLabel || undefined}` shape matches the sibling `common/Switch.svelte`. Every label expression is the same one that renders the visible text next to the checkbox, so the accessible name always matches what is on screen.

Three call sites are deliberately left out of this PR, because they nest `Checkbox` inside another `<button>`, which is invalid HTML and independently broken:

- `workspace/Knowledge/KnowledgeBase.svelte` — the Checkbox's `on:change` sets `includeContent = true` and then the same click bubbles to the outer button, which flips it back with `includeContent = !includeContent`. Clicking the checkbox square is a no-op today, only the text label works. Giving it a confident name would advertise a control that does nothing.
- `workspace/common/MemberSelector.svelte` (two instances) — the inner Checkbox has no `on:change` at all and only works because its click bubbles to the row button. Naming it would create two focusable controls per row with the same name.

Both need the nesting resolved first, so that the row button carries the checkbox semantics. That is a behavioural fix and belongs in its own PR.

Severity: Serious. Affects model capabilities, default features, builtin tools, tool/filter/skill/action selectors and group membership.

### Contributor License Agreement

<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.

Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->

- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.

> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
2026-07-26 17:37:06 -04:00

99 lines
2.9 KiB
Svelte

<script lang="ts">
import { getContext } from 'svelte';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { marked } from 'marked';
const i18n = getContext('i18n');
const toolLabels = {
time: {
label: $i18n.t('Time & Calculation'),
description: $i18n.t('Get current time and perform date/time calculations')
},
memory: {
label: $i18n.t('Memory'),
description: $i18n.t('Search and manage user memories')
},
chats: {
label: $i18n.t('Chat History'),
description: $i18n.t('Search and view user chat history')
},
notes: {
label: $i18n.t('Notes'),
description: $i18n.t('Search, view, and manage user notes')
},
knowledge: {
label: $i18n.t('Knowledge Base'),
description: $i18n.t('Browse and query knowledge bases')
},
channels: {
label: $i18n.t('Channels'),
description: $i18n.t('Search channels and channel messages')
},
notifications: {
label: $i18n.t('Notifications'),
description: $i18n.t('Send notifications to configured webhook targets')
},
web_search: {
label: $i18n.t('Web Search'),
description: $i18n.t('Search the web and fetch URLs')
},
image_generation: {
label: $i18n.t('Image Generation'),
description: $i18n.t('Generate and edit images')
},
code_interpreter: {
label: $i18n.t('Code Interpreter'),
description: $i18n.t('Execute code')
},
tasks: {
label: $i18n.t('Task Management'),
description: $i18n.t('Break down complex requests into trackable steps')
},
automations: {
label: $i18n.t('Automations'),
description: $i18n.t('Create and manage scheduled automations')
},
calendar: {
label: $i18n.t('Calendar'),
description: $i18n.t('List calendars, search, create, update, and delete calendar events')
},
subagents: {
label: $i18n.t('Sub-agents'),
description: $i18n.t('Delegate focused work to parallel sub-agents')
}
};
const allTools = Object.keys(toolLabels) as Array<keyof typeof toolLabels>;
export let builtinTools: Record<string, boolean> = {};
</script>
<div>
<div class="mb-1.5 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Builtin Tools')}</div>
<div class="grid grid-cols-1 gap-x-5 gap-y-1 sm:grid-cols-2 lg:grid-cols-3">
{#each allTools as tool}
<div class="flex min-h-6 items-center justify-between gap-2.5">
<div class="min-w-0 text-xs text-gray-600 dark:text-gray-400">
<Tooltip content={marked.parse(toolLabels[tool].description)}>
<span class="truncate">{$i18n.t(toolLabels[tool].label)}</span>
</Tooltip>
</div>
<Checkbox
ariaLabel={$i18n.t(toolLabels[tool].label)}
state={builtinTools[tool] !== false ? 'checked' : 'unchecked'}
on:change={(e) => {
if (e.detail === 'checked') {
delete builtinTools[tool];
} else {
builtinTools[tool] = false;
}
builtinTools = builtinTools;
}}
/>
</div>
{/each}
</div>
</div>