<!-- GENERATED by scripts/build-llms.mjs from llms/agent-tools.md — do not edit this file. -->

# `lr-tool-select-dialog`

- **Import** `import '@aceshooting/lyra-ui/components/lr-tool-select-dialog.js';` (stable tag alias; registers the tag)
- **Class** `LyraToolSelectDialog`, also available unregistered from `@aceshooting/lyra-ui/components/agent-tools/tool-select-dialog/tool-select-dialog.class.js`
- **Family** `components/agent-tools/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 26 parts, 10 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-tool-select-dialog`

A category-grouped, filterable, searchable tool-enablement dialog for picking which agent tools are
available in a conversation. It keeps its own panel template rather than nesting `<lr-dialog>`, so
it has no dependency on the general-purpose dialog, while its modal behavior participates in the
shared overlay stack. First-party invention (no Web Awesome equivalent).

Programmatic `selectedToolIds` and `useDefaults` replacements synchronize the live checkbox and
switch state after user edits without emitting change events. Removing `search-placeholder` restores
localized copy, while an explicitly empty placeholder stays empty.

**Exported types:**

- `ToolSelectDialogTool { id: string; name: string; description?: string; category?: string; icon?:
string; disabled?: boolean; disabledReason?: string }` — one selectable agent tool. `category` groups
  the row into a heading; tools with no `category` (or an empty/whitespace-only one) fall into a
  trailing localized "Other" bucket. A caller category literally named `"Other"` remains its own
  ordinary, first-seen category and is not merged into or reordered with that uncategorized bucket.
  `icon` is a literal glyph (e.g. an emoji) rendered next to `name` — an opaque string, not a registry
  lookup, the same convention `<lr-tool-call-chip>`'s `icon` uses. `disabled` individually gates a
  tool regardless of `useDefaults`/`selectedToolIds` (e.g. a tool requiring admin approval);
  `description` and `disabledReason` are supporting descriptions associated with the checkbox
  through its stable `aria-describedby` owner; only `name` contributes to the checkbox's accessible
  name. `disabledReason` is ignored when `disabled` is falsy.
- `ToolSelectFilter = (tool: ToolSelectDialogTool, query: string) => boolean` — a predicate deciding
  whether `tool` matches an already-trimmed, already-lowercased `query`. Assign `filter` to replace the
  built-in case-insensitive name/description substring match entirely (mirrors `<lr-combobox>`'s
  `OptionFilter` convention).
- `ToolSelectionChangeDetail { selectedToolIds: string[]; useDefaults: boolean }` — the `lr-change` detail
  shape.
- `ToolSelectDialogCloseReason = 'escape' | 'backdrop' | 'api' | string` — the `lr-close` detail;
  `'escape'`/`'backdrop'` come from the dialog's own built-in dismiss triggers, any other string is
  whatever a caller passes to `close()` directly.

**Properties:**

- `open: boolean = false` (reflected) — set it directly or use the lifecycle methods below.
- `lightDismiss: boolean = false` (attribute `light-dismiss`) — opt in to backdrop-click
  dismissal; Escape remains available without it.
- `tools: ToolSelectDialogTool[] = []` (attribute: false) — the full set of tools a consumer offers,
  across all categories. `id` is the public identity: empty/blank ids are omitted and when provider
  data repeats one, the first occurrence wins consistently for grouping, filtering, counts,
  selection, and emitted ids.
- `selectedToolIds: string[] = []` (attribute: false) — the currently-enabled tool ids. Empty/blank ids
  are omitted and repeated ids are treated as one selection.
- `useDefaults: boolean = false` (attribute `use-defaults`, reflected) — whether the conversation is
  using the default tool set (`true`) or a custom selection (`false`).
- `label?: string` — the dialog's visible heading and accessible name. Omission uses localized
  `selectTools`; every supplied string, including `"Select tools"` and `""`, remains literal.
- `accessibleLabel: string | null = null` (attribute `aria-label`) — a host attribute names the
  host; the panel remains labelled by its visible heading instead of cloning that name. A direct
  property assignment made without the attribute can name the panel.
- `searchPlaceholder?: string` (attribute `search-placeholder`) — omission uses localized
  `searchToolsPlaceholder`; every supplied string, including `"Search tools…"` and `""`, remains
  literal as placeholder copy. Empty/whitespace-only copy leaves the field visually empty while
  its accessible name falls back to localized `searchToolsPlaceholder`.
- `filter: ToolSelectFilter | null = null` (attribute: false) — overrides the built-in
  case-insensitive name/description substring match.
- `autocomplete: string = ''`, `spellcheck: boolean = true`, `autocapitalize: string = ''`,
  `autoCorrect: string = ''` (`autocorrect`), `inputMode: string = ''` (`inputmode`), and
  `enterKeyHint: string = ''` (`enterkeyhint`) — forwarded to the search `<input>`.

**Methods:** `show(): void` opens the dialog; `hide(reason: ToolSelectDialogCloseReason = 'api'):
void` performs the reasoned API dismissal;
`close(reason: ToolSelectDialogCloseReason = 'api'): void` closes the dialog, emits `lr-close` with
`reason`, and returns focus to whatever had it before the dialog opened.

**Events:** `lr-change` (`detail: ToolSelectionChangeDetail` — the proposed enabled-tool selection and
`useDefaults` state) is cancelable and fires before either property changes. Calling
`preventDefault()` retains the current `selectedToolIds`/`useDefaults` values, and the built-in
checkbox or switch never flips at all — the proposal is raised from that control's own
`lr-checkbox-toggle-request`/`lr-switch-toggle-request`, before it writes its `checked` state, so a
refused change shows no flip-and-snap-back. A host can prevent a proposal while it validates or
persists it, then assign the desired detail values after that work succeeds. `lr-close`
(`detail: ToolSelectDialogCloseReason` — fired exactly once per dismissal, via Escape, a backdrop
click when `lightDismiss` is enabled, or a `close()` call), and no-detail `focus`/`blur` events
re-dispatched when the internal search input gains or loses focus.
Native `input`/`change` and prefixed `lr-input`/`lr-change` events from the built-in checkbox and
switch controls stop at the dialog boundary, as do their own
`lr-checkbox-toggle-request`/`lr-switch-toggle-request` proposals; listen for the single aggregate
`lr-change` proposal.

**Slots:** `footer` — optional action buttons (e.g. a "Done" button), rendered in a bottom row. Changes
already apply live via `lr-change`, so this slot is purely optional; only visually shown once it has
assigned elements.

**CSS parts:** `backdrop`, `panel`, `header`, `title`, `subtitle`, `search-row`, `search-input`,
`search-clear` (replaces the native search-cancel glyph the component resets; rendered only while
the field has text), `defaults-row`, `defaults-toggle`, `defaults-hint`, `body` (the
keyboard-focusable scroll region), `empty`, `category`, `category-heading`,
`category-count`, `category-list`, `tool-row`, `tool-checkbox`, `tool-name`, `tool-icon`,
`tool-description`, `tool-disabled-reason`, `limit`, `load-more`, `footer`

**Themeable custom properties:** `--lr-tool-select-dialog-overlay-color` (default
`var(--lr-color-overlay)` — the backdrop scrim color, the same shared token
`<lr-dialog>`/`<lr-tool-result-dialog>` read). `--lr-tool-select-dialog-search-min-height` (default
`auto`), `--lr-tool-select-dialog-search-font-size` (default `inherit`),
`--lr-tool-select-dialog-search-padding-inline` (default `var(--lr-space-m)`),
`--lr-tool-select-dialog-search-padding-block` (default `var(--lr-space-s)`) and
`--lr-tool-select-dialog-search-radius` (default `var(--lr-radius)`) size the search field; point
the height at a `--lr-form-control-height-*` tier to match it to a themed search field. The trailing
inline gutter is reserved for the overlaid `search-clear` button and is not a knob. The scrollable
tool list's mouse-hover preview on `[part="body"]` has its own four-longhand outline shape:
`--lr-tool-select-dialog-body-hover-outline-width` (default `var(--lr-border-width-thin)`),
`--lr-tool-select-dialog-body-hover-outline-style` (default `solid`),
`--lr-tool-select-dialog-body-hover-outline-color` (default `var(--lr-color-border)`, set to
`transparent` to opt out entirely), and `--lr-tool-select-dialog-body-hover-outline-offset`
(default `calc(-1 * var(--lr-border-width-thin))`, kept inward so the ring isn't clipped by the
body's own scrolling). Unset, all four resolve to the rule's previous literal paint. Plus shared
`--lr-space-*`, `--lr-color-surface/-border/-text/-text-quiet/-warning`, `--lr-radius`,
`--lr-shadow`, `--lr-focus-ring-width/-color/-offset`, `--lr-opacity-disabled`.

**Optional peer deps:** none — internally renders `<lr-checkbox>` and `<lr-switch>`, both bundled
dependencies of this package imported directly, not optional peers.

```html
<lr-tool-select-dialog label="Select tools">
  <button slot="footer" id="done-btn">Done</button>
</lr-tool-select-dialog>
<script type="module">
  const dialog = document.querySelector("lr-tool-select-dialog");
  dialog.tools = [
    { id: "search", name: "Web search", category: "Research" },
    { id: "python", name: "Python", category: "Code", description: "Run sandboxed Python" },
    { id: "admin", name: "Admin console", disabled: true, disabledReason: "Requires admin approval" },
  ];
  dialog.selectedToolIds = enabledToolIds;
  dialog.useDefaults = usingDefaults;
  dialog.open = true;
  dialog.addEventListener("lr-change", (e) =>
    updateTools(e.detail.selectedToolIds, e.detail.useDefaults)
  );
  dialog.addEventListener("lr-close", () => (dialog.open = false));
  dialog.querySelector("#done-btn").addEventListener("click", () => dialog.close("done"));
</script>
```

`useDefaults` is a single top-level switch: while `true`, every per-tool checkbox renders disabled
(still reflecting whatever `selected` holds — populate that with the actual default tool set whenever
`useDefaults` is true) alongside a hint explaining that turning the switch off is how to customize.
Turning it off is the only control that both flips `useDefaults` to `false` _and_ unlocks the per-tool
checkboxes for editing.

**Known gotchas:**

- No built-in footer/close button — dismissal happens via Escape, an opted-in (`light-dismiss`)
  backdrop click, or a consumer's own `footer`-slotted action calling `close()` directly.
- A row is effectively disabled whenever _either_ its own `tool.disabled` is true _or_ the top-level
  `useDefaults` switch is on — a tool without `disabled` set can still render as a locked checkbox while
  `useDefaults` is true.
- `disabledReason` text only renders when both `tool.disabled` and `tool.disabledReason` are set.
- Tool descriptions and disabled reasons are checkbox descriptions, not label content: the tool
  name remains the concise accessible name and the supporting text is linked through the checkbox's
  stable description bridge.
- Categories are grouped in first-seen order across `tools`; an empty/whitespace-only `category` folds
  into a trailing "Other" bucket that's always rendered last. A category left with zero matches after
  filtering is dropped entirely, not rendered as an empty heading. A caller-supplied category
  literally named `"Other"` is not the internal uncategorized sentinel: it stays in first-seen order
  and remains separate even when uncategorized tools are also present.
- Reconnecting the element while still `open` (e.g. a drag-and-drop reparent that keeps the same
  instance) resumes its shared overlay registration and re-acquires the scroll lock dropped in
  `disconnectedCallback`.
- The search input is the first focusable element in the panel and receives focus automatically on open.
- Matching rows mount in batches of 200. Selected matches reserve positions in the current batch,
  and a localized `[part="limit"]` notice plus `[part="load-more"]` button mounts the next 200;
  searching always considers the complete first-wins tool catalog.

---
