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

# `lr-tool-approval-dialog`

- **Import** `import '@aceshooting/lyra-ui/components/lr-tool-approval-dialog.js';` (stable tag alias; registers the tag)
- **Class** `LyraToolApprovalDialog`, also available unregistered from `@aceshooting/lyra-ui/components/agent-tools/tool-approval-dialog/tool-approval-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** 22 parts, 4 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-approval-dialog`

A human-in-the-loop gate: presents one proposed tool/function call (`toolName` + `args`) and blocks an
agent from executing it until a person explicitly approves or denies it, with an optional inline
"edit the arguments before approving" step. First-party invention (no Web Awesome equivalent). It
keeps its own panel template rather than nesting `<lr-dialog>`, so it has no dependency on the
general-purpose dialog component, while its modal behavior participates in the shared overlay stack.

Approve/Deny/Edit are built-in chrome, not a `footer` slot a consumer must assemble — there is exactly
one correct action set for "approve this call". The `footer` slot is offered only for _supplementary_
content alongside those buttons (e.g. a "remember this choice for this tool" checkbox); its content
renders at the start of the action row, before Deny/Edit/Approve.

**Exported types:**

- `ApprovalAction = 'approve' | 'deny'` — shared imperative vocabulary for an approval operation
  that is proposed or awaiting persistence
- `ApprovalDecision = 'approved' | 'denied'` — shared final-outcome vocabulary, deliberately
  separate from `ApprovalAction`
- `ToolApprovalDialogCloseReason = 'escape' | 'backdrop' | 'approve' | 'deny' | 'api' | string` — the
  `lr-close` detail; `'escape'`/`'approve'`/`'deny'` come from the dialog's built-in triggers,
  `'backdrop'` requires `lightDismiss`, and 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 and the built-in decision buttons remain available without it
- `accessibleLabel: string | null = null` (attribute `aria-label`) — a host attribute names the
  host; the panel remains labelled by its visible heading rather than cloning the same name. A
  direct property assignment made without the attribute can name the panel
- `proposalKey: string = ''` (attribute `proposal-key`) — immutable identity/generation for the
  open proposal. Change it whenever a source reuses the same visible tool name/arguments for a new
  proposal; draft, editing, validation-announcement, and pending-decision state reset immediately
- `toolName: string = ''` (attribute `tool-name`) — the proposed call's name, e.g. `web_search`;
  drives the heading and the dialog's accessible name
- `args: unknown = {}` (attribute: false) — the proposed call's arguments, rendered via
  `<lr-json-viewer>` read-only, or stringified into a `<textarea>` while editing
- `editable: boolean = true` (reflected) — whether an "Edit" affordance is offered at all (assign
  `false` via a PROPERTY binding, e.g. `.editable=${false}` — a `?editable=${false}`
  boolean-attribute binding cannot override a true default). When `false`, `args` is always shown
  read-only and can never be changed before approval.
- `spellcheck: boolean = false`, `autocapitalize: string = 'off'`,
  `autoCorrect: string = 'off'` (attribute `autocorrect`), `autocomplete: string = 'off'`,
  `wrap: 'hard'|'soft'|'off' = 'soft'`, `inputMode: string = ''` (attribute `inputmode`),
  and `enterKeyHint: string = ''` (attribute `enterkeyhint`) — forwarded to the raw-JSON
  `<textarea>` while editing; the defaults keep browser editing assistance from changing JSON text.
  `pending: 'approve' | 'deny' | null = null` (reflected) — which decision is awaiting host
  resolution while an `lr-approve`/`lr-deny` listener has called `preventDefault()` on the
  now-cancelable event; the pending button shows `loading`, the other is `disabled` (Approve is
  also still `disabled` while an in-progress edit is invalid JSON, independent of `pending`).
  Escape and an enabled backdrop dismissal are suppressed while `pending` is set. Finalize by calling
  `close('approve'|'deny')`, or clear `.pending` back to `null` to bounce back to the undecided
  state; `pending` also resets to `null` every time the dialog re-opens. If that same listener
  resolves the decision itself synchronously (calling `close('approve'|'deny')` or setting
  `.pending` directly before returning), that wins outright: the component's own built-in `pending`
  bookkeeping only applies when the listener left both `.pending` and `.open` untouched.

**Methods:** `show(): void` opens the dialog; `hide(reason: ToolApprovalDialogCloseReason = 'api'):
void` and `close(reason = 'api'): void` close through the same reasoned lifecycle, emit `lr-close`,
and return focus to whatever had it before opening; all are no-ops when already in the target state.

**Events:** `lr-approve` (`detail: { args: unknown }` — the current, already-parsed arguments: the
original `args` prop, or the user's edited-and-validated version if an edit was in progress.
Cancelable: a listener calling `preventDefault()` sets `pending` to `'approve'` instead of
closing; otherwise always followed by `lr-close` with reason `'approve'`), `lr-deny` (no detail —
`this.emit('lr-deny')` is called with no second argument, so per the DOM spec's `CustomEventInit`
default, `event.detail` is `null`, not `undefined`. Cancelable, same `pending` mechanism, setting
`pending` to `'deny'`; otherwise always followed by `lr-close` with reason `'deny'`), `lr-close`
(`detail: ToolApprovalDialogCloseReason` — fired exactly once per dismissal, via Escape, an opted-in
backdrop click, the Approve/Deny buttons, or a `close()` call), and no-detail `focus`/`blur` events
re-dispatched when the raw-JSON editor gains or loses focus.

`waitUntil()` is `<lr-confirm-bar>`-only and this dialog does not carry it. The two components share
the `lr-approve`/`lr-deny` event *names*, so the generated `HTMLElementEventMap['lr-approve']` is the
union of both details and only the confirm bar's arm has the field: a listener bound to the shared
name (`document.addEventListener('lr-approve', ...)`) must narrow on `event.target` before reaching
for it, while one bound through `LyraConfirmBarEventMap`/`LyraToolApprovalDialogEventMap` already
sees the right detail. Hold a decision open here with `preventDefault()` + `pending`, then finalize
with `close('approve'|'deny')` or bounce back by clearing `.pending`.

**Slots:** `footer` — optional supplementary content (e.g. a "remember this choice" checkbox),
rendered before the built-in Deny/Edit/Approve buttons.

**CSS parts:** `backdrop`, `panel`, `header`, `tool-name`, `body`, `args-view`, `args-editor`, `error`,
`footer`, `deny-button`, `edit-button`, `approve-button`,
`deny-button-base`, `deny-button-label`, `deny-button-start`, `deny-button-end`,
`deny-button-spinner`, `approve-button-base`, `approve-button-label`, `approve-button-start`,
`approve-button-end`, `approve-button-spinner` (`deny-button`/`approve-button` are each an
`<lr-button>` host; these five per-button parts are re-exported from its own `lr-button` parts via
`exportparts`. Each `*-button-base` route accepts the button's same-node `base` and `button`
wrapper aliases, so either name survives the nested shadow boundary; `edit-button` stays a plain
`<button>`, unaffected by this).

**Themeable custom properties:** `--lr-tool-approval-dialog-overlay-color` (default
`var(--lr-color-overlay)` — the backdrop scrim color, the same shared token `<lr-dialog>` and
`<lr-tool-select-dialog>` read), `--lr-tool-approval-dialog-mono-font` (default
`var(--lr-font-mono)`, the library's shared monospace stack — used by both `tool-name` and the
raw-JSON editor), `--lr-tool-approval-dialog-invalid-border-color` (default
`var(--lr-color-danger)` — border color of the invalid raw-JSON editor, independently retunable
without changing error text or other danger-coloured surfaces), and
`--lr-tool-approval-dialog-hover-border-color` (default `var(--lr-color-brand)` — border color of
the raw-JSON args editor on hover, giving mouse users the same "this is editable" affordance every
other text-entry surface in the library provides), plus shared tokens
`--lr-space-xs/-s/-m/-l`, `--lr-color-surface`, `--lr-color-border`, `--lr-radius`,
`--lr-shadow`, `--lr-color-brand`, `--lr-color-on-brand`, `--lr-color-danger`,
`--lr-color-text`, `--lr-focus-ring-width/-color/-offset`, `--lr-opacity-disabled`.

**Optional peer deps:** none — internally renders `<lr-json-viewer>`, a bundled dependency of this
package, not an optional peer.

```html
<lr-tool-approval-dialog tool-name="send_email"></lr-tool-approval-dialog>
<script type="module">
  const dialog = document.querySelector("lr-tool-approval-dialog");
  dialog.args = { to: "ops@example.com", subject: "Deploy finished" };
  dialog.addEventListener("lr-approve", (e) => runTool(e.detail.args));
  dialog.addEventListener("lr-deny", () => console.log("denied"));
  dialog.addEventListener("lr-close", (e) => console.log("closed:", e.detail));
  dialog.open = true;
</script>
```

While `editable`, an Edit button swaps the read-only `<lr-json-viewer>` for a plain `<textarea>`
pre-filled with `JSON.stringify(args, null, 2)`. Every keystroke re-validates with `JSON.parse` — the
Approve button is `disabled` for as long as the current textarea content fails to parse, so a
malformed edit can never be silently approved as either the broken text or a stale copy of the
original args. The same button relabels to "Cancel" while editing; clicking it discards the draft
entirely and returns to the read-only view of the _original_ `args` — there is no separate "save"
step independent of Approve itself. Both `editing` and any in-progress draft reset back to the
read-only view whenever the dialog opens or `proposalKey`/`toolName`/`args` identifies a replacement
proposal, so a reused instance never leaks one proposal's half-finished edit into the next.

The raw-JSON editor deliberately fixes native `resize` to `vertical`, so a user can make a long
draft taller without changing the dialog's constrained inline size. This focused approval flow has
no generic resize property or auto-grow mode; compose a dedicated editor around the approval UI
when either behavior is required.

The visible JSON error remains ordinary descriptive text. A transition from a valid draft into
invalid JSON is additionally appended once to the shared assertive light-DOM announcement sink;
further invalid keystrokes do not repeat it. An invalid draft already present at initial mount or
reconnect establishes a silent baseline rather than replaying stale context.

Initial focus deliberately does _not_ land on Approve: approving a tool call is a consequential,
potentially irreversible action, so a user who opens the dialog and reflexively presses Enter/Space
before reading anything should deny, not approve. Deny gets the initial focus instead — the same
"focus the safe action" convention a native destructive-confirmation dialog typically follows for its
own Cancel button. Tab/Shift+Tab are bounded to the panel's own focusable content, and
`<lr-json-viewer>`'s internal controls plus slotted custom-element controls are found through the
shared composed-tree focus traversal used by the other modal families.

**Known gotchas:**

- `editable` defaults to `true` and reflects — see the property note above about overriding it with a
  property binding, not a boolean-attribute binding.
- `lr-deny` has no detail payload: its `event.detail` is `null`, not `undefined`.
- a consumer flipping `editable` off while an edit is already in progress automatically exits edit mode
  and discards the draft, so an unreachable "Cancel" affordance is never left stranded on screen.
- 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 ref-counted scroll lock
  dropped in `disconnectedCallback` — `willUpdate()` alone wouldn't otherwise notice, since
  disconnect/reconnect fire back-to-back with no update in between.
- the Approve button's native `disabled` attribute (while the draft is invalid JSON) automatically
  excludes it from the shared Tab trap, whose focusable-set computation skips disabled controls.
- the raw-JSON `args-editor` textarea defaults to `spellcheck="false"`, `autocapitalize="off"`,
  `autocorrect="off"`, and `autocomplete="off"` because its content is JSON, never prose. These
  native editing-assistance values remain configurable through the corresponding properties for
  integrations that intentionally need different browser behavior.
- `deny-button`/`approve-button` are `<lr-button>` hosts (`variant="neutral"`/`"brand"` — the dialog
  itself has no `variant` property, unlike its in-flow sibling `<lr-confirm-bar>`, so the Approve
  button is always `brand` here) — `--lr-button-*` theming reaches them directly. A consumer
  previously styling `::part(deny-button)`/`::part(approve-button)` for
  padding/border/font/`:hover`/`:focus-visible` must move that CSS onto the re-exported
  `deny-button-base`/`approve-button-base` sub-parts instead. `edit-button` is unaffected and stays
  a raw `<button>`.
- Backdrop clicks leave the dialog open by default; add `light-dismiss` to opt in, matching
  `<lr-dialog>`, `<lr-drawer>`, `<lr-lightbox>`, and the sibling tool dialogs.
- An `lr-approve`/`lr-deny` listener can call `preventDefault()` to keep the decision open while
  its own async work is in flight — see `pending` above. While `pending` is set, Escape and an
  enabled backdrop dismissal are suppressed, so a consumer that never resolves the pending decision leaves the
  dialog open until it clears `.pending` or calls `close()` directly itself.

---
