@threadplane/chat 0.0.52 → 0.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -173,6 +173,17 @@ Agents can emit surface specs via `buildA2uiActionMessage(...)`. Actions from ca
173
173
 
174
174
  The built-in catalog ships via `a2uiBasicCatalog`. Compose a custom catalog with `withViews()` and pass it to the surface.
175
175
 
176
+ **Icons.** The catalog `Icon` component renders [Material Symbols](https://fonts.google.com/icons) by name (the A2UI canonical icon set — e.g. `check`, `trending_up`, `star`). For glyphs to render, include the Material Symbols Outlined stylesheet in your app's `<head>` (the library does not inject any web font):
177
+
178
+ ```html
179
+ <link
180
+ rel="stylesheet"
181
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
182
+ />
183
+ ```
184
+
185
+ Without the font, the icon name falls back to plain text. Icons inherit `currentColor` and size via the `size` prop.
186
+
176
187
  ### Streaming markdown
177
188
 
178
189
  `<chat-streaming-md>` renders markdown token-by-token as the agent streams. The `cacheplaneMarkdownViews` registry maps each CommonMark node type to an Angular component.
@@ -196,6 +207,17 @@ Per-instance, bind the registry on `<chat-streaming-md [viewRegistry]="…" />`
196
207
 
197
208
  The `renderMarkdown(md, options?)` function produces a parse tree for use outside streaming contexts.
198
209
 
210
+ #### Math (KaTeX)
211
+
212
+ LaTeX math — inline `$…$` / `\(…\)` and display `$$…$$` / `\[…\]` — renders via [KaTeX](https://katex.org), an **optional** peer dependency loaded lazily only when a message actually contains math (so non-math chats carry zero extra bundle weight). To enable styled math, install `katex` and import its stylesheet once in your app:
213
+
214
+ ```typescript
215
+ // e.g. in your global styles or app bootstrap
216
+ import 'katex/dist/katex.min.css';
217
+ ```
218
+
219
+ Without `katex` installed, or without the stylesheet, math degrades gracefully — the raw `$…$` source is shown rather than breaking. Currency like `$5` is not treated as math.
220
+
199
221
  ### Theming
200
222
 
201
223
  `<a2ui-surface>` declares ~50 `--a2ui-*` CSS custom properties at `:host` with dark-theme defaults covering color, spacing, typography, shape radius, focus ring, motion, and elevation. Catalog components consume them via `var(--a2ui-*)`.
@@ -4,6 +4,7 @@ import { NgTemplateOutlet, NgComponentOutlet, KeyValuePipe } from '@angular/comm
4
4
  import { createPartialMarkdownParser, materialize } from '@cacheplane/partial-markdown';
5
5
  import { views, RenderSpecComponent, toRenderRegistry, signalStateStore, withViews, RenderElementComponent, injectRenderHost } from '@threadplane/render';
6
6
  export { toRenderRegistry, views, withViews, withoutViews } from '@threadplane/render';
7
+ import { DomSanitizer } from '@angular/platform-browser';
7
8
  import { runLicenseCheck, inferNoncommercial, LICENSE_PUBLIC_KEY } from '@threadplane/licensing';
8
9
  import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
10
  import { Router, NavigationEnd } from '@angular/router';
@@ -1794,6 +1795,11 @@ const CHAT_MARKDOWN_STYLES = `
1794
1795
  }
1795
1796
  chat-streaming-md .chat-md-image__icon { font-size: 1em; line-height: 1; }
1796
1797
  chat-streaming-md .chat-md-image__alt { font-style: italic; }
1798
+ /* Math (KaTeX). Display math is a centered block that scrolls horizontally
1799
+ on overflow; the raw fallback (KaTeX missing or invalid LaTeX) reads as
1800
+ monospace so the source delimiters look intentional. */
1801
+ chat-streaming-md .chat-md-math--display { display: block; margin: 0.5em 0; overflow-x: auto; }
1802
+ chat-streaming-md .chat-md-math--raw { font-family: var(--ngaf-chat-font-mono, ui-monospace, monospace); }
1797
1803
  `;
1798
1804
 
1799
1805
  // libs/chat/src/lib/markdown/markdown-view-registry.ts
@@ -2143,6 +2149,125 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
2143
2149
  }]
2144
2150
  }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2145
2151
 
2152
+ // libs/chat/src/lib/markdown/katex-loader.ts
2153
+ // SPDX-License-Identifier: MIT
2154
+ /**
2155
+ * Lazy KaTeX integration for the markdown math view. Mirrors the lazy `marked`
2156
+ * loader in ../streaming/markdown-render.ts: `katex` is an optional peer
2157
+ * dependency, dynamically imported on module load, so chats that never contain
2158
+ * math pay zero base-bundle cost. `renderMath` returns the KaTeX HTML string,
2159
+ * or null when KaTeX is unavailable or the LaTeX is invalid — the view then
2160
+ * renders the raw `$…$` source instead.
2161
+ *
2162
+ * CSS: the KaTeX stylesheet (and its woff2 fonts) is the CONSUMER's
2163
+ * responsibility — this lib injects nothing at runtime (no third-party CDN
2164
+ * fetch). Apps import `katex/dist/katex.min.css` themselves; math still renders
2165
+ * without it, just unstyled.
2166
+ */
2167
+ let katexRender = null;
2168
+ /**
2169
+ * Flips to true once KaTeX has loaded. The math view reads it as a signal
2170
+ * dependency so any math that fell back to raw source while KaTeX was still
2171
+ * loading re-renders once it becomes available.
2172
+ */
2173
+ const katexReady = signal(false, ...(ngDevMode ? [{ debugName: "katexReady" }] : []));
2174
+ /** Resolves once the KaTeX import has settled (success or failure). Tests await this. */
2175
+ const katexLoaded = import('katex')
2176
+ .then((m) => {
2177
+ const katex = (m.default ?? m);
2178
+ katexRender = (latex, displayMode) =>
2179
+ // throwOnError:true so invalid LaTeX throws → we catch → raw-source
2180
+ // fallback, instead of KaTeX emitting its own red error markup.
2181
+ katex.renderToString(latex, { displayMode, throwOnError: true });
2182
+ katexReady.set(true);
2183
+ })
2184
+ .catch(() => {
2185
+ katexRender = null;
2186
+ });
2187
+ /**
2188
+ * Render LaTeX to a KaTeX HTML string, or null if KaTeX is unavailable or the
2189
+ * input throws (invalid LaTeX) — the caller then renders the raw `$…$` source.
2190
+ */
2191
+ function renderMath(latex, displayMode) {
2192
+ if (!katexRender)
2193
+ return null;
2194
+ try {
2195
+ return katexRender(latex, displayMode);
2196
+ }
2197
+ catch {
2198
+ return null;
2199
+ }
2200
+ }
2201
+
2202
+ // libs/chat/src/lib/markdown/views/markdown-math.component.ts
2203
+ // SPDX-License-Identifier: MIT
2204
+ /** Opener/closer source text per delimiter, used for the raw fallback. */
2205
+ const DELIMITERS = {
2206
+ $: ['$', '$'],
2207
+ '$$': ['$$', '$$'],
2208
+ '\\(\\)': ['\\(', '\\)'],
2209
+ '\\[\\]': ['\\[', '\\]'],
2210
+ };
2211
+ /**
2212
+ * Renders a `math-inline` / `math-display` markdown node as KaTeX. KaTeX is
2213
+ * lazy-loaded (see katex-loader); until it resolves, or if the LaTeX is
2214
+ * invalid, the raw `$…$` source is shown — never blank, never a crash.
2215
+ */
2216
+ class MarkdownMathComponent {
2217
+ node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
2218
+ sanitizer = inject(DomSanitizer);
2219
+ display = computed(() => this.node().type === 'math-display', ...(ngDevMode ? [{ debugName: "display" }] : []));
2220
+ raw = computed(() => {
2221
+ const n = this.node();
2222
+ const [open, close] = DELIMITERS[n.delimiter];
2223
+ return `${open}${n.text}${close}`;
2224
+ }, ...(ngDevMode ? [{ debugName: "raw" }] : []));
2225
+ html = computed(() => {
2226
+ katexReady(); // re-render once KaTeX finishes loading
2227
+ const n = this.node();
2228
+ const out = renderMath(n.text, n.type === 'math-display');
2229
+ if (out == null)
2230
+ return null;
2231
+ // Trust KaTeX output directly: with KaTeX's default `trust:false` it emits
2232
+ // only safe presentational markup (no scripts/event handlers/links), and
2233
+ // Angular's HTML sanitizer would strip the inline styles KaTeX layout
2234
+ // depends on.
2235
+ return this.sanitizer.bypassSecurityTrustHtml(out);
2236
+ }, ...(ngDevMode ? [{ debugName: "html" }] : []));
2237
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownMathComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2238
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: MarkdownMathComponent, isStandalone: true, selector: "chat-md-math", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
2239
+ @if (html(); as h) {
2240
+ <span
2241
+ class="chat-md-math"
2242
+ [class.chat-md-math--display]="display()"
2243
+ [innerHTML]="h"
2244
+ ></span>
2245
+ } @else {
2246
+ <span class="chat-md-math chat-md-math--raw">{{ raw() }}</span>
2247
+ }
2248
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2249
+ }
2250
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownMathComponent, decorators: [{
2251
+ type: Component,
2252
+ args: [{
2253
+ selector: 'chat-md-math',
2254
+ standalone: true,
2255
+ changeDetection: ChangeDetectionStrategy.OnPush,
2256
+ encapsulation: ViewEncapsulation.None,
2257
+ template: `
2258
+ @if (html(); as h) {
2259
+ <span
2260
+ class="chat-md-math"
2261
+ [class.chat-md-math--display]="display()"
2262
+ [innerHTML]="h"
2263
+ ></span>
2264
+ } @else {
2265
+ <span class="chat-md-math chat-md-math--raw">{{ raw() }}</span>
2266
+ }
2267
+ `,
2268
+ }]
2269
+ }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2270
+
2146
2271
  // libs/chat/src/lib/markdown/views/markdown-link.component.ts
2147
2272
  // SPDX-License-Identifier: MIT
2148
2273
  class MarkdownLinkComponent {
@@ -2484,6 +2609,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
2484
2609
  }]
2485
2610
  }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2486
2611
 
2612
+ // libs/chat/src/lib/markdown/views/markdown-html.component.ts
2613
+ // SPDX-License-Identifier: MIT
2614
+ /**
2615
+ * Renders a `html-block` / `html-inline` markdown node as **escaped text** —
2616
+ * the raw HTML is shown literally (Angular interpolation auto-escapes it),
2617
+ * never injected as live markup. This preserves the pre-0.4 behavior where
2618
+ * raw HTML was plain text, and keeps the chat XSS-safe: model-emitted
2619
+ * `<script>`, `<iframe>`, etc. are displayed as text and never executed.
2620
+ */
2621
+ class MarkdownHtmlComponent {
2622
+ node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
2623
+ raw = computed(() => this.node().raw, ...(ngDevMode ? [{ debugName: "raw" }] : []));
2624
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownHtmlComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2625
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.6", type: MarkdownHtmlComponent, isStandalone: true, selector: "chat-md-html", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `{{ raw() }}`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
2626
+ }
2627
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: MarkdownHtmlComponent, decorators: [{
2628
+ type: Component,
2629
+ args: [{
2630
+ selector: 'chat-md-html',
2631
+ standalone: true,
2632
+ changeDetection: ChangeDetectionStrategy.OnPush,
2633
+ template: `{{ raw() }}`,
2634
+ }]
2635
+ }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2636
+
2487
2637
  // libs/chat/src/lib/markdown/cacheplane-markdown-views.ts
2488
2638
  // SPDX-License-Identifier: MIT
2489
2639
  /**
@@ -2507,6 +2657,8 @@ const cacheplaneMarkdownViews = views({
2507
2657
  'strong': MarkdownStrongComponent,
2508
2658
  'strikethrough': MarkdownStrikethroughComponent,
2509
2659
  'inline-code': MarkdownInlineCodeComponent,
2660
+ 'math-inline': MarkdownMathComponent,
2661
+ 'math-display': MarkdownMathComponent,
2510
2662
  'link': MarkdownLinkComponent,
2511
2663
  'autolink': MarkdownAutolinkComponent,
2512
2664
  'image': MarkdownImageComponent,
@@ -2516,6 +2668,10 @@ const cacheplaneMarkdownViews = views({
2516
2668
  'table': MarkdownTableComponent,
2517
2669
  'table-row': MarkdownTableRowComponent,
2518
2670
  'table-cell': MarkdownTableCellComponent,
2671
+ // Raw HTML (added by partial-markdown 0.4.x) renders as escaped literal text
2672
+ // — XSS-safe, and matches the pre-0.4 behavior where HTML was plain text.
2673
+ 'html-block': MarkdownHtmlComponent,
2674
+ 'html-inline': MarkdownHtmlComponent,
2519
2675
  });
2520
2676
 
2521
2677
  // libs/chat/src/lib/streaming/streaming-markdown.component.ts
@@ -2609,7 +2765,7 @@ class ChatStreamingMdComponent {
2609
2765
  @if (root(); as r) {
2610
2766
  <chat-md-children [parent]="r" />
2611
2767
  }
2612
- `, isInline: true, styles: ["chat-streaming-md{display:block;color:var(--ngaf-chat-text);line-height:var(--ngaf-chat-line-height)}chat-streaming-md h1,chat-streaming-md h2,chat-streaming-md h3,chat-streaming-md h4,chat-streaming-md h5,chat-streaming-md h6{font-weight:600;line-height:1.25;margin:1.25rem 0 .75rem}chat-streaming-md h1:first-child,chat-streaming-md h2:first-child,chat-streaming-md h3:first-child,chat-streaming-md h4:first-child,chat-streaming-md h5:first-child,chat-streaming-md h6:first-child{margin-top:0}chat-streaming-md h1{font-size:1.5em;font-weight:700}chat-streaming-md h2{font-size:1.25em}chat-streaming-md h3{font-size:1.1em}chat-streaming-md h4{font-size:1em}chat-streaming-md h5,chat-streaming-md h6{font-size:.95em;color:var(--ngaf-chat-text-muted)}chat-streaming-md p{margin:0 0 .75rem;line-height:1.6;font-size:var(--ngaf-chat-font-size)}chat-streaming-md p:last-child{margin-bottom:0}chat-streaming-md strong,chat-streaming-md b{font-weight:700}chat-streaming-md em,chat-streaming-md i{font-style:italic}chat-streaming-md del,chat-streaming-md s{text-decoration:line-through;color:var(--ngaf-chat-text-muted)}chat-streaming-md mark{background:var(--ngaf-chat-surface-alt);padding:0 2px;border-radius:2px}chat-streaming-md sub{font-size:.75em;vertical-align:sub}chat-streaming-md sup{font-size:.75em;vertical-align:super}chat-streaming-md a{color:var(--ngaf-chat-primary);text-decoration:underline;text-underline-offset:2px}chat-streaming-md a:hover{text-decoration-thickness:2px}chat-streaming-md ul,chat-streaming-md ol{margin:0 0 .75rem;padding-left:1.5rem}chat-streaming-md ul{list-style:disc outside}chat-streaming-md ol{list-style:decimal outside}chat-streaming-md ul ul{list-style:circle outside}chat-streaming-md ul ul ul{list-style:square outside}chat-streaming-md li{margin:.2rem 0}chat-streaming-md li::marker{color:var(--ngaf-chat-text-muted)}chat-streaming-md li>p{margin:0 0 .25rem}chat-streaming-md li>ul,chat-streaming-md li>ol{margin:.25rem 0 0}chat-streaming-md li:has(>input[type=checkbox]){list-style:none;margin-left:-1.25rem}chat-streaming-md li>input[type=checkbox]{margin-right:.5rem;vertical-align:middle}chat-streaming-md code{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text);padding:1px 5px;border-radius:4px;font-family:var(--ngaf-chat-font-mono);font-size:.9em}chat-streaming-md pre{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text);padding:12px 14px;border-radius:var(--ngaf-chat-radius-card);overflow-x:auto;font-family:var(--ngaf-chat-font-mono);font-size:var(--ngaf-chat-font-size-sm);line-height:1.5;margin:0 0 .75rem}chat-streaming-md pre code{background:transparent;padding:0;border-radius:0;font-size:inherit}chat-streaming-md blockquote{border-left:3px solid var(--ngaf-chat-separator);padding:.25rem 0 .25rem 12px;margin:0 0 .75rem;color:var(--ngaf-chat-text-muted)}chat-streaming-md blockquote>:last-child{margin-bottom:0}chat-streaming-md hr{border:none;border-top:1px solid var(--ngaf-chat-separator);margin:1rem 0}chat-streaming-md table{border-collapse:collapse;margin:0 0 .75rem;width:100%;font-size:.95em}chat-streaming-md thead{background:var(--ngaf-chat-surface-alt)}chat-streaming-md th,chat-streaming-md td{border:1px solid var(--ngaf-chat-separator);padding:6px 10px;text-align:left;vertical-align:top}chat-streaming-md th{font-weight:600}chat-streaming-md chat-md-table{display:block;overflow-x:auto;max-width:100%;margin:0 0 .75rem}chat-streaming-md chat-md-table-row{display:contents}chat-streaming-md chat-md-table-cell{display:contents}chat-streaming-md chat-md-table>table{margin:0}chat-streaming-md li.chat-md-list-item--task{list-style:none;margin-left:-1.25rem;display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}chat-streaming-md li.chat-md-list-item--task>input[type=checkbox]{margin:0;flex:0 0 auto;transform:translateY(2px)}chat-streaming-md li.chat-md-list-item--task>chat-md-children{flex:1 1 auto;min-width:0}chat-streaming-md li.chat-md-list-item--task>chat-md-children>chat-md-paragraph:first-child>p{margin:0}chat-streaming-md img{max-width:100%;height:auto;border-radius:6px}chat-streaming-md .chat-md-image--broken{display:inline-flex;align-items:center;gap:.4rem;padding:.25rem .5rem;background:var(--ngaf-chat-surface-alt);border:1px dashed var(--ngaf-chat-separator);border-radius:6px;font-size:.9em;color:var(--ngaf-chat-text-muted, currentColor);opacity:.85}chat-streaming-md .chat-md-image__icon{font-size:1em;line-height:1}chat-streaming-md .chat-md-image__alt{font-style:italic}\n"], dependencies: [{ kind: "component", type: MarkdownChildrenComponent, selector: "chat-md-children", inputs: ["parent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2768
+ `, isInline: true, styles: ["chat-streaming-md{display:block;color:var(--ngaf-chat-text);line-height:var(--ngaf-chat-line-height)}chat-streaming-md h1,chat-streaming-md h2,chat-streaming-md h3,chat-streaming-md h4,chat-streaming-md h5,chat-streaming-md h6{font-weight:600;line-height:1.25;margin:1.25rem 0 .75rem}chat-streaming-md h1:first-child,chat-streaming-md h2:first-child,chat-streaming-md h3:first-child,chat-streaming-md h4:first-child,chat-streaming-md h5:first-child,chat-streaming-md h6:first-child{margin-top:0}chat-streaming-md h1{font-size:1.5em;font-weight:700}chat-streaming-md h2{font-size:1.25em}chat-streaming-md h3{font-size:1.1em}chat-streaming-md h4{font-size:1em}chat-streaming-md h5,chat-streaming-md h6{font-size:.95em;color:var(--ngaf-chat-text-muted)}chat-streaming-md p{margin:0 0 .75rem;line-height:1.6;font-size:var(--ngaf-chat-font-size)}chat-streaming-md p:last-child{margin-bottom:0}chat-streaming-md strong,chat-streaming-md b{font-weight:700}chat-streaming-md em,chat-streaming-md i{font-style:italic}chat-streaming-md del,chat-streaming-md s{text-decoration:line-through;color:var(--ngaf-chat-text-muted)}chat-streaming-md mark{background:var(--ngaf-chat-surface-alt);padding:0 2px;border-radius:2px}chat-streaming-md sub{font-size:.75em;vertical-align:sub}chat-streaming-md sup{font-size:.75em;vertical-align:super}chat-streaming-md a{color:var(--ngaf-chat-primary);text-decoration:underline;text-underline-offset:2px}chat-streaming-md a:hover{text-decoration-thickness:2px}chat-streaming-md ul,chat-streaming-md ol{margin:0 0 .75rem;padding-left:1.5rem}chat-streaming-md ul{list-style:disc outside}chat-streaming-md ol{list-style:decimal outside}chat-streaming-md ul ul{list-style:circle outside}chat-streaming-md ul ul ul{list-style:square outside}chat-streaming-md li{margin:.2rem 0}chat-streaming-md li::marker{color:var(--ngaf-chat-text-muted)}chat-streaming-md li>p{margin:0 0 .25rem}chat-streaming-md li>ul,chat-streaming-md li>ol{margin:.25rem 0 0}chat-streaming-md li:has(>input[type=checkbox]){list-style:none;margin-left:-1.25rem}chat-streaming-md li>input[type=checkbox]{margin-right:.5rem;vertical-align:middle}chat-streaming-md code{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text);padding:1px 5px;border-radius:4px;font-family:var(--ngaf-chat-font-mono);font-size:.9em}chat-streaming-md pre{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text);padding:12px 14px;border-radius:var(--ngaf-chat-radius-card);overflow-x:auto;font-family:var(--ngaf-chat-font-mono);font-size:var(--ngaf-chat-font-size-sm);line-height:1.5;margin:0 0 .75rem}chat-streaming-md pre code{background:transparent;padding:0;border-radius:0;font-size:inherit}chat-streaming-md blockquote{border-left:3px solid var(--ngaf-chat-separator);padding:.25rem 0 .25rem 12px;margin:0 0 .75rem;color:var(--ngaf-chat-text-muted)}chat-streaming-md blockquote>:last-child{margin-bottom:0}chat-streaming-md hr{border:none;border-top:1px solid var(--ngaf-chat-separator);margin:1rem 0}chat-streaming-md table{border-collapse:collapse;margin:0 0 .75rem;width:100%;font-size:.95em}chat-streaming-md thead{background:var(--ngaf-chat-surface-alt)}chat-streaming-md th,chat-streaming-md td{border:1px solid var(--ngaf-chat-separator);padding:6px 10px;text-align:left;vertical-align:top}chat-streaming-md th{font-weight:600}chat-streaming-md chat-md-table{display:block;overflow-x:auto;max-width:100%;margin:0 0 .75rem}chat-streaming-md chat-md-table-row{display:contents}chat-streaming-md chat-md-table-cell{display:contents}chat-streaming-md chat-md-table>table{margin:0}chat-streaming-md li.chat-md-list-item--task{list-style:none;margin-left:-1.25rem;display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}chat-streaming-md li.chat-md-list-item--task>input[type=checkbox]{margin:0;flex:0 0 auto;transform:translateY(2px)}chat-streaming-md li.chat-md-list-item--task>chat-md-children{flex:1 1 auto;min-width:0}chat-streaming-md li.chat-md-list-item--task>chat-md-children>chat-md-paragraph:first-child>p{margin:0}chat-streaming-md img{max-width:100%;height:auto;border-radius:6px}chat-streaming-md .chat-md-image--broken{display:inline-flex;align-items:center;gap:.4rem;padding:.25rem .5rem;background:var(--ngaf-chat-surface-alt);border:1px dashed var(--ngaf-chat-separator);border-radius:6px;font-size:.9em;color:var(--ngaf-chat-text-muted, currentColor);opacity:.85}chat-streaming-md .chat-md-image__icon{font-size:1em;line-height:1}chat-streaming-md .chat-md-image__alt{font-style:italic}chat-streaming-md .chat-md-math--display{display:block;margin:.5em 0;overflow-x:auto}chat-streaming-md .chat-md-math--raw{font-family:var(--ngaf-chat-font-mono, ui-monospace, monospace)}\n"], dependencies: [{ kind: "component", type: MarkdownChildrenComponent, selector: "chat-md-children", inputs: ["parent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2613
2769
  }
2614
2770
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatStreamingMdComponent, decorators: [{
2615
2771
  type: Component,
@@ -2623,7 +2779,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
2623
2779
  useFactory: (host) => host.resolvedRegistry(),
2624
2780
  deps: [ChatStreamingMdComponent],
2625
2781
  },
2626
- ], styles: ["chat-streaming-md{display:block;color:var(--ngaf-chat-text);line-height:var(--ngaf-chat-line-height)}chat-streaming-md h1,chat-streaming-md h2,chat-streaming-md h3,chat-streaming-md h4,chat-streaming-md h5,chat-streaming-md h6{font-weight:600;line-height:1.25;margin:1.25rem 0 .75rem}chat-streaming-md h1:first-child,chat-streaming-md h2:first-child,chat-streaming-md h3:first-child,chat-streaming-md h4:first-child,chat-streaming-md h5:first-child,chat-streaming-md h6:first-child{margin-top:0}chat-streaming-md h1{font-size:1.5em;font-weight:700}chat-streaming-md h2{font-size:1.25em}chat-streaming-md h3{font-size:1.1em}chat-streaming-md h4{font-size:1em}chat-streaming-md h5,chat-streaming-md h6{font-size:.95em;color:var(--ngaf-chat-text-muted)}chat-streaming-md p{margin:0 0 .75rem;line-height:1.6;font-size:var(--ngaf-chat-font-size)}chat-streaming-md p:last-child{margin-bottom:0}chat-streaming-md strong,chat-streaming-md b{font-weight:700}chat-streaming-md em,chat-streaming-md i{font-style:italic}chat-streaming-md del,chat-streaming-md s{text-decoration:line-through;color:var(--ngaf-chat-text-muted)}chat-streaming-md mark{background:var(--ngaf-chat-surface-alt);padding:0 2px;border-radius:2px}chat-streaming-md sub{font-size:.75em;vertical-align:sub}chat-streaming-md sup{font-size:.75em;vertical-align:super}chat-streaming-md a{color:var(--ngaf-chat-primary);text-decoration:underline;text-underline-offset:2px}chat-streaming-md a:hover{text-decoration-thickness:2px}chat-streaming-md ul,chat-streaming-md ol{margin:0 0 .75rem;padding-left:1.5rem}chat-streaming-md ul{list-style:disc outside}chat-streaming-md ol{list-style:decimal outside}chat-streaming-md ul ul{list-style:circle outside}chat-streaming-md ul ul ul{list-style:square outside}chat-streaming-md li{margin:.2rem 0}chat-streaming-md li::marker{color:var(--ngaf-chat-text-muted)}chat-streaming-md li>p{margin:0 0 .25rem}chat-streaming-md li>ul,chat-streaming-md li>ol{margin:.25rem 0 0}chat-streaming-md li:has(>input[type=checkbox]){list-style:none;margin-left:-1.25rem}chat-streaming-md li>input[type=checkbox]{margin-right:.5rem;vertical-align:middle}chat-streaming-md code{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text);padding:1px 5px;border-radius:4px;font-family:var(--ngaf-chat-font-mono);font-size:.9em}chat-streaming-md pre{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text);padding:12px 14px;border-radius:var(--ngaf-chat-radius-card);overflow-x:auto;font-family:var(--ngaf-chat-font-mono);font-size:var(--ngaf-chat-font-size-sm);line-height:1.5;margin:0 0 .75rem}chat-streaming-md pre code{background:transparent;padding:0;border-radius:0;font-size:inherit}chat-streaming-md blockquote{border-left:3px solid var(--ngaf-chat-separator);padding:.25rem 0 .25rem 12px;margin:0 0 .75rem;color:var(--ngaf-chat-text-muted)}chat-streaming-md blockquote>:last-child{margin-bottom:0}chat-streaming-md hr{border:none;border-top:1px solid var(--ngaf-chat-separator);margin:1rem 0}chat-streaming-md table{border-collapse:collapse;margin:0 0 .75rem;width:100%;font-size:.95em}chat-streaming-md thead{background:var(--ngaf-chat-surface-alt)}chat-streaming-md th,chat-streaming-md td{border:1px solid var(--ngaf-chat-separator);padding:6px 10px;text-align:left;vertical-align:top}chat-streaming-md th{font-weight:600}chat-streaming-md chat-md-table{display:block;overflow-x:auto;max-width:100%;margin:0 0 .75rem}chat-streaming-md chat-md-table-row{display:contents}chat-streaming-md chat-md-table-cell{display:contents}chat-streaming-md chat-md-table>table{margin:0}chat-streaming-md li.chat-md-list-item--task{list-style:none;margin-left:-1.25rem;display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}chat-streaming-md li.chat-md-list-item--task>input[type=checkbox]{margin:0;flex:0 0 auto;transform:translateY(2px)}chat-streaming-md li.chat-md-list-item--task>chat-md-children{flex:1 1 auto;min-width:0}chat-streaming-md li.chat-md-list-item--task>chat-md-children>chat-md-paragraph:first-child>p{margin:0}chat-streaming-md img{max-width:100%;height:auto;border-radius:6px}chat-streaming-md .chat-md-image--broken{display:inline-flex;align-items:center;gap:.4rem;padding:.25rem .5rem;background:var(--ngaf-chat-surface-alt);border:1px dashed var(--ngaf-chat-separator);border-radius:6px;font-size:.9em;color:var(--ngaf-chat-text-muted, currentColor);opacity:.85}chat-streaming-md .chat-md-image__icon{font-size:1em;line-height:1}chat-streaming-md .chat-md-image__alt{font-style:italic}\n"] }]
2782
+ ], styles: ["chat-streaming-md{display:block;color:var(--ngaf-chat-text);line-height:var(--ngaf-chat-line-height)}chat-streaming-md h1,chat-streaming-md h2,chat-streaming-md h3,chat-streaming-md h4,chat-streaming-md h5,chat-streaming-md h6{font-weight:600;line-height:1.25;margin:1.25rem 0 .75rem}chat-streaming-md h1:first-child,chat-streaming-md h2:first-child,chat-streaming-md h3:first-child,chat-streaming-md h4:first-child,chat-streaming-md h5:first-child,chat-streaming-md h6:first-child{margin-top:0}chat-streaming-md h1{font-size:1.5em;font-weight:700}chat-streaming-md h2{font-size:1.25em}chat-streaming-md h3{font-size:1.1em}chat-streaming-md h4{font-size:1em}chat-streaming-md h5,chat-streaming-md h6{font-size:.95em;color:var(--ngaf-chat-text-muted)}chat-streaming-md p{margin:0 0 .75rem;line-height:1.6;font-size:var(--ngaf-chat-font-size)}chat-streaming-md p:last-child{margin-bottom:0}chat-streaming-md strong,chat-streaming-md b{font-weight:700}chat-streaming-md em,chat-streaming-md i{font-style:italic}chat-streaming-md del,chat-streaming-md s{text-decoration:line-through;color:var(--ngaf-chat-text-muted)}chat-streaming-md mark{background:var(--ngaf-chat-surface-alt);padding:0 2px;border-radius:2px}chat-streaming-md sub{font-size:.75em;vertical-align:sub}chat-streaming-md sup{font-size:.75em;vertical-align:super}chat-streaming-md a{color:var(--ngaf-chat-primary);text-decoration:underline;text-underline-offset:2px}chat-streaming-md a:hover{text-decoration-thickness:2px}chat-streaming-md ul,chat-streaming-md ol{margin:0 0 .75rem;padding-left:1.5rem}chat-streaming-md ul{list-style:disc outside}chat-streaming-md ol{list-style:decimal outside}chat-streaming-md ul ul{list-style:circle outside}chat-streaming-md ul ul ul{list-style:square outside}chat-streaming-md li{margin:.2rem 0}chat-streaming-md li::marker{color:var(--ngaf-chat-text-muted)}chat-streaming-md li>p{margin:0 0 .25rem}chat-streaming-md li>ul,chat-streaming-md li>ol{margin:.25rem 0 0}chat-streaming-md li:has(>input[type=checkbox]){list-style:none;margin-left:-1.25rem}chat-streaming-md li>input[type=checkbox]{margin-right:.5rem;vertical-align:middle}chat-streaming-md code{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text);padding:1px 5px;border-radius:4px;font-family:var(--ngaf-chat-font-mono);font-size:.9em}chat-streaming-md pre{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text);padding:12px 14px;border-radius:var(--ngaf-chat-radius-card);overflow-x:auto;font-family:var(--ngaf-chat-font-mono);font-size:var(--ngaf-chat-font-size-sm);line-height:1.5;margin:0 0 .75rem}chat-streaming-md pre code{background:transparent;padding:0;border-radius:0;font-size:inherit}chat-streaming-md blockquote{border-left:3px solid var(--ngaf-chat-separator);padding:.25rem 0 .25rem 12px;margin:0 0 .75rem;color:var(--ngaf-chat-text-muted)}chat-streaming-md blockquote>:last-child{margin-bottom:0}chat-streaming-md hr{border:none;border-top:1px solid var(--ngaf-chat-separator);margin:1rem 0}chat-streaming-md table{border-collapse:collapse;margin:0 0 .75rem;width:100%;font-size:.95em}chat-streaming-md thead{background:var(--ngaf-chat-surface-alt)}chat-streaming-md th,chat-streaming-md td{border:1px solid var(--ngaf-chat-separator);padding:6px 10px;text-align:left;vertical-align:top}chat-streaming-md th{font-weight:600}chat-streaming-md chat-md-table{display:block;overflow-x:auto;max-width:100%;margin:0 0 .75rem}chat-streaming-md chat-md-table-row{display:contents}chat-streaming-md chat-md-table-cell{display:contents}chat-streaming-md chat-md-table>table{margin:0}chat-streaming-md li.chat-md-list-item--task{list-style:none;margin-left:-1.25rem;display:flex;flex-wrap:wrap;align-items:baseline;gap:.5rem}chat-streaming-md li.chat-md-list-item--task>input[type=checkbox]{margin:0;flex:0 0 auto;transform:translateY(2px)}chat-streaming-md li.chat-md-list-item--task>chat-md-children{flex:1 1 auto;min-width:0}chat-streaming-md li.chat-md-list-item--task>chat-md-children>chat-md-paragraph:first-child>p{margin:0}chat-streaming-md img{max-width:100%;height:auto;border-radius:6px}chat-streaming-md .chat-md-image--broken{display:inline-flex;align-items:center;gap:.4rem;padding:.25rem .5rem;background:var(--ngaf-chat-surface-alt);border:1px dashed var(--ngaf-chat-separator);border-radius:6px;font-size:.9em;color:var(--ngaf-chat-text-muted, currentColor);opacity:.85}chat-streaming-md .chat-md-image__icon{font-size:1em;line-height:1}chat-streaming-md .chat-md-image__alt{font-style:italic}chat-streaming-md .chat-md-math--display{display:block;margin:.5em 0;overflow-x:auto}chat-streaming-md .chat-md-math--raw{font-family:var(--ngaf-chat-font-mono, ui-monospace, monospace)}\n"] }]
2627
2783
  }], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "content", required: false }] }], streaming: [{ type: i0.Input, args: [{ isSignal: true, alias: "streaming", required: false }] }], viewRegistry: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewRegistry", required: false }] }] } });
2628
2784
 
2629
2785
  // SPDX-License-Identifier: MIT
@@ -4108,6 +4264,19 @@ const CHAT_ERROR_STYLES = `
4108
4264
 
4109
4265
  // libs/chat/src/lib/primitives/chat-error/chat-error.component.ts
4110
4266
  // SPDX-License-Identifier: MIT
4267
+ /**
4268
+ * Coerce an unknown error value into a human-readable message string — reads
4269
+ * `.message` from `Error`s, returns strings as-is, and `String()`-casts the
4270
+ * rest. Useful when rendering an agent's `error` outside the built-in
4271
+ * `chat-error` component.
4272
+ *
4273
+ * @param error Any caught/agent error value.
4274
+ * @returns The message text, or `null` when `error` is nullish.
4275
+ * @example
4276
+ * ```ts
4277
+ * const msg = extractErrorMessage(agent.error());
4278
+ * ```
4279
+ */
4111
4280
  function extractErrorMessage(error) {
4112
4281
  if (!error)
4113
4282
  return null;
@@ -8791,6 +8960,62 @@ class ChatComponent {
8791
8960
  const text = typeof message.content === 'string' ? message.content : '';
8792
8961
  return text.length === 0;
8793
8962
  }
8963
+ /** The nearest preceding assistant message (skipping hidden tool messages), or undefined. */
8964
+ prevAssistant(msgs, index) {
8965
+ for (let j = index - 1; j >= 0; j--) {
8966
+ if (msgs[j].role === 'tool')
8967
+ continue;
8968
+ return msgs[j].role === 'assistant' ? msgs[j] : undefined;
8969
+ }
8970
+ return undefined;
8971
+ }
8972
+ /**
8973
+ * True when message[index] starts a reasoning RUN — a maximal sequence of
8974
+ * consecutive assistant reasoning steps separated only by (hidden) tool
8975
+ * messages. The merged reasoning pill renders once, here.
8976
+ */
8977
+ reasoningRunStart(index) {
8978
+ const msgs = this.agent().messages();
8979
+ if (!msgs[index]?.reasoning)
8980
+ return false;
8981
+ return !this.prevAssistant(msgs, index)?.reasoning;
8982
+ }
8983
+ /**
8984
+ * Aggregate the reasoning RUN starting at `index`: joins each step's
8985
+ * reasoning, sums durations, counts steps, and computes the streaming flag
8986
+ * and the merged label when N > 1 ("Thought for {total} · {N} steps", or
8987
+ * just "{N} steps" when no step reported timing).
8988
+ */
8989
+ reasoningRun(index) {
8990
+ const msgs = this.agent().messages();
8991
+ const steps = [];
8992
+ for (let j = index; j < msgs.length; j++) {
8993
+ const m = msgs[j];
8994
+ if (m.role === 'tool')
8995
+ continue; // skip hidden tool messages
8996
+ if (m.role === 'assistant' && m.reasoning) {
8997
+ steps.push({ msg: m, idx: j });
8998
+ continue;
8999
+ }
9000
+ break; // any other message ends the run
9001
+ }
9002
+ const content = steps.map((s) => s.msg.reasoning ?? '').filter(Boolean).join('\n\n');
9003
+ const durations = steps
9004
+ .map((s) => s.msg.reasoningDurationMs)
9005
+ .filter((d) => typeof d === 'number');
9006
+ const durationMs = durations.length ? durations.reduce((a, b) => a + b, 0) : undefined;
9007
+ const last = steps[steps.length - 1];
9008
+ const streaming = last ? this.isReasoningStreaming(last.msg, last.idx) : false;
9009
+ // Only claim a duration when at least one step reported timing. Otherwise
9010
+ // "Thought for <1s" would read as "fast" when it really means "unknown", so
9011
+ // drop the duration phrase and label by step count alone.
9012
+ const label = steps.length > 1
9013
+ ? durationMs !== undefined
9014
+ ? `Thought for ${formatDuration(durationMs)} · ${steps.length} steps`
9015
+ : `${steps.length} steps`
9016
+ : undefined;
9017
+ return { content, durationMs, streaming, label };
9018
+ }
8794
9019
  classifiers = new Map();
8795
9020
  destroyRef = inject(DestroyRef);
8796
9021
  injector = inject(Injector);
@@ -9350,11 +9575,19 @@ class ChatComponent {
9350
9575
  [streaming]="agent().isLoading() && i === agent().messages().length - 1"
9351
9576
  [current]="i === agent().messages().length - 1"
9352
9577
  >
9353
- @if (message.reasoning) {
9578
+ <!-- Reasoning is merged across a run of consecutive (tool-
9579
+ separated) reasoning steps and rendered ONCE at the run's
9580
+ first step as "Thought for {total} · {N} steps", so a
9581
+ multi-step agent shows one compact pill instead of a
9582
+ stack of "Thought for 1s" chips. Single-step turns render
9583
+ a normal "Thought for {duration}" pill. -->
9584
+ @if (message.reasoning && reasoningRunStart(i)) {
9585
+ @let run = reasoningRun(i);
9354
9586
  <chat-reasoning
9355
- [content]="message.reasoning"
9356
- [isStreaming]="isReasoningStreaming(message, i)"
9357
- [durationMs]="message.reasoningDurationMs"
9587
+ [content]="run.content"
9588
+ [isStreaming]="run.streaming"
9589
+ [durationMs]="run.durationMs"
9590
+ [label]="run.label"
9358
9591
  />
9359
9592
  }
9360
9593
  <chat-tool-calls [agent]="agent()" [message]="message" [excludeToolNames]="excludedToolNames()">
@@ -9401,14 +9634,21 @@ class ChatComponent {
9401
9634
  />
9402
9635
  }
9403
9636
  }
9404
- <chat-message-actions
9405
- chatMessageControls
9406
- [content]="content"
9407
- [disabled]="agent().isLoading()"
9408
- (regenerate)="onRegenerate(i)"
9409
- (rate)="onRate(message, $event)"
9410
- (contentCopied)="onCopy(message, $event)"
9411
- />
9637
+ <!-- Only show message actions when there is copyable assistant
9638
+ text. Content-less messages (a bare tool call or a subagent
9639
+ delegation card) have nothing to copy/regenerate/rate, so the
9640
+ actions panel is pure whitespace there — suppress it to keep
9641
+ the stream compact. -->
9642
+ @if (content.trim()) {
9643
+ <chat-message-actions
9644
+ chatMessageControls
9645
+ [content]="content"
9646
+ [disabled]="agent().isLoading()"
9647
+ (regenerate)="onRegenerate(i)"
9648
+ (rate)="onRate(message, $event)"
9649
+ (contentCopied)="onCopy(message, $event)"
9650
+ />
9651
+ }
9412
9652
  </chat-message>
9413
9653
  </ng-template>
9414
9654
 
@@ -9519,11 +9759,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
9519
9759
  [streaming]="agent().isLoading() && i === agent().messages().length - 1"
9520
9760
  [current]="i === agent().messages().length - 1"
9521
9761
  >
9522
- @if (message.reasoning) {
9762
+ <!-- Reasoning is merged across a run of consecutive (tool-
9763
+ separated) reasoning steps and rendered ONCE at the run's
9764
+ first step as "Thought for {total} · {N} steps", so a
9765
+ multi-step agent shows one compact pill instead of a
9766
+ stack of "Thought for 1s" chips. Single-step turns render
9767
+ a normal "Thought for {duration}" pill. -->
9768
+ @if (message.reasoning && reasoningRunStart(i)) {
9769
+ @let run = reasoningRun(i);
9523
9770
  <chat-reasoning
9524
- [content]="message.reasoning"
9525
- [isStreaming]="isReasoningStreaming(message, i)"
9526
- [durationMs]="message.reasoningDurationMs"
9771
+ [content]="run.content"
9772
+ [isStreaming]="run.streaming"
9773
+ [durationMs]="run.durationMs"
9774
+ [label]="run.label"
9527
9775
  />
9528
9776
  }
9529
9777
  <chat-tool-calls [agent]="agent()" [message]="message" [excludeToolNames]="excludedToolNames()">
@@ -9570,14 +9818,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
9570
9818
  />
9571
9819
  }
9572
9820
  }
9573
- <chat-message-actions
9574
- chatMessageControls
9575
- [content]="content"
9576
- [disabled]="agent().isLoading()"
9577
- (regenerate)="onRegenerate(i)"
9578
- (rate)="onRate(message, $event)"
9579
- (contentCopied)="onCopy(message, $event)"
9580
- />
9821
+ <!-- Only show message actions when there is copyable assistant
9822
+ text. Content-less messages (a bare tool call or a subagent
9823
+ delegation card) have nothing to copy/regenerate/rate, so the
9824
+ actions panel is pure whitespace there — suppress it to keep
9825
+ the stream compact. -->
9826
+ @if (content.trim()) {
9827
+ <chat-message-actions
9828
+ chatMessageControls
9829
+ [content]="content"
9830
+ [disabled]="agent().isLoading()"
9831
+ (regenerate)="onRegenerate(i)"
9832
+ (rate)="onRate(message, $event)"
9833
+ (contentCopied)="onCopy(message, $event)"
9834
+ />
9835
+ }
9581
9836
  </chat-message>
9582
9837
  </ng-template>
9583
9838
 
@@ -11612,6 +11867,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
11612
11867
  }], propDecorators: { axis: [{ type: i0.Input, args: [{ isSignal: true, alias: "axis", required: false }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], bindings: [{ type: i0.Input, args: [{ isSignal: true, alias: "bindings", required: false }] }], emit: [{ type: i0.Input, args: [{ isSignal: true, alias: "emit", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], childKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "childKeys", required: false }] }], spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: false }] }] } });
11613
11868
 
11614
11869
  // SPDX-License-Identifier: MIT
11870
+ /**
11871
+ * Convert an icon identifier to its Material Symbols ligature form.
11872
+ *
11873
+ * Material Symbols ligatures are snake_case (`account_circle`, `trending_up`),
11874
+ * but A2UI catalogs commonly emit camelCase identifiers (`accountCircle`,
11875
+ * `shoppingCart`). Splitting on lower→upper boundaries and lowercasing maps
11876
+ * camelCase → the matching ligature. Already-snake_case names, single words,
11877
+ * and non-identifier glyphs (emoji) have no boundaries to split and pass
11878
+ * through unchanged. Unknown names still fall back to the browser default.
11879
+ */
11880
+ function toMaterialSymbolName(name) {
11881
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
11882
+ }
11615
11883
  class A2uiIconComponent {
11616
11884
  /** v1 canonical prop. */
11617
11885
  name = input(undefined, ...(ngDevMode ? [{ debugName: "name" }] : []));
@@ -11625,24 +11893,32 @@ class A2uiIconComponent {
11625
11893
  childKeys = input([], ...(ngDevMode ? [{ debugName: "childKeys" }] : []));
11626
11894
  spec = input(undefined, ...(ngDevMode ? [{ debugName: "spec" }] : []));
11627
11895
  effectiveName = computed(() => this.name() ?? this.icon(), ...(ngDevMode ? [{ debugName: "effectiveName" }] : []));
11896
+ /** The effective name as a Material Symbols ligature (camelCase → snake_case). */
11897
+ glyphName = computed(() => toMaterialSymbolName(this.effectiveName()), ...(ngDevMode ? [{ debugName: "glyphName" }] : []));
11628
11898
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: A2uiIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11629
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.6", type: A2uiIconComponent, isStandalone: true, selector: "a2ui-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, bindings: { classPropertyName: "bindings", publicName: "bindings", isSignal: true, isRequired: false, transformFunction: null }, emit: { classPropertyName: "emit", publicName: "emit", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, childKeys: { classPropertyName: "childKeys", publicName: "childKeys", isSignal: true, isRequired: false, transformFunction: null }, spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
11630
- <span
11631
- class="a2ui-icon"
11632
- [style.font-size]="size() ? size() + 'px' : '1.125rem'"
11633
- [attr.aria-label]="effectiveName()"
11634
- >{{ effectiveName() }}</span>
11635
- `, isInline: true, styles: [".a2ui-icon{display:inline-flex;align-items:center;justify-content:center;-webkit-user-select:none;user-select:none}\n"] });
11899
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: A2uiIconComponent, isStandalone: true, selector: "a2ui-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, bindings: { classPropertyName: "bindings", publicName: "bindings", isSignal: true, isRequired: false, transformFunction: null }, emit: { classPropertyName: "emit", publicName: "emit", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, childKeys: { classPropertyName: "childKeys", publicName: "childKeys", isSignal: true, isRequired: false, transformFunction: null }, spec: { classPropertyName: "spec", publicName: "spec", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
11900
+ @if (effectiveName(); as name) {
11901
+ <span
11902
+ class="a2ui-icon material-symbols-outlined"
11903
+ [style.font-size]="size() ? size() + 'px' : '1.125rem'"
11904
+ [attr.aria-label]="name"
11905
+ role="img"
11906
+ >{{ glyphName() }}</span>
11907
+ }
11908
+ `, isInline: true, styles: [".a2ui-icon{font-family:Material Symbols Outlined;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;font-feature-settings:\"liga\";-webkit-font-feature-settings:\"liga\";-webkit-font-smoothing:antialiased;font-variation-settings:\"FILL\" 0,\"wght\" 400,\"GRAD\" 0,\"opsz\" 24;color:currentColor;display:inline-flex;align-items:center;justify-content:center;-webkit-user-select:none;user-select:none}\n"] });
11636
11909
  }
11637
11910
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: A2uiIconComponent, decorators: [{
11638
11911
  type: Component,
11639
11912
  args: [{ selector: 'a2ui-icon', standalone: true, template: `
11640
- <span
11641
- class="a2ui-icon"
11642
- [style.font-size]="size() ? size() + 'px' : '1.125rem'"
11643
- [attr.aria-label]="effectiveName()"
11644
- >{{ effectiveName() }}</span>
11645
- `, styles: [".a2ui-icon{display:inline-flex;align-items:center;justify-content:center;-webkit-user-select:none;user-select:none}\n"] }]
11913
+ @if (effectiveName(); as name) {
11914
+ <span
11915
+ class="a2ui-icon material-symbols-outlined"
11916
+ [style.font-size]="size() ? size() + 'px' : '1.125rem'"
11917
+ [attr.aria-label]="name"
11918
+ role="img"
11919
+ >{{ glyphName() }}</span>
11920
+ }
11921
+ `, styles: [".a2ui-icon{font-family:Material Symbols Outlined;font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;white-space:nowrap;word-wrap:normal;direction:ltr;font-feature-settings:\"liga\";-webkit-font-feature-settings:\"liga\";-webkit-font-smoothing:antialiased;font-variation-settings:\"FILL\" 0,\"wght\" 400,\"GRAD\" 0,\"opsz\" 24;color:currentColor;display:inline-flex;align-items:center;justify-content:center;-webkit-user-select:none;user-select:none}\n"] }]
11646
11922
  }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], bindings: [{ type: i0.Input, args: [{ isSignal: true, alias: "bindings", required: false }] }], emit: [{ type: i0.Input, args: [{ isSignal: true, alias: "emit", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], childKeys: [{ type: i0.Input, args: [{ isSignal: true, alias: "childKeys", required: false }] }], spec: [{ type: i0.Input, args: [{ isSignal: true, alias: "spec", required: false }] }] } });
11647
11923
 
11648
11924
  // SPDX-License-Identifier: MIT
@@ -12537,5 +12813,5 @@ function mockAgent(opts = {}) {
12537
12813
  * Generated bundle index. Do not edit.
12538
12814
  */
12539
12815
 
12540
- export { A2uiAudioPlayerComponent, A2uiButtonComponent, A2uiCardComponent, A2uiCheckBoxComponent, A2uiColumnComponent, A2uiDateTimeInputComponent, A2uiDividerComponent, A2uiIconComponent, A2uiImageComponent, A2uiListComponent, A2uiModalComponent, A2uiMultipleChoiceComponent, A2uiRowComponent, A2uiSliderComponent, A2uiSurfaceComponent, A2uiTabsComponent, A2uiTextComponent, A2uiTextFieldComponent, A2uiVideoComponent, AGENT_ERROR_MESSAGES, AgentError, CHAT_CONFIG, CHAT_LIFECYCLE, ChatApprovalCardComponent, ChatCitationCardTemplateDirective, ChatCitationsCardComponent, ChatCitationsComponent, ChatComponent, ChatConfirmDialogComponent, ChatErrorComponent, ChatGenerativeUiComponent, ChatGenuiSkeletonComponent, ChatHistorySearchPaletteComponent, ChatInputComponent, ChatInterruptComponent, ChatInterruptPanelComponent, ChatLauncherButtonComponent, ChatMessageActionsComponent, ChatMessageComponent, ChatMessageListComponent, ChatOverflowMenuComponent, ChatPopupComponent, ChatProjectListComponent, ChatReasoningComponent, ChatScrollBubbleComponent, ChatSelectComponent, ChatSidebarComponent, ChatSidenavComponent, ChatSidenavScrimComponent, ChatStreamingMdComponent, ChatSubagentCardComponent, ChatSubagentsComponent, ChatSuggestionsComponent, ChatThreadListComponent, ChatTimelineComponent, ChatTimelineSliderComponent, ChatToolCallCardComponent, ChatToolCallTemplateDirective, ChatToolCallsComponent, ChatToolViewsComponent, ChatTraceComponent, ChatTypingIndicatorComponent, ChatWelcomeComponent, ChatWelcomeSuggestionComponent, ChatWindowComponent, CitationsResolverService, IS_HEADER_ROW, MARKDOWN_VIEW_REGISTRY, MarkdownAutolinkComponent, MarkdownBlockquoteComponent, MarkdownChildrenComponent, MarkdownCitationReferenceComponent, MarkdownCodeBlockComponent, MarkdownDocumentComponent, MarkdownEmphasisComponent, MarkdownHardBreakComponent, MarkdownHeadingComponent, MarkdownImageComponent, MarkdownInlineCodeComponent, MarkdownLinkComponent, MarkdownListComponent, MarkdownListItemComponent, MarkdownParagraphComponent, MarkdownSoftBreakComponent, MarkdownStrikethroughComponent, MarkdownStrongComponent, MarkdownTableCellComponent, MarkdownTableComponent, MarkdownTableRowComponent, MarkdownTextComponent, MarkdownThematicBreakComponent, MessageTemplateDirective, a2uiBasicCatalog, action, ask, buildA2uiActionMessage, cacheplaneMarkdownViews, createA2uiSurfaceStore, createAgentRef, createContentClassifier, createParseTreeStore, createPartialArgsBridge, deriveJsonSchema, emitBinding, executeFunctionTool, extractErrorMessage, formatDuration, getInterrupt, getMessageType, injectThreadRouting, isAbortError, isAssistantMessage, isSystemMessage, isToolMessage, isTyping, isUserMessage, messageContent, mockAgent, normalizeEnvelopeArgs, normalizeViewEntry, provideChat, renderMarkdown, startClientToolExecutor, statusColor, submitMessage, toAgentError, toClientToolSpecs, tools, validateArgs, view };
12816
+ export { A2uiAudioPlayerComponent, A2uiButtonComponent, A2uiCardComponent, A2uiCheckBoxComponent, A2uiColumnComponent, A2uiDateTimeInputComponent, A2uiDividerComponent, A2uiIconComponent, A2uiImageComponent, A2uiListComponent, A2uiModalComponent, A2uiMultipleChoiceComponent, A2uiRowComponent, A2uiSliderComponent, A2uiSurfaceComponent, A2uiTabsComponent, A2uiTextComponent, A2uiTextFieldComponent, A2uiVideoComponent, AGENT_ERROR_MESSAGES, AgentError, CHAT_CONFIG, CHAT_LIFECYCLE, ChatApprovalCardComponent, ChatCitationCardTemplateDirective, ChatCitationsCardComponent, ChatCitationsComponent, ChatComponent, ChatConfirmDialogComponent, ChatErrorComponent, ChatGenerativeUiComponent, ChatGenuiSkeletonComponent, ChatHistorySearchPaletteComponent, ChatInputComponent, ChatInterruptComponent, ChatInterruptPanelComponent, ChatLauncherButtonComponent, ChatMessageActionsComponent, ChatMessageComponent, ChatMessageListComponent, ChatOverflowMenuComponent, ChatPopupComponent, ChatProjectListComponent, ChatReasoningComponent, ChatScrollBubbleComponent, ChatSelectComponent, ChatSidebarComponent, ChatSidenavComponent, ChatSidenavScrimComponent, ChatStreamingMdComponent, ChatSubagentCardComponent, ChatSubagentsComponent, ChatSuggestionsComponent, ChatThreadListComponent, ChatTimelineComponent, ChatTimelineSliderComponent, ChatToolCallCardComponent, ChatToolCallTemplateDirective, ChatToolCallsComponent, ChatToolViewsComponent, ChatTraceComponent, ChatTypingIndicatorComponent, ChatWelcomeComponent, ChatWelcomeSuggestionComponent, ChatWindowComponent, CitationsResolverService, IS_HEADER_ROW, MARKDOWN_VIEW_REGISTRY, MarkdownAutolinkComponent, MarkdownBlockquoteComponent, MarkdownChildrenComponent, MarkdownCitationReferenceComponent, MarkdownCodeBlockComponent, MarkdownDocumentComponent, MarkdownEmphasisComponent, MarkdownHardBreakComponent, MarkdownHeadingComponent, MarkdownHtmlComponent, MarkdownImageComponent, MarkdownInlineCodeComponent, MarkdownLinkComponent, MarkdownListComponent, MarkdownListItemComponent, MarkdownMathComponent, MarkdownParagraphComponent, MarkdownSoftBreakComponent, MarkdownStrikethroughComponent, MarkdownStrongComponent, MarkdownTableCellComponent, MarkdownTableComponent, MarkdownTableRowComponent, MarkdownTextComponent, MarkdownThematicBreakComponent, MessageTemplateDirective, a2uiBasicCatalog, action, ask, buildA2uiActionMessage, cacheplaneMarkdownViews, createA2uiSurfaceStore, createAgentRef, createContentClassifier, createParseTreeStore, createPartialArgsBridge, deriveJsonSchema, emitBinding, executeFunctionTool, extractErrorMessage, formatDuration, getInterrupt, getMessageType, injectThreadRouting, isAbortError, isAssistantMessage, isSystemMessage, isToolMessage, isTyping, isUserMessage, messageContent, mockAgent, normalizeEnvelopeArgs, normalizeViewEntry, provideChat, renderMarkdown, startClientToolExecutor, statusColor, submitMessage, toAgentError, toClientToolSpecs, tools, validateArgs, view };
12541
12817
  //# sourceMappingURL=threadplane-chat.mjs.map