@threadplane/chat 0.0.51 → 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.
@@ -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';
@@ -174,15 +175,55 @@ function toAgentError(raw) {
174
175
  return make('server', true, raw, undefined, msg);
175
176
  }
176
177
 
178
+ /**
179
+ * Type guard narrowing a {@link Message} to `role: 'user'`.
180
+ *
181
+ * @param m The message to test.
182
+ * @returns `true` (and narrows `m`) when the message was sent by the user.
183
+ * @example
184
+ * ```ts
185
+ * const userTurns = agent.messages().filter(isUserMessage);
186
+ * ```
187
+ */
177
188
  function isUserMessage(m) {
178
189
  return m.role === 'user';
179
190
  }
191
+ /**
192
+ * Type guard narrowing a {@link Message} to `role: 'assistant'`.
193
+ *
194
+ * @param m The message to test.
195
+ * @returns `true` (and narrows `m`) when the message came from the assistant.
196
+ * @example
197
+ * ```ts
198
+ * const reply = agent.messages().findLast(isAssistantMessage);
199
+ * ```
200
+ */
180
201
  function isAssistantMessage(m) {
181
202
  return m.role === 'assistant';
182
203
  }
204
+ /**
205
+ * Type guard narrowing a {@link Message} to `role: 'tool'` (a tool result turn).
206
+ *
207
+ * @param m The message to test.
208
+ * @returns `true` (and narrows `m`) when the message is a tool result.
209
+ * @example
210
+ * ```ts
211
+ * if (isToolMessage(m)) console.log(m.toolCallId);
212
+ * ```
213
+ */
183
214
  function isToolMessage(m) {
184
215
  return m.role === 'tool';
185
216
  }
217
+ /**
218
+ * Type guard narrowing a {@link Message} to `role: 'system'`.
219
+ *
220
+ * @param m The message to test.
221
+ * @returns `true` (and narrows `m`) when the message is a system message.
222
+ * @example
223
+ * ```ts
224
+ * const visible = agent.messages().filter((m) => !isSystemMessage(m));
225
+ * ```
226
+ */
186
227
  function isSystemMessage(m) {
187
228
  return m.role === 'system';
188
229
  }
@@ -1754,6 +1795,11 @@ const CHAT_MARKDOWN_STYLES = `
1754
1795
  }
1755
1796
  chat-streaming-md .chat-md-image__icon { font-size: 1em; line-height: 1; }
1756
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); }
1757
1803
  `;
1758
1804
 
1759
1805
  // libs/chat/src/lib/markdown/markdown-view-registry.ts
@@ -2103,6 +2149,125 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
2103
2149
  }]
2104
2150
  }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2105
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
+
2106
2271
  // libs/chat/src/lib/markdown/views/markdown-link.component.ts
2107
2272
  // SPDX-License-Identifier: MIT
2108
2273
  class MarkdownLinkComponent {
@@ -2444,6 +2609,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
2444
2609
  }]
2445
2610
  }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }] } });
2446
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
+
2447
2637
  // libs/chat/src/lib/markdown/cacheplane-markdown-views.ts
2448
2638
  // SPDX-License-Identifier: MIT
2449
2639
  /**
@@ -2467,6 +2657,8 @@ const cacheplaneMarkdownViews = views({
2467
2657
  'strong': MarkdownStrongComponent,
2468
2658
  'strikethrough': MarkdownStrikethroughComponent,
2469
2659
  'inline-code': MarkdownInlineCodeComponent,
2660
+ 'math-inline': MarkdownMathComponent,
2661
+ 'math-display': MarkdownMathComponent,
2470
2662
  'link': MarkdownLinkComponent,
2471
2663
  'autolink': MarkdownAutolinkComponent,
2472
2664
  'image': MarkdownImageComponent,
@@ -2476,6 +2668,10 @@ const cacheplaneMarkdownViews = views({
2476
2668
  'table': MarkdownTableComponent,
2477
2669
  'table-row': MarkdownTableRowComponent,
2478
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,
2479
2675
  });
2480
2676
 
2481
2677
  // libs/chat/src/lib/streaming/streaming-markdown.component.ts
@@ -2569,7 +2765,7 @@ class ChatStreamingMdComponent {
2569
2765
  @if (root(); as r) {
2570
2766
  <chat-md-children [parent]="r" />
2571
2767
  }
2572
- `, 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 });
2573
2769
  }
2574
2770
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatStreamingMdComponent, decorators: [{
2575
2771
  type: Component,
@@ -2583,7 +2779,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
2583
2779
  useFactory: (host) => host.resolvedRegistry(),
2584
2780
  deps: [ChatStreamingMdComponent],
2585
2781
  },
2586
- ], 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"] }]
2587
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 }] }] } });
2588
2784
 
2589
2785
  // SPDX-License-Identifier: MIT
@@ -3148,6 +3344,18 @@ const CHAT_TYPING_INDICATOR_STYLES = `
3148
3344
 
3149
3345
  // libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts
3150
3346
  // SPDX-License-Identifier: MIT
3347
+ /**
3348
+ * Whether the agent should show a "typing" indicator — it is loading and has
3349
+ * not yet started streaming the assistant's reply.
3350
+ *
3351
+ * @param agent The agent to inspect.
3352
+ * @returns `true` while the agent is awaiting a response but no assistant text
3353
+ * has streamed yet; `false` once tokens arrive or the agent is idle.
3354
+ * @example
3355
+ * ```ts
3356
+ * \@if (isTyping(agent)) { <chat-typing-indicator [agent]="agent" /> }
3357
+ * ```
3358
+ */
3151
3359
  function isTyping(agent) {
3152
3360
  if (!agent.isLoading())
3153
3361
  return false;
@@ -4056,6 +4264,19 @@ const CHAT_ERROR_STYLES = `
4056
4264
 
4057
4265
  // libs/chat/src/lib/primitives/chat-error/chat-error.component.ts
4058
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
+ */
4059
4280
  function extractErrorMessage(error) {
4060
4281
  if (!error)
4061
4282
  return null;
@@ -4118,6 +4339,18 @@ const CHAT_INTERRUPT_STYLES = `
4118
4339
 
4119
4340
  // libs/chat/src/lib/primitives/chat-interrupt/chat-interrupt.component.ts
4120
4341
  // SPDX-License-Identifier: MIT
4342
+ /**
4343
+ * Read the agent's current human-in-the-loop interrupt, if any.
4344
+ *
4345
+ * @param agent The agent to inspect.
4346
+ * @returns The pending {@link AgentInterrupt}, or `undefined` when the agent is
4347
+ * not currently waiting on an interrupt.
4348
+ * @example
4349
+ * ```ts
4350
+ * const interrupt = getInterrupt(agent);
4351
+ * if (interrupt) agent.resume('approved');
4352
+ * ```
4353
+ */
4121
4354
  function getInterrupt(agent) {
4122
4355
  return agent.interrupt?.();
4123
4356
  }
@@ -4298,6 +4531,99 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
4298
4531
  `, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:block}.tcc__name{font-family:var(--ngaf-chat-font-mono);font-size:var(--ngaf-chat-font-size-sm, 13px);color:var(--ngaf-chat-text-muted);font-weight:400;padding-left:2px}.tcc__pill{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:9999px;background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text-muted);font-size:10px;font-weight:500;margin-left:6px;line-height:1.4}.tcc__pill svg{width:10px;height:10px}.tcc__pill[data-status=running] svg{animation:tcc-spin .8s linear infinite}@keyframes tcc-spin{to{transform:rotate(360deg)}}.tcc__section{padding:8px 0}.tcc__section+.tcc__section{border-top:1px solid var(--ngaf-chat-separator)}.tcc__section-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ngaf-chat-text-muted);margin:0 0 4px}.tcc__section-body{font-family:var(--ngaf-chat-font-mono);font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text);white-space:pre-wrap;overflow-x:auto;margin:0}\n"] }]
4299
4532
  }], propDecorators: { toolCall: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolCall", required: true }] }], defaultCollapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultCollapsed", required: false }] }] } });
4300
4533
 
4534
+ // libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts
4535
+ // SPDX-License-Identifier: MIT
4536
+ /**
4537
+ * Returns a CSS style string for a subagent's status badge.
4538
+ * Kept exported for backward compatibility with existing consumers; the
4539
+ * preferred way to style status visually is via the `data-status` attribute
4540
+ * + CSS selectors (see component styles below).
4541
+ */
4542
+ function statusColor(status) {
4543
+ switch (status) {
4544
+ case 'pending': return 'background: var(--ngaf-chat-surface-alt); color: var(--ngaf-chat-text-muted);';
4545
+ case 'running': return 'background: var(--ngaf-chat-warning-bg); color: var(--ngaf-chat-warning-text);';
4546
+ case 'complete': return 'color: var(--ngaf-chat-success);';
4547
+ case 'error': return 'background: var(--ngaf-chat-error-bg); color: var(--ngaf-chat-error-text);';
4548
+ }
4549
+ }
4550
+ function statusToTraceState(s) {
4551
+ switch (s) {
4552
+ case 'pending': return 'pending';
4553
+ case 'running': return 'running';
4554
+ case 'complete': return 'done';
4555
+ case 'error': return 'error';
4556
+ }
4557
+ }
4558
+ class ChatSubagentCardComponent {
4559
+ subagent = input.required(...(ngDevMode ? [{ debugName: "subagent" }] : []));
4560
+ state = computed(() => statusToTraceState(this.subagent().status()), ...(ngDevMode ? [{ debugName: "state" }] : []));
4561
+ textOf(m) {
4562
+ const c = m.content;
4563
+ return typeof c === 'string' ? c : '';
4564
+ }
4565
+ toolCallsFor(m) {
4566
+ const ids = m.toolCallIds ?? [];
4567
+ if (ids.length === 0)
4568
+ return [];
4569
+ const all = this.subagent().toolCalls?.() ?? [];
4570
+ return ids.map((id) => all.find((tc) => tc.id === id)).filter((tc) => !!tc);
4571
+ }
4572
+ toToolCallInfo(tc) {
4573
+ return { id: tc.id, name: tc.name, args: tc.args, result: tc.result, status: tc.status };
4574
+ }
4575
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSubagentCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4576
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatSubagentCardComponent, isStandalone: true, selector: "chat-subagent-card", inputs: { subagent: { classPropertyName: "subagent", publicName: "subagent", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
4577
+ <chat-trace [state]="state()">
4578
+ <span traceLabel>
4579
+ <span class="sac__name">{{ subagent().name ?? 'Subagent' }}</span>
4580
+ <span class="sac__id">{{ subagent().toolCallId }}</span>
4581
+ <span class="sac__pill" [attr.data-status]="subagent().status()">{{ subagent().status() }}</span>
4582
+ </span>
4583
+ <div class="sac__count" traceMeta>{{ subagent().messages().length }} message(s)</div>
4584
+ @for (m of subagent().messages(); track m.id) {
4585
+ <div class="sac__msg" [attr.data-role]="m.role">
4586
+ @if (m.reasoning) {
4587
+ <div class="sac__reasoning">{{ m.reasoning }}</div>
4588
+ }
4589
+ @if (textOf(m); as t) {
4590
+ <chat-streaming-md [content]="t" />
4591
+ }
4592
+ @for (tc of toolCallsFor(m); track tc.id) {
4593
+ <chat-tool-call-card [toolCall]="toToolCallInfo(tc)" />
4594
+ }
4595
+ </div>
4596
+ }
4597
+ </chat-trace>
4598
+ `, isInline: true, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:block}.sac__name{color:var(--ngaf-chat-text);font-weight:500;font-size:var(--ngaf-chat-font-size-sm)}.sac__id{font-family:var(--ngaf-chat-font-mono);font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted);margin-left:4px}.sac__pill{padding:1px 8px;border-radius:9999px;font-size:11px;font-weight:500;margin-left:4px}.sac__pill[data-status=pending]{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text-muted)}.sac__pill[data-status=running]{background:var(--ngaf-chat-warning-bg);color:var(--ngaf-chat-warning-text)}.sac__pill[data-status=complete]{color:var(--ngaf-chat-success)}.sac__pill[data-status=error]{background:var(--ngaf-chat-error-bg);color:var(--ngaf-chat-error-text)}.sac__count{font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted)}.sac__msg{padding:6px 0}.sac__msg+.sac__msg{border-top:1px solid var(--ngaf-chat-separator)}.sac__reasoning{font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted);font-style:italic;margin-bottom:4px}\n"], dependencies: [{ kind: "component", type: ChatTraceComponent, selector: "chat-trace", inputs: ["state", "defaultExpanded"] }, { kind: "component", type: ChatToolCallCardComponent, selector: "chat-tool-call-card", inputs: ["toolCall", "defaultCollapsed"] }, { kind: "component", type: ChatStreamingMdComponent, selector: "chat-streaming-md", inputs: ["content", "streaming", "viewRegistry"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4599
+ }
4600
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSubagentCardComponent, decorators: [{
4601
+ type: Component,
4602
+ args: [{ selector: 'chat-subagent-card', standalone: true, imports: [ChatTraceComponent, ChatToolCallCardComponent, ChatStreamingMdComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
4603
+ <chat-trace [state]="state()">
4604
+ <span traceLabel>
4605
+ <span class="sac__name">{{ subagent().name ?? 'Subagent' }}</span>
4606
+ <span class="sac__id">{{ subagent().toolCallId }}</span>
4607
+ <span class="sac__pill" [attr.data-status]="subagent().status()">{{ subagent().status() }}</span>
4608
+ </span>
4609
+ <div class="sac__count" traceMeta>{{ subagent().messages().length }} message(s)</div>
4610
+ @for (m of subagent().messages(); track m.id) {
4611
+ <div class="sac__msg" [attr.data-role]="m.role">
4612
+ @if (m.reasoning) {
4613
+ <div class="sac__reasoning">{{ m.reasoning }}</div>
4614
+ }
4615
+ @if (textOf(m); as t) {
4616
+ <chat-streaming-md [content]="t" />
4617
+ }
4618
+ @for (tc of toolCallsFor(m); track tc.id) {
4619
+ <chat-tool-call-card [toolCall]="toToolCallInfo(tc)" />
4620
+ }
4621
+ </div>
4622
+ }
4623
+ </chat-trace>
4624
+ `, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:block}.sac__name{color:var(--ngaf-chat-text);font-weight:500;font-size:var(--ngaf-chat-font-size-sm)}.sac__id{font-family:var(--ngaf-chat-font-mono);font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted);margin-left:4px}.sac__pill{padding:1px 8px;border-radius:9999px;font-size:11px;font-weight:500;margin-left:4px}.sac__pill[data-status=pending]{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text-muted)}.sac__pill[data-status=running]{background:var(--ngaf-chat-warning-bg);color:var(--ngaf-chat-warning-text)}.sac__pill[data-status=complete]{color:var(--ngaf-chat-success)}.sac__pill[data-status=error]{background:var(--ngaf-chat-error-bg);color:var(--ngaf-chat-error-text)}.sac__count{font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted)}.sac__msg{padding:6px 0}.sac__msg+.sac__msg{border-top:1px solid var(--ngaf-chat-separator)}.sac__reasoning{font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted);font-style:italic;margin-bottom:4px}\n"] }]
4625
+ }], propDecorators: { subagent: [{ type: i0.Input, args: [{ isSignal: true, alias: "subagent", required: true }] }] } });
4626
+
4301
4627
  // libs/chat/src/lib/primitives/chat-tool-calls/chat-tool-call-template.directive.ts
4302
4628
  // SPDX-License-Identifier: MIT
4303
4629
  /**
@@ -4426,14 +4752,23 @@ class ChatToolCallsComponent {
4426
4752
  groups = computed(() => {
4427
4753
  const excludeSet = new Set(this.excludeToolNames());
4428
4754
  const calls = this.toolCalls().filter(tc => !excludeSet.has(tc.name));
4755
+ const subs = this.agent().subagents?.() ?? new Map();
4429
4756
  const groupingMode = this.grouping();
4430
4757
  const registry = this.templateRegistry();
4431
4758
  const wildcard = registry.get('*');
4432
4759
  const out = [];
4433
4760
  for (const tc of calls) {
4761
+ // A tool call that spawned a subagent renders as a standalone subagent
4762
+ // card anchored to that call. It never groups with adjacent calls, on
4763
+ // either side: it is its own group and carries a `subagent`, so the next
4764
+ // call can't append to it (a subagent group is never a group target).
4765
+ if (subs.has(tc.id)) {
4766
+ out.push({ name: tc.name, calls: [tc], subagent: subs.get(tc.id) });
4767
+ continue;
4768
+ }
4434
4769
  const tpl = registry.get(tc.name) ?? wildcard;
4435
4770
  const last = out[out.length - 1];
4436
- const sameName = last && last.name === tc.name;
4771
+ const sameName = last && !last.subagent && last.name === tc.name;
4437
4772
  const canGroup = groupingMode === 'auto' && sameName;
4438
4773
  if (canGroup) {
4439
4774
  last.calls.push(tc);
@@ -4467,7 +4802,9 @@ class ChatToolCallsComponent {
4467
4802
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatToolCallsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4468
4803
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatToolCallsComponent, isStandalone: true, selector: "chat-tool-calls", inputs: { agent: { classPropertyName: "agent", publicName: "agent", isSignal: true, isRequired: true, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null }, grouping: { classPropertyName: "grouping", publicName: "grouping", isSignal: true, isRequired: false, transformFunction: null }, groupSummary: { classPropertyName: "groupSummary", publicName: "groupSummary", isSignal: true, isRequired: false, transformFunction: null }, excludeToolNames: { classPropertyName: "excludeToolNames", publicName: "excludeToolNames", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "templates", predicate: ChatToolCallTemplateDirective, isSignal: true }], ngImport: i0, template: `
4469
4804
  @for (group of groups(); track $index) {
4470
- @if (group.calls.length > 1 && !group.templateRef) {
4805
+ @if (group.subagent) {
4806
+ <chat-subagent-card [subagent]="group.subagent" />
4807
+ } @else if (group.calls.length > 1 && !group.templateRef) {
4471
4808
  <!-- Default grouped strip -->
4472
4809
  @let expanded = expandedGroups().has($index);
4473
4810
  <div class="ctc__group" [attr.data-group]="true" [attr.data-expanded]="expanded">
@@ -4498,13 +4835,15 @@ class ChatToolCallsComponent {
4498
4835
  }
4499
4836
  }
4500
4837
  }
4501
- `, isInline: true, styles: [":host{display:block;margin-bottom:20px}.ctc__group{border:1px solid var(--ngaf-chat-separator);border-radius:var(--ngaf-chat-radius-card);margin:0 0 4px}.ctc__group-header{display:flex;align-items:center;gap:.5rem;width:100%;padding:8px 12px;background:transparent;border:0;font:inherit;color:var(--ngaf-chat-text);cursor:pointer;text-align:left}.ctc__group-chevron{width:10px;height:10px;transition:transform .12s ease}.ctc__group[data-expanded=true] .ctc__group-chevron{transform:rotate(90deg)}.ctc__group-body{padding:0 12px 8px;border-top:1px solid var(--ngaf-chat-separator)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: ChatToolCallCardComponent, selector: "chat-tool-call-card", inputs: ["toolCall", "defaultCollapsed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4838
+ `, isInline: true, styles: [":host{display:block;margin-bottom:20px}.ctc__group{border:1px solid var(--ngaf-chat-separator);border-radius:var(--ngaf-chat-radius-card);margin:0 0 4px}.ctc__group-header{display:flex;align-items:center;gap:.5rem;width:100%;padding:8px 12px;background:transparent;border:0;font:inherit;color:var(--ngaf-chat-text);cursor:pointer;text-align:left}.ctc__group-chevron{width:10px;height:10px;transition:transform .12s ease}.ctc__group[data-expanded=true] .ctc__group-chevron{transform:rotate(90deg)}.ctc__group-body{padding:0 12px 8px;border-top:1px solid var(--ngaf-chat-separator)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: ChatToolCallCardComponent, selector: "chat-tool-call-card", inputs: ["toolCall", "defaultCollapsed"] }, { kind: "component", type: ChatSubagentCardComponent, selector: "chat-subagent-card", inputs: ["subagent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4502
4839
  }
4503
4840
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatToolCallsComponent, decorators: [{
4504
4841
  type: Component,
4505
- args: [{ selector: 'chat-tool-calls', standalone: true, imports: [NgTemplateOutlet, ChatToolCallCardComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
4842
+ args: [{ selector: 'chat-tool-calls', standalone: true, imports: [NgTemplateOutlet, ChatToolCallCardComponent, ChatSubagentCardComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
4506
4843
  @for (group of groups(); track $index) {
4507
- @if (group.calls.length > 1 && !group.templateRef) {
4844
+ @if (group.subagent) {
4845
+ <chat-subagent-card [subagent]="group.subagent" />
4846
+ } @else if (group.calls.length > 1 && !group.templateRef) {
4508
4847
  <!-- Default grouped strip -->
4509
4848
  @let expanded = expandedGroups().has($index);
4510
4849
  <div class="ctc__group" [attr.data-group]="true" [attr.data-expanded]="expanded">
@@ -4778,99 +5117,6 @@ function isRecord$2(v) {
4778
5117
  return typeof v === 'object' && v !== null && !Array.isArray(v);
4779
5118
  }
4780
5119
 
4781
- // libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts
4782
- // SPDX-License-Identifier: MIT
4783
- /**
4784
- * Returns a CSS style string for a subagent's status badge.
4785
- * Kept exported for backward compatibility with existing consumers; the
4786
- * preferred way to style status visually is via the `data-status` attribute
4787
- * + CSS selectors (see component styles below).
4788
- */
4789
- function statusColor(status) {
4790
- switch (status) {
4791
- case 'pending': return 'background: var(--ngaf-chat-surface-alt); color: var(--ngaf-chat-text-muted);';
4792
- case 'running': return 'background: var(--ngaf-chat-warning-bg); color: var(--ngaf-chat-warning-text);';
4793
- case 'complete': return 'color: var(--ngaf-chat-success);';
4794
- case 'error': return 'background: var(--ngaf-chat-error-bg); color: var(--ngaf-chat-error-text);';
4795
- }
4796
- }
4797
- function statusToTraceState(s) {
4798
- switch (s) {
4799
- case 'pending': return 'pending';
4800
- case 'running': return 'running';
4801
- case 'complete': return 'done';
4802
- case 'error': return 'error';
4803
- }
4804
- }
4805
- class ChatSubagentCardComponent {
4806
- subagent = input.required(...(ngDevMode ? [{ debugName: "subagent" }] : []));
4807
- state = computed(() => statusToTraceState(this.subagent().status()), ...(ngDevMode ? [{ debugName: "state" }] : []));
4808
- textOf(m) {
4809
- const c = m.content;
4810
- return typeof c === 'string' ? c : '';
4811
- }
4812
- toolCallsFor(m) {
4813
- const ids = m.toolCallIds ?? [];
4814
- if (ids.length === 0)
4815
- return [];
4816
- const all = this.subagent().toolCalls?.() ?? [];
4817
- return ids.map((id) => all.find((tc) => tc.id === id)).filter((tc) => !!tc);
4818
- }
4819
- toToolCallInfo(tc) {
4820
- return { id: tc.id, name: tc.name, args: tc.args, result: tc.result, status: tc.status };
4821
- }
4822
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSubagentCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4823
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatSubagentCardComponent, isStandalone: true, selector: "chat-subagent-card", inputs: { subagent: { classPropertyName: "subagent", publicName: "subagent", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
4824
- <chat-trace [state]="state()">
4825
- <span traceLabel>
4826
- <span class="sac__name">{{ subagent().name ?? 'Subagent' }}</span>
4827
- <span class="sac__id">{{ subagent().toolCallId }}</span>
4828
- <span class="sac__pill" [attr.data-status]="subagent().status()">{{ subagent().status() }}</span>
4829
- </span>
4830
- <div class="sac__count">{{ subagent().messages().length }} message(s)</div>
4831
- @for (m of subagent().messages(); track m.id) {
4832
- <div class="sac__msg" [attr.data-role]="m.role">
4833
- @if (m.reasoning) {
4834
- <div class="sac__reasoning">{{ m.reasoning }}</div>
4835
- }
4836
- @if (textOf(m); as t) {
4837
- <chat-streaming-md [content]="t" />
4838
- }
4839
- @for (tc of toolCallsFor(m); track tc.id) {
4840
- <chat-tool-call-card [toolCall]="toToolCallInfo(tc)" />
4841
- }
4842
- </div>
4843
- }
4844
- </chat-trace>
4845
- `, isInline: true, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:block}.sac__name{color:var(--ngaf-chat-text);font-weight:500;font-size:var(--ngaf-chat-font-size-sm)}.sac__id{font-family:var(--ngaf-chat-font-mono);font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted);margin-left:4px}.sac__pill{padding:1px 8px;border-radius:9999px;font-size:11px;font-weight:500;margin-left:4px}.sac__pill[data-status=pending]{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text-muted)}.sac__pill[data-status=running]{background:var(--ngaf-chat-warning-bg);color:var(--ngaf-chat-warning-text)}.sac__pill[data-status=complete]{color:var(--ngaf-chat-success)}.sac__pill[data-status=error]{background:var(--ngaf-chat-error-bg);color:var(--ngaf-chat-error-text)}.sac__count{font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted)}.sac__msg{padding:6px 0}.sac__msg+.sac__msg{border-top:1px solid var(--ngaf-chat-separator)}.sac__reasoning{font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted);font-style:italic;margin-bottom:4px}\n"], dependencies: [{ kind: "component", type: ChatTraceComponent, selector: "chat-trace", inputs: ["state", "defaultExpanded"] }, { kind: "component", type: ChatToolCallCardComponent, selector: "chat-tool-call-card", inputs: ["toolCall", "defaultCollapsed"] }, { kind: "component", type: ChatStreamingMdComponent, selector: "chat-streaming-md", inputs: ["content", "streaming", "viewRegistry"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4846
- }
4847
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSubagentCardComponent, decorators: [{
4848
- type: Component,
4849
- args: [{ selector: 'chat-subagent-card', standalone: true, imports: [ChatTraceComponent, ChatToolCallCardComponent, ChatStreamingMdComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
4850
- <chat-trace [state]="state()">
4851
- <span traceLabel>
4852
- <span class="sac__name">{{ subagent().name ?? 'Subagent' }}</span>
4853
- <span class="sac__id">{{ subagent().toolCallId }}</span>
4854
- <span class="sac__pill" [attr.data-status]="subagent().status()">{{ subagent().status() }}</span>
4855
- </span>
4856
- <div class="sac__count">{{ subagent().messages().length }} message(s)</div>
4857
- @for (m of subagent().messages(); track m.id) {
4858
- <div class="sac__msg" [attr.data-role]="m.role">
4859
- @if (m.reasoning) {
4860
- <div class="sac__reasoning">{{ m.reasoning }}</div>
4861
- }
4862
- @if (textOf(m); as t) {
4863
- <chat-streaming-md [content]="t" />
4864
- }
4865
- @for (tc of toolCallsFor(m); track tc.id) {
4866
- <chat-tool-call-card [toolCall]="toToolCallInfo(tc)" />
4867
- }
4868
- </div>
4869
- }
4870
- </chat-trace>
4871
- `, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:block}.sac__name{color:var(--ngaf-chat-text);font-weight:500;font-size:var(--ngaf-chat-font-size-sm)}.sac__id{font-family:var(--ngaf-chat-font-mono);font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted);margin-left:4px}.sac__pill{padding:1px 8px;border-radius:9999px;font-size:11px;font-weight:500;margin-left:4px}.sac__pill[data-status=pending]{background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text-muted)}.sac__pill[data-status=running]{background:var(--ngaf-chat-warning-bg);color:var(--ngaf-chat-warning-text)}.sac__pill[data-status=complete]{color:var(--ngaf-chat-success)}.sac__pill[data-status=error]{background:var(--ngaf-chat-error-bg);color:var(--ngaf-chat-error-text)}.sac__count{font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted)}.sac__msg{padding:6px 0}.sac__msg+.sac__msg{border-top:1px solid var(--ngaf-chat-separator)}.sac__reasoning{font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted);font-style:italic;margin-bottom:4px}\n"] }]
4872
- }], propDecorators: { subagent: [{ type: i0.Input, args: [{ isSignal: true, alias: "subagent", required: true }] }] } });
4873
-
4874
5120
  // libs/chat/src/lib/primitives/chat-subagents/chat-subagents.component.ts
4875
5121
  // SPDX-License-Identifier: MIT
4876
5122
  function activeSubagentsFromAgent(agent) {
@@ -7441,6 +7687,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
7441
7687
  }], propDecorators: { surface: [{ type: i0.Input, args: [{ isSignal: true, alias: "surface", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], catalog: [{ type: i0.Input, args: [{ isSignal: true, alias: "catalog", required: true }] }], handlers: [{ type: i0.Input, args: [{ isSignal: true, alias: "handlers", required: false }] }], surfaceFallback: [{ type: i0.Input, args: [{ isSignal: true, alias: "surfaceFallback", required: false }] }], events: [{ type: i0.Output, args: ["events"] }], action: [{ type: i0.Output, args: ["action"] }] } });
7442
7688
 
7443
7689
  // SPDX-License-Identifier: MIT
7690
+ /**
7691
+ * Create a {@link ParseTreeStore} — feeds streamed JSON chunks through a
7692
+ * partial-JSON parser and exposes the progressively-materialized spec and
7693
+ * per-element accumulation state as signals, so a generative-UI surface can
7694
+ * render while the spec is still arriving.
7695
+ *
7696
+ * @param parser The partial-JSON parser used to incrementally materialize chunks.
7697
+ * @returns A {@link ParseTreeStore}; call `push(chunk)` as bytes stream in.
7698
+ * @example
7699
+ * ```ts
7700
+ * const store = createParseTreeStore(parser);
7701
+ * store.push('{"type":"Car');
7702
+ * store.spec(); // best-effort Spec | null
7703
+ * ```
7704
+ */
7444
7705
  function createParseTreeStore(parser) {
7445
7706
  const specSignal = signal(null, ...(ngDevMode ? [{ debugName: "specSignal" }] : []));
7446
7707
  const elementStatesSignal = signal(new Map(), ...(ngDevMode ? [{ debugName: "elementStatesSignal" }] : []));
@@ -7581,6 +7842,19 @@ function resolveProps(value, dataModel) {
7581
7842
  }
7582
7843
  return value;
7583
7844
  }
7845
+ /**
7846
+ * Create an {@link A2uiSurfaceStore} — the per-conversation store that buffers
7847
+ * streamed A2UI surface updates, tracks each surface's data model + lifecycle
7848
+ * state, and exposes them as signals for rendering. One store backs a chat
7849
+ * thread's A2UI surfaces.
7850
+ *
7851
+ * @returns A fresh, empty {@link A2uiSurfaceStore}.
7852
+ * @example
7853
+ * ```ts
7854
+ * const store = createA2uiSurfaceStore();
7855
+ * const surfaces = store.surfaces; // Signal<Map<string, A2uiSurface>>
7856
+ * ```
7857
+ */
7584
7858
  function createA2uiSurfaceStore() {
7585
7859
  const surfacesSignal = signal(new Map(), ...(ngDevMode ? [{ debugName: "surfacesSignal" }] : []));
7586
7860
  const surfaceStatesSignal = signal(new Map(), ...(ngDevMode ? [{ debugName: "surfaceStatesSignal" }] : []));
@@ -7802,6 +8076,19 @@ function trace(...args) {
7802
8076
 
7803
8077
  // SPDX-License-Identifier: MIT
7804
8078
  const A2UI_PREFIX = '---a2ui_JSON---';
8079
+ /**
8080
+ * Create a {@link ContentClassifier} — the streaming accumulator that inspects
8081
+ * an assistant message's content as it arrives and classifies it (markdown vs a
8082
+ * generative-UI/A2UI spec), exposing the parsed result and per-element state as
8083
+ * signals so the renderer can switch modes mid-stream.
8084
+ *
8085
+ * @returns A fresh {@link ContentClassifier}; call `dispose()` when done.
8086
+ * @example
8087
+ * ```ts
8088
+ * const cc = createContentClassifier();
8089
+ * effect(() => console.log(cc.type())); // 'pending' | 'markdown' | 'spec'
8090
+ * ```
8091
+ */
7805
8092
  function createContentClassifier() {
7806
8093
  const typeSignal = signal('pending', ...(ngDevMode ? [{ debugName: "typeSignal" }] : []));
7807
8094
  const markdownSignal = signal('', ...(ngDevMode ? [{ debugName: "markdownSignal" }] : []));
@@ -8673,6 +8960,62 @@ class ChatComponent {
8673
8960
  const text = typeof message.content === 'string' ? message.content : '';
8674
8961
  return text.length === 0;
8675
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
+ }
8676
9019
  classifiers = new Map();
8677
9020
  destroyRef = inject(DestroyRef);
8678
9021
  injector = inject(Injector);
@@ -9232,11 +9575,19 @@ class ChatComponent {
9232
9575
  [streaming]="agent().isLoading() && i === agent().messages().length - 1"
9233
9576
  [current]="i === agent().messages().length - 1"
9234
9577
  >
9235
- @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);
9236
9586
  <chat-reasoning
9237
- [content]="message.reasoning"
9238
- [isStreaming]="isReasoningStreaming(message, i)"
9239
- [durationMs]="message.reasoningDurationMs"
9587
+ [content]="run.content"
9588
+ [isStreaming]="run.streaming"
9589
+ [durationMs]="run.durationMs"
9590
+ [label]="run.label"
9240
9591
  />
9241
9592
  }
9242
9593
  <chat-tool-calls [agent]="agent()" [message]="message" [excludeToolNames]="excludedToolNames()">
@@ -9252,7 +9603,6 @@ class ChatComponent {
9252
9603
  [handlers]="handlers()"
9253
9604
  (events)="onClientToolEvent($event)"
9254
9605
  />
9255
- <chat-subagents [agent]="agent()" />
9256
9606
  @if (classified.markdown(); as md) {
9257
9607
  <chat-streaming-md [content]="md" [streaming]="agent().isLoading() && i === agent().messages().length - 1" />
9258
9608
  }
@@ -9284,14 +9634,21 @@ class ChatComponent {
9284
9634
  />
9285
9635
  }
9286
9636
  }
9287
- <chat-message-actions
9288
- chatMessageControls
9289
- [content]="content"
9290
- [disabled]="agent().isLoading()"
9291
- (regenerate)="onRegenerate(i)"
9292
- (rate)="onRate(message, $event)"
9293
- (contentCopied)="onCopy(message, $event)"
9294
- />
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
+ }
9295
9652
  </chat-message>
9296
9653
  </ng-template>
9297
9654
 
@@ -9340,7 +9697,7 @@ class ChatComponent {
9340
9697
  </div>
9341
9698
  </div>
9342
9699
  }
9343
- `, isInline: true, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:flex;flex-direction:column;flex:1 1 auto;height:100%;min-height:0;max-height:100%;overflow:hidden;background:var(--ngaf-chat-bg)}:host>chat-welcome{display:flex;flex:1 1 auto;width:100%}.chat-shell{display:flex;flex:1;min-height:0;overflow:hidden}.chat-shell__sidebar{width:240px;flex-shrink:0;border-right:1px solid var(--ngaf-chat-separator);background:var(--ngaf-chat-surface-alt);overflow-y:auto;display:none}@media(min-width:768px){.chat-shell__sidebar{display:block}}.chat-shell__main{flex:1;min-width:0;display:flex;flex-direction:column;min-height:0}.chat-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:60px 20px;color:var(--ngaf-chat-text-muted);text-align:center;flex:1;min-height:0}.chat-empty[hidden]{display:none}.chat-empty__title{font-size:1.125rem;font-weight:500;color:var(--ngaf-chat-text);margin:0}.chat-empty__sub{margin:0;font-size:var(--ngaf-chat-font-size-sm)}.chat-scroll{flex:1;min-height:0;overflow-y:auto;padding-top:var(--ngaf-chat-edge-pad)}.chat-scroll::-webkit-scrollbar{width:6px}.chat-scroll::-webkit-scrollbar-thumb{background:var(--ngaf-chat-separator);border-radius:10px}[chatFooter]{padding-bottom:var(--ngaf-chat-edge-pad)}.chat-footer-wrap{position:relative}\n"], dependencies: [{ kind: "component", type: ChatWindowComponent, selector: "chat-window" }, { kind: "component", type: ChatMessageListComponent, selector: "chat-message-list", inputs: ["agent"] }, { kind: "directive", type: MessageTemplateDirective, selector: "ng-template[chatMessageTemplate]", inputs: ["chatMessageTemplate"] }, { kind: "component", type: ChatMessageComponent, selector: "chat-message", inputs: ["role", "current", "streaming", "prevRole", "message"] }, { kind: "component", type: ChatInputComponent, selector: "chat-input", inputs: ["agent", "submitOnEnter", "placeholder", "showStopButton"], outputs: ["submitted", "stopped"] }, { kind: "component", type: ChatTypingIndicatorComponent, selector: "chat-typing-indicator", inputs: ["agent"] }, { kind: "component", type: ChatErrorComponent, selector: "chat-error", inputs: ["agent"] }, { kind: "component", type: ChatThreadListComponent, selector: "chat-thread-list", inputs: ["threads", "activeThreadId", "showNewThreadButton", "actions", "mode", "projects"], outputs: ["threadSelected", "newThreadRequested"] }, { kind: "component", type: ChatGenerativeUiComponent, selector: "chat-generative-ui", inputs: ["spec", "registry", "store", "handlers", "loading"], outputs: ["events"] }, { kind: "component", type: ChatStreamingMdComponent, selector: "chat-streaming-md", inputs: ["content", "streaming", "viewRegistry"] }, { kind: "component", type: ChatToolCallsComponent, selector: "chat-tool-calls", inputs: ["agent", "message", "grouping", "groupSummary", "excludeToolNames"] }, { kind: "component", type: ChatToolViewsComponent, selector: "chat-tool-views", inputs: ["agent", "message", "views", "store", "handlers"], outputs: ["events"] }, { kind: "component", type: ChatSubagentsComponent, selector: "chat-subagents", inputs: ["agent"] }, { kind: "component", type: A2uiSurfaceComponent, selector: "a2ui-surface", inputs: ["surface", "state", "catalog", "handlers", "surfaceFallback"], outputs: ["events", "action"] }, { kind: "component", type: ChatMessageActionsComponent, selector: "chat-message-actions", inputs: ["content", "disabled"], outputs: ["regenerate", "rate", "contentCopied"] }, { kind: "component", type: ChatWelcomeComponent, selector: "chat-welcome" }, { kind: "component", type: ChatSelectComponent, selector: "chat-select", inputs: ["options", "value", "placeholder", "disabled", "menuLabel"], outputs: ["valueChange"] }, { kind: "component", type: ChatReasoningComponent, selector: "chat-reasoning", inputs: ["content", "isStreaming", "durationMs", "label", "defaultExpanded"] }, { kind: "component", type: ChatScrollBubbleComponent, selector: "chat-scroll-bubble", inputs: ["mode"], outputs: ["clicked"] }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9700
+ `, isInline: true, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:flex;flex-direction:column;flex:1 1 auto;height:100%;min-height:0;max-height:100%;overflow:hidden;background:var(--ngaf-chat-bg)}:host>chat-welcome{display:flex;flex:1 1 auto;width:100%}.chat-shell{display:flex;flex:1;min-height:0;overflow:hidden}.chat-shell__sidebar{width:240px;flex-shrink:0;border-right:1px solid var(--ngaf-chat-separator);background:var(--ngaf-chat-surface-alt);overflow-y:auto;display:none}@media(min-width:768px){.chat-shell__sidebar{display:block}}.chat-shell__main{flex:1;min-width:0;display:flex;flex-direction:column;min-height:0}.chat-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:60px 20px;color:var(--ngaf-chat-text-muted);text-align:center;flex:1;min-height:0}.chat-empty[hidden]{display:none}.chat-empty__title{font-size:1.125rem;font-weight:500;color:var(--ngaf-chat-text);margin:0}.chat-empty__sub{margin:0;font-size:var(--ngaf-chat-font-size-sm)}.chat-scroll{flex:1;min-height:0;overflow-y:auto;padding-top:var(--ngaf-chat-edge-pad)}.chat-scroll::-webkit-scrollbar{width:6px}.chat-scroll::-webkit-scrollbar-thumb{background:var(--ngaf-chat-separator);border-radius:10px}[chatFooter]{padding-bottom:var(--ngaf-chat-edge-pad)}.chat-footer-wrap{position:relative}\n"], dependencies: [{ kind: "component", type: ChatWindowComponent, selector: "chat-window" }, { kind: "component", type: ChatMessageListComponent, selector: "chat-message-list", inputs: ["agent"] }, { kind: "directive", type: MessageTemplateDirective, selector: "ng-template[chatMessageTemplate]", inputs: ["chatMessageTemplate"] }, { kind: "component", type: ChatMessageComponent, selector: "chat-message", inputs: ["role", "current", "streaming", "prevRole", "message"] }, { kind: "component", type: ChatInputComponent, selector: "chat-input", inputs: ["agent", "submitOnEnter", "placeholder", "showStopButton"], outputs: ["submitted", "stopped"] }, { kind: "component", type: ChatTypingIndicatorComponent, selector: "chat-typing-indicator", inputs: ["agent"] }, { kind: "component", type: ChatErrorComponent, selector: "chat-error", inputs: ["agent"] }, { kind: "component", type: ChatThreadListComponent, selector: "chat-thread-list", inputs: ["threads", "activeThreadId", "showNewThreadButton", "actions", "mode", "projects"], outputs: ["threadSelected", "newThreadRequested"] }, { kind: "component", type: ChatGenerativeUiComponent, selector: "chat-generative-ui", inputs: ["spec", "registry", "store", "handlers", "loading"], outputs: ["events"] }, { kind: "component", type: ChatStreamingMdComponent, selector: "chat-streaming-md", inputs: ["content", "streaming", "viewRegistry"] }, { kind: "component", type: ChatToolCallsComponent, selector: "chat-tool-calls", inputs: ["agent", "message", "grouping", "groupSummary", "excludeToolNames"] }, { kind: "component", type: ChatToolViewsComponent, selector: "chat-tool-views", inputs: ["agent", "message", "views", "store", "handlers"], outputs: ["events"] }, { kind: "component", type: A2uiSurfaceComponent, selector: "a2ui-surface", inputs: ["surface", "state", "catalog", "handlers", "surfaceFallback"], outputs: ["events", "action"] }, { kind: "component", type: ChatMessageActionsComponent, selector: "chat-message-actions", inputs: ["content", "disabled"], outputs: ["regenerate", "rate", "contentCopied"] }, { kind: "component", type: ChatWelcomeComponent, selector: "chat-welcome" }, { kind: "component", type: ChatSelectComponent, selector: "chat-select", inputs: ["options", "value", "placeholder", "disabled", "menuLabel"], outputs: ["valueChange"] }, { kind: "component", type: ChatReasoningComponent, selector: "chat-reasoning", inputs: ["content", "isStreaming", "durationMs", "label", "defaultExpanded"] }, { kind: "component", type: ChatScrollBubbleComponent, selector: "chat-scroll-bubble", inputs: ["mode"], outputs: ["clicked"] }, { kind: "pipe", type: KeyValuePipe, name: "keyvalue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9344
9701
  }
9345
9702
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatComponent, decorators: [{
9346
9703
  type: Component,
@@ -9349,7 +9706,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
9349
9706
  ChatWindowComponent, ChatMessageListComponent, MessageTemplateDirective, ChatMessageComponent,
9350
9707
  ChatInputComponent, ChatTypingIndicatorComponent, ChatErrorComponent,
9351
9708
  ChatThreadListComponent, ChatGenerativeUiComponent,
9352
- ChatStreamingMdComponent, ChatToolCallsComponent, ChatToolViewsComponent, ChatSubagentsComponent, A2uiSurfaceComponent,
9709
+ ChatStreamingMdComponent, ChatToolCallsComponent, ChatToolViewsComponent, A2uiSurfaceComponent,
9353
9710
  ChatMessageActionsComponent, ChatWelcomeComponent, ChatSelectComponent, ChatReasoningComponent,
9354
9711
  ChatScrollBubbleComponent,
9355
9712
  ], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
@@ -9402,11 +9759,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
9402
9759
  [streaming]="agent().isLoading() && i === agent().messages().length - 1"
9403
9760
  [current]="i === agent().messages().length - 1"
9404
9761
  >
9405
- @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);
9406
9770
  <chat-reasoning
9407
- [content]="message.reasoning"
9408
- [isStreaming]="isReasoningStreaming(message, i)"
9409
- [durationMs]="message.reasoningDurationMs"
9771
+ [content]="run.content"
9772
+ [isStreaming]="run.streaming"
9773
+ [durationMs]="run.durationMs"
9774
+ [label]="run.label"
9410
9775
  />
9411
9776
  }
9412
9777
  <chat-tool-calls [agent]="agent()" [message]="message" [excludeToolNames]="excludedToolNames()">
@@ -9422,7 +9787,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
9422
9787
  [handlers]="handlers()"
9423
9788
  (events)="onClientToolEvent($event)"
9424
9789
  />
9425
- <chat-subagents [agent]="agent()" />
9426
9790
  @if (classified.markdown(); as md) {
9427
9791
  <chat-streaming-md [content]="md" [streaming]="agent().isLoading() && i === agent().messages().length - 1" />
9428
9792
  }
@@ -9454,14 +9818,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
9454
9818
  />
9455
9819
  }
9456
9820
  }
9457
- <chat-message-actions
9458
- chatMessageControls
9459
- [content]="content"
9460
- [disabled]="agent().isLoading()"
9461
- (regenerate)="onRegenerate(i)"
9462
- (rate)="onRate(message, $event)"
9463
- (contentCopied)="onCopy(message, $event)"
9464
- />
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
+ }
9465
9836
  </chat-message>
9466
9837
  </ng-template>
9467
9838
 
@@ -11179,22 +11550,6 @@ function renderMarkdownToString(content, sanitizer) {
11179
11550
  return plainTextToHtml(content);
11180
11551
  }
11181
11552
 
11182
- // SPDX-License-Identifier: MIT
11183
- /** Chevron down (▼ replacement). 12x12, stroke-based. */
11184
- const ICON_CHEVRON_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4.5L6 7.5L9 4.5"/></svg>`;
11185
- /** Chevron up (▲ replacement). 12x12, stroke-based. */
11186
- const ICON_CHEVRON_UP = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5L6 4.5L9 7.5"/></svg>`;
11187
- /** Gear icon (⚙ replacement). 14x14. */
11188
- const ICON_TOOL = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>`;
11189
- /** Warning triangle (⚠ replacement). 18x18. */
11190
- const ICON_WARNING = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`;
11191
- /** Robot/agent icon (replacement). 14x14. */
11192
- const ICON_AGENT = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="2"/><path d="M12 7v4"/><line x1="8" y1="16" x2="8" y2="16"/><line x1="16" y1="16" x2="16" y2="16"/></svg>`;
11193
- /** Check mark replacement. 12x12. */
11194
- const ICON_CHECK = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 6L5 8.5L9.5 3.5"/></svg>`;
11195
- /** Send arrow (for chat input). 16x16. */
11196
- const ICON_SEND = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 12V4M8 4L4 8M8 4L12 8"/></svg>`;
11197
-
11198
11553
  /** Normalize a catalog entry to the `A2uiViewEntry` shape. Bare
11199
11554
  * `Type<unknown>` entries are wrapped as `{ component }`; entries
11200
11555
  * already in the discriminated shape are returned unchanged. */
@@ -11512,6 +11867,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
11512
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 }] }] } });
11513
11868
 
11514
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
+ }
11515
11883
  class A2uiIconComponent {
11516
11884
  /** v1 canonical prop. */
11517
11885
  name = input(undefined, ...(ngDevMode ? [{ debugName: "name" }] : []));
@@ -11525,24 +11893,32 @@ class A2uiIconComponent {
11525
11893
  childKeys = input([], ...(ngDevMode ? [{ debugName: "childKeys" }] : []));
11526
11894
  spec = input(undefined, ...(ngDevMode ? [{ debugName: "spec" }] : []));
11527
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" }] : []));
11528
11898
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: A2uiIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11529
- 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: `
11530
- <span
11531
- class="a2ui-icon"
11532
- [style.font-size]="size() ? size() + 'px' : '1.125rem'"
11533
- [attr.aria-label]="effectiveName()"
11534
- >{{ effectiveName() }}</span>
11535
- `, 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"] });
11536
11909
  }
11537
11910
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: A2uiIconComponent, decorators: [{
11538
11911
  type: Component,
11539
11912
  args: [{ selector: 'a2ui-icon', standalone: true, template: `
11540
- <span
11541
- class="a2ui-icon"
11542
- [style.font-size]="size() ? size() + 'px' : '1.125rem'"
11543
- [attr.aria-label]="effectiveName()"
11544
- >{{ effectiveName() }}</span>
11545
- `, 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"] }]
11546
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 }] }] } });
11547
11923
 
11548
11924
  // SPDX-License-Identifier: MIT
@@ -12222,6 +12598,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
12222
12598
  }], propDecorators: { url: [{ type: i0.Input, args: [{ isSignal: true, alias: "url", required: false }] }], autoPlay: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoPlay", required: false }] }], controls: [{ type: i0.Input, args: [{ isSignal: true, alias: "controls", 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 }] }] } });
12223
12599
 
12224
12600
  // SPDX-License-Identifier: MIT
12601
+ /**
12602
+ * Build the built-in A2UI component catalog — a {@link ViewRegistry} mapping
12603
+ * the standard A2UI element types (Card, Button, TextField, Image, AudioPlayer,
12604
+ * Video, …) to their Angular renderers. Spread it into `provideViews` (with any
12605
+ * of your own views) so an agent's A2UI surface specs render.
12606
+ *
12607
+ * @returns A {@link ViewRegistry} of the standard A2UI components.
12608
+ * @example
12609
+ * ```ts
12610
+ * providers: [provideViews({ ...a2uiBasicCatalog(), MyWidget: MyWidgetComponent })]
12611
+ * ```
12612
+ */
12225
12613
  function a2uiBasicCatalog() {
12226
12614
  return views({
12227
12615
  AudioPlayer: A2uiAudioPlayerComponent,
@@ -12359,6 +12747,21 @@ function tools(map) {
12359
12747
  }
12360
12748
 
12361
12749
  // SPDX-License-Identifier: MIT
12750
+ /**
12751
+ * Build an in-memory {@link Agent} for tests and stories — no transport, no
12752
+ * network. Every field is a writable signal so a test can drive UI states
12753
+ * (loading, error, interrupts, tool calls, subagents) deterministically.
12754
+ *
12755
+ * @param opts Initial values for the mock's signals; all optional.
12756
+ * @returns A {@link MockAgent} satisfying the full `Agent` contract.
12757
+ * @example
12758
+ * ```ts
12759
+ * const agent = mockAgent({
12760
+ * messages: [{ id: '1', role: 'assistant', content: 'Hi' }],
12761
+ * isLoading: true,
12762
+ * });
12763
+ * ```
12764
+ */
12362
12765
  function mockAgent(opts = {}) {
12363
12766
  const messages = signal(opts.messages ?? [], ...(ngDevMode ? [{ debugName: "messages" }] : []));
12364
12767
  const status = signal(opts.status ?? 'idle', ...(ngDevMode ? [{ debugName: "status" }] : []));
@@ -12410,5 +12813,5 @@ function mockAgent(opts = {}) {
12410
12813
  * Generated bundle index. Do not edit.
12411
12814
  */
12412
12815
 
12413
- 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, CHAT_MARKDOWN_STYLES, 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, ICON_AGENT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CHEVRON_UP, ICON_SEND, ICON_TOOL, ICON_WARNING, 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, createClientToolsCoordinator, 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, surfaceToSpec, 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 };
12414
12817
  //# sourceMappingURL=threadplane-chat.mjs.map