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

# `lr-tool-result-view`

- **Import** `import '@aceshooting/lyra-ui/components/lr-tool-result-view.js';` (stable tag alias; registers the tag)
- **Class** `LyraToolResultView`, also available unregistered from `@aceshooting/lyra-ui/components/agent-tools/tool-result-view/tool-result-view.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** 3 parts, 1 custom property — 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-result-view`

Renders a tool call's result via whichever custom renderer a host app has registered for it,
falling back to `<lr-json-viewer>` whenever no renderer matches, a candidate renderer's
`matches()` predicate throws during dispatch, a renderer's optional `load()` rejects, or its
`render()` throws. First-party invention (no Web Awesome equivalent). This component
owns none of the actual visual weight of a populated tool result — that's entirely whatever the
registered renderer returns; `<lr-tool-result-view>` is just the dispatch + fallback + loading-state
shell around it.

**Properties:**

- `registry?: ToolRendererRegistry` (property only, no attribute) — a custom
  `ReadonlyMap<string, ToolRendererDefinition>` to dispatch against instead of the module-level
  default registry (see `registry.ts` below). Assignment synchronously copies at most 10,000
  entries behind a frozen readonly facade. Later mutation of the source map is not observed;
  create and reassign a new map to update dispatch. Definition records are cloned and frozen while
  callback identities are retained; lazy-load caching remains stable per assigned snapshot.
- `toolName: string = ''` (attribute `tool-name`) — the tool's name; the primary dispatch key
- `result: unknown` (property only, no attribute) — the tool call's result payload, handed to the
  matched renderer's `render()` (and to `matches()` for shape-based dispatch, and to the
  `<lr-json-viewer>` fallback)
- `args: unknown` (property only, no attribute) — the tool call's original arguments, if available,
  handed to the matched renderer's `render()` alongside `result`
- `fallback: ToolResultFallback = 'json'` (reflected), where exported `ToolResultFallback =
'json' | 'text'` — fallback-kind selector. `"json"` (the default) is
  an unconditional `<lr-json-viewer>`. `"text"` renders a _string_ `result` as preformatted text
  instead — falling back to the `"json"` behavior when `result` isn't a string, so setting
  `fallback="text"` defensively against an unpredictable result shape never renders broken output.
  Foreign runtime values normalize to the reflected `"json"` default.
- `copyable: boolean = false` (reflected) — shows a copy-to-clipboard affordance alongside the
  fallback view, for either `fallback` kind: forwarded to `<lr-json-viewer>`'s own `copyable` for
  `"json"`, or a `<lr-copy-button>` rendered next to the text for `"text"`.
- `status: 'pending'|'running'|'success'|'error'|'denied' = 'success'` (reflected) — the outcome of
  the currently-rendered result, as reported by the matched renderer's own `context.reportStatus()`
  (see below). Reset to `'success'` immediately before every `render()` call, so a renderer that
  never calls `reportStatus` — including every pre-existing 2-arg renderer written before this
  property existed — leaves it at that default, and a later renderer that stays quiet doesn't
  inherit a stale outcome left behind by a previous one. Same status vocabulary as
  `<lr-tool-result-dialog>`/`<lr-tool-call-chip>`.

**Events:** `lr-render-error` (`detail: { toolName: string; error: unknown }`) — fired immediately
before falling back to `<lr-json-viewer>`, whether because no renderer matched, a candidate
renderer's `matches()` predicate threw during dispatch, a renderer's `load()` rejected, or its
`render()` threw.

**Slots:** none.

**CSS parts:** `base` — the root wrapper around the resolved renderer's output (or the loading/
fallback view); it keeps `aria-busy="true"` while a lazy renderer is loading and explicitly returns
to `aria-busy="false"` afterward. `fallback-text` — the `<pre>` element for the `fallback="text"` kind's preformatted
result text (only present in that mode). `fallback-copy` — the `<lr-copy-button>` shown when
`copyable` is set alongside the `fallback="text"` kind (only present when both are set).

**Themeable custom properties:** `--lr-tool-result-view-font` (default `var(--lr-font-mono)`, the
library's shared monospace stack, so a `--lr-theme-font-family-mono` override reaches it) — only used by the
`fallback="text"` kind's `[part='fallback-text']`. Otherwise none — the component's own styling is
deliberately minimal; all visible styling comes from whatever renderer/`<lr-skeleton>`/
`<lr-json-viewer>`/`<lr-copy-button>` child is currently mounted.

**Optional peer deps:** none required by the component itself — individual registered renderers may
of course pull in whatever they need (a charting library, a markdown renderer), which is exactly what
the lazy `load()` path in the registry exists for.

```html
<lr-tool-result-view tool-name="get_weather"></lr-tool-result-view>
<script type="module">
  const view = document.querySelector("lr-tool-result-view");
  view.result = { tempC: 21, condition: "cloudy" };
  view.args = { city: "Brussels" };
  view.addEventListener("lr-render-error", (e) => console.warn("renderer failed", e.detail));
</script>
```

### `registerToolRenderer()` and the tool-renderer registry (`registry.ts`)

A type-keyed dispatch registry — a tiny plugin system so a host app can teach
`<lr-tool-result-view>` how to draw the result of e.g. a `get_weather` or `run_query` tool call
without this library knowing anything about either. Every registered instance dispatches against
this same module-level registry unless a given `<lr-tool-result-view>`'s `registry` property is
set to a different readonly map snapshot.

**`ToolRendererDefinition`** — an exclusive
`DirectToolRendererDefinition | LazyToolRendererDefinition` union. Runtime registration, custom
registry lookup, and loaded-module boundaries validate the same shape, so plain JavaScript cannot
silently register `{}`, combine `render` with `load`, or cache an invalid loaded definition:

- direct: `render: (result: unknown, args: unknown, context?: ToolRenderContext) => unknown` and
  `load?: never` — renders the
  result (and the args that produced it) as UI. Typed as `unknown` rather than Lit's
  `TemplateResult` so any lit-html-renderable value works (a plain string, a DOM node, an array of
  templates) — consumers already own their own Lit import and don't need this module to add one.
  The 3rd `context` argument is additive: it's the _last_ positional parameter, so a pre-existing
  2-arg `render(result, args)` function stays assignable to this type unchanged — JS/TS function
  assignability allows an implementation with fewer parameters than its declared type. Direct
  callers may omit `context`; component invocations always provide it. Use
  `context?.reportStatus(status)` (see `ToolRenderContext` below) to signal a non-throwing outcome
  — e.g. an application-level failure the renderer still drew real UI for — instead of throwing,
  which discards that UI for the `<lr-json-viewer>` fallback instead
- either branch may include `matches?: (payload: unknown) => boolean` — facade/shape-based dispatch predicate, consulted only
  when no exact `toolName` key matches (see dispatch order below); only ever consulted _before_
  `load` resolves when supplied inline at registration time — a definition that needs shape-based
  dispatch and also wants to lazy-load its `render` should register a lightweight synchronous
  `matches` up front alongside `load`
- lazy: `load: () => Promise<DirectToolRendererDefinition | { default:
DirectToolRendererDefinition }>` and `render?: never` — lazy loader
  for a code-split renderer, so a host app can defer the cost of a rarely-used or heavy renderer
  (e.g. one pulling in a charting library) instead of paying for it on every page that merely
  registers it. Resolves to either a definition directly, or a `{ default }`-shaped module namespace
  object, so `load: () => import('./my-renderer.js')` works unmodified when that module's default
  export is itself a `ToolRendererDefinition`

**`ToolRenderContext`** — the shape of `render()`'s 3rd argument:

- `reportStatus: (status: ToolResultStatus) => void` — reports this render's outcome without
  throwing. `ToolResultStatus` is `'pending' | 'running' | 'success' | 'error' | 'denied'`, the same
  union `<lr-tool-result-dialog>`/`<lr-tool-call-chip>` use, re-exported from this module. Calling
  it is entirely optional: a renderer that never calls it leaves `<lr-tool-result-view>`'s `status`
  property at its default, `'success'`. This threads through the lazy `load()` path exactly the
  same way — a `render()` resolved via `load()` receives the same 3rd `context` argument as one
  registered directly.

```ts
registerToolRenderer("run_query", {
  render: (result, _args, context) => {
    if ((result as { rows?: unknown[] })?.rows === undefined) {
      context?.reportStatus("error");
      return html`<p class="query-error">The query returned no result set.</p>`;
    }
    return html`<query-result-table
      .rows=${(result as { rows: unknown[] }).rows}
    ></query-result-table>`;
  },
});
```

**Exports:**

- `registerToolRenderer(name: string, def: ToolRendererDefinition): void` — registers (or
  overwrites) the renderer for `name` in the module-level default registry
- `getDefaultToolRendererRegistry(): ToolRendererRegistry` — returns the default `Map` that
  `registerToolRenderer()` writes to and every `<lr-tool-result-view>` reads from unless its own
  `registry` prop is set
- `findToolRenderer(toolName: string, payload: unknown, registry?: ToolRendererRegistry):
ToolRendererDefinition | undefined` — the dispatch function `<lr-tool-result-view>` calls
  internally on every resolve; exposed for direct use too
- `loadToolRenderer(def: ToolRendererDefinition): Promise<DirectToolRendererDefinition>` — resolves `def`
  to a definition guaranteed to carry a real `render`, awaiting/unwrapping `def.load()` when present
  (or returning `def` unchanged otherwise)
- `clearToolRenderers(): void` — clears the default registry and its `load()` cache

**Dispatch order** (`findToolRenderer`), exactly as `<lr-tool-result-view>`'s own `resolve()` uses
it:

1. An exact `toolName` key match in the registry.
2. Failing that, the first entry — in registration order, since a `Map` already iterates that way —
   whose `matches(payload)` returns `true`. Useful when several tool names share one result shape
   (e.g. every `*_search` tool returning `{ results: [...] }`) or when the caller doesn't reliably
   know the tool name at all.
3. `undefined` if neither matches — `<lr-tool-result-view>` falls back to `<lr-json-viewer>` and
   fires `lr-render-error`.

Once a definition is found, if it carries `load`, `<lr-tool-result-view>` shows a
decorative `<lr-skeleton shape="rect" height="4rem">` while `loadToolRenderer()` resolves it.
The nested skeleton has announcements disabled; the stable `base` busy state and an ordinary,
visually hidden localized Loading label expose the in-progress state without creating a shadow-root
live region. The resolved
`load()` promise is cached keyed by _definition object identity_ (a `WeakMap`, not by tool-name
string) — two different registries that happen to reuse the same tool-name string get independently
cached loads, and any given lazy definition's `load()` runs at most once no matter how many times
it's dispatched to, across every `<lr-tool-result-view>` instance that resolves to it. A **rejected**
`load()` is _not_ cached — nor is a load that resolves to an invalid/another-lazy definition. The
definition stays registered, so a later resolution attempt (e.g. after a transient network failure)
gets a fresh `load()` call rather than being stuck replaying one failed promise forever.

```ts
import { registerToolRenderer } from "@aceshooting/lyra-ui/components/agent-tools/tool-result-view/registry.js";

registerToolRenderer("get_weather", {
  render: (result, args) =>
    html`<weather-card .data=${result} .city=${args?.city}></weather-card>`,
});

// Lazily loaded, shape-based fallback for every *_search tool:
registerToolRenderer("web_search", {
  matches: (payload) =>
    typeof payload === "object" && payload !== null && "results" in payload,
  load: () => import("./search-result-renderer.js"), // default export is a ToolRendererDefinition
});
```

**Known gotchas:**

- `<lr-tool-result-view>` re-resolves (re-runs the full dispatch → load → render pipeline)
  whenever `toolName`, `result`, `args`, or `registry` changes, or on first update — a stale
  in-flight `load()` superseded by a newer change is detected via an internal generation counter and
  its result is discarded rather than clobbering a more recent render
- registering under the same `name` twice silently overwrites the earlier definition — there is no
  warning or error
- `matches` is a linear scan over every registered definition's `matches` in registration order; it
  only runs when the exact-name lookup misses, so tool names with a direct registration never pay
  that scan cost
- `status` is reset to `'success'` immediately before every `render()` call, not merely at
  construction — a renderer that reported `'error'` on one result does not leave that status
  behind once dispatch moves on to a different (quiet) renderer; a `reportStatus()` call that
  arrives asynchronously after a _newer_ resolve has already started (a stale promise the previous
  render kicked off) is detected via the same generation counter as the `load()` staleness guard
  and discarded rather than clobbering the newer status
- **don't type a custom renderer against a hand-rolled, over-generic function signature** (e.g.
  `render: (...args: any[]) => unknown`, or a locally-declared narrower alias then cast to
  `ToolRendererDefinition`) — write the registration as a plain object literal (as in every example
  above) or annotate it as `ToolRendererDefinition` directly, so TypeScript checks the actual
  current `render`/`matches`/`load` shape, including the `context: ToolRenderContext` 3rd
  parameter and the exact `ToolResultStatus` string union `reportStatus` accepts. A loosened/`any`
  signature type-checks either way but silently gives up the compiler's ability to catch a typo'd
  status string or a dropped `context` parameter
- `fallback` implements exactly two kinds, `"json"` and `"text"`; any _other_ runtime value
  normalizes to reflected `"json"`, while `"text"` with a non-string result uses the JSON view.
  Only `"text"` renders
  `[part="fallback-text"]`/`[part="fallback-copy"]`

---
