@threadplane/chat 0.0.50 → 0.0.51

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.
@@ -1,17 +1,179 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, inject, TemplateRef, Directive, contentChildren, computed, ChangeDetectionStrategy, Component, signal, Injectable, ContentChild, effect, output, DOCUMENT, InjectionToken, ViewEncapsulation, viewChild, model, contentChild, untracked, ElementRef, DestroyRef, makeEnvironmentProviders, Injector, runInInjectionContext, ViewContainerRef, SecurityContext } from '@angular/core';
2
+ import { InjectionToken, input, inject, TemplateRef, Directive, contentChildren, computed, ChangeDetectionStrategy, Component, signal, Injectable, ContentChild, effect, output, DOCUMENT, ViewEncapsulation, viewChild, model, contentChild, untracked, ElementRef, DestroyRef, makeEnvironmentProviders, Injector, runInInjectionContext, ViewContainerRef, SecurityContext } from '@angular/core';
3
3
  import { NgTemplateOutlet, NgComponentOutlet, KeyValuePipe } from '@angular/common';
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
7
  import { runLicenseCheck, inferNoncommercial, LICENSE_PUBLIC_KEY } from '@threadplane/licensing';
8
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
9
+ import { Router, NavigationEnd } from '@angular/router';
10
+ import { filter, map, startWith } from 'rxjs/operators';
9
11
  import { toJSONSchema } from 'zod/v4';
10
12
  import { resolveDynamic, getByPointer, isPathRef, setByPointer, createA2uiMessageParser } from '@threadplane/a2ui';
11
13
  export { isLiteralBoolean, isLiteralNumber, isLiteralString, isPathRef } from '@threadplane/a2ui';
12
14
  import { materialize as materialize$1, createPartialJsonParser } from '@cacheplane/partial-json';
13
15
  import { fromEvent, EMPTY } from 'rxjs';
14
16
 
17
+ // SPDX-License-Identifier: MIT
18
+ /**
19
+ * Structured, classified failure surfaced on `Agent.error`. Extends `Error`, so
20
+ * existing `.message` / `instanceof Error` reads keep working — but adds a
21
+ * machine-readable {@link AgentErrorKind}, a `retryable` flag, an optional HTTP
22
+ * `status`, and the original `cause`.
23
+ *
24
+ * You rarely construct one yourself; adapters normalize raw failures via
25
+ * {@link toAgentError}. Read it off the agent to render legible, cause-specific UI:
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const err = agent.error(); // AgentError | undefined
30
+ * if (err) {
31
+ * console.warn(err.message); // legible, per-kind copy
32
+ * if (err.kind === 'auth') showApiKeyHelp();
33
+ * if (err.retryable) showRetryButton(); // → agent.retry()
34
+ * }
35
+ * ```
36
+ */
37
+ class AgentError extends Error {
38
+ /** The classified failure type. See {@link AgentErrorKind}. */
39
+ kind;
40
+ /** Whether retrying the same request could plausibly succeed:
41
+ * `connection` | `server` (5xx) | `interrupted` → true; `auth` | `aborted` | non-auth `4xx` → false. */
42
+ retryable;
43
+ /** The HTTP status code when the failure came from an HTTP response. */
44
+ status;
45
+ /** The original raw error this was classified from, preserved for debugging/telemetry. */
46
+ cause;
47
+ constructor(init) {
48
+ super(init.message);
49
+ this.name = 'AgentError';
50
+ this.kind = init.kind;
51
+ this.retryable = init.retryable;
52
+ this.status = init.status;
53
+ this.cause = init.cause;
54
+ }
55
+ }
56
+ /**
57
+ * Default, human-facing copy per {@link AgentErrorKind}. Used as the message when
58
+ * a classified error has no better text. Override by mapping `error.kind` to your
59
+ * own strings in a custom error component.
60
+ */
61
+ const AGENT_ERROR_MESSAGES = {
62
+ connection: "Can't reach the server. Check your connection and try again.",
63
+ auth: 'Authentication failed. Check your API key or credentials.',
64
+ server: 'The server ran into an error. You can try again.',
65
+ interrupted: 'The response was interrupted. Try again.',
66
+ aborted: 'Stopped.',
67
+ };
68
+
69
+ // SPDX-License-Identifier: MIT
70
+ /**
71
+ * Whether `raw` represents an abort (a `DOMException`/`Error` named `AbortError`,
72
+ * or an abort-ish message). Shared by the runtime adapters and {@link toAgentError}
73
+ * so a user-requested stop settles to idle instead of surfacing as an error.
74
+ *
75
+ * @param raw Any thrown/rejected value.
76
+ * @returns `true` if it looks like an abort.
77
+ */
78
+ function isAbortError(raw) {
79
+ return raw instanceof Error && (raw.name === 'AbortError' || /\babort/i.test(raw.message));
80
+ }
81
+ /**
82
+ * Reads a numeric status from `raw.status` or `raw.cause.status` only.
83
+ * No text parsing — structured fields only.
84
+ */
85
+ function structuredStatus(raw) {
86
+ const obj = raw;
87
+ const direct = typeof obj?.status === 'number' ? obj.status : undefined;
88
+ const viaCause = typeof obj?.cause?.status === 'number' ? obj.cause.status : undefined;
89
+ if (direct !== undefined)
90
+ return direct;
91
+ if (viaCause !== undefined)
92
+ return viaCause;
93
+ return undefined;
94
+ }
95
+ function isConnectionError(raw) {
96
+ if (!(raw instanceof Error))
97
+ return false;
98
+ return /failed to fetch|networkerror|econnrefused|enotfound|network request failed|load failed/i.test(`${raw.name} ${raw.message}`);
99
+ }
100
+ /**
101
+ * Extracts an HTTP status code from a message string, but ONLY when the token
102
+ * is unambiguously HTTP-shaped:
103
+ * - `HTTP/502`, `HTTP 404`, `HTTP404`
104
+ * - `status: 503`, `status=503`, `code: 404`
105
+ *
106
+ * Bare 3-digit numbers (e.g. model version strings like "gpt-500") are NOT matched.
107
+ */
108
+ function httpStatusFromMessage(raw) {
109
+ const msg = raw instanceof Error ? raw.message : typeof raw === 'string' ? raw : '';
110
+ if (!msg)
111
+ return undefined;
112
+ // "HTTP 500", "HTTP/500", "HTTP500"
113
+ const httpToken = /\bHTTP[ /]?(\d{3})\b/i.exec(msg);
114
+ if (httpToken)
115
+ return Number(httpToken[1]);
116
+ // "status: 503", "status=503", "status 503" (up to 4 non-digit chars between keyword and digits)
117
+ // Also matches "code: 404", "code=404", etc.
118
+ const prefixed = /\b(?:status|code)\b\D{0,4}(\d{3})\b/i.exec(msg);
119
+ if (prefixed)
120
+ return Number(prefixed[1]);
121
+ return undefined;
122
+ }
123
+ function make(kind, retryable, raw, status, message) {
124
+ return new AgentError({ kind, retryable, status, cause: raw, message: message ?? AGENT_ERROR_MESSAGES[kind] });
125
+ }
126
+ function classifyByStatus(status, raw) {
127
+ if (status === 401 || status === 403)
128
+ return make('auth', false, raw, status);
129
+ if (status >= 500)
130
+ return make('server', true, raw, status);
131
+ if (status >= 400)
132
+ return make('server', false, raw, status, `The request was rejected (HTTP ${status}).`);
133
+ // Stray 2xx/3xx from a status field — treat as unknown transient failure, no status.
134
+ return make('server', true, raw, undefined, 'Something went wrong. You can try again.');
135
+ }
136
+ /**
137
+ * Classify any raw error into a structured {@link AgentError}.
138
+ *
139
+ * Resolution order (first match wins): an existing `AgentError` is returned
140
+ * unchanged (idempotent) → a user abort → a structured `status`/`cause.status`
141
+ * → network/connection markers → an HTTP-shaped status in the message → a
142
+ * `server` + retryable fallback. The original error is always preserved on
143
+ * `cause`. Runtime adapters call this before setting `Agent.error`; custom
144
+ * backends can call it too (or throw an `AgentError` directly).
145
+ *
146
+ * @param raw Any thrown/rejected value — an `Error`, a `{ status }` object, a string, etc.
147
+ * @returns The classified {@link AgentError} (kind, retryable, status?, cause).
148
+ * @example
149
+ * ```ts
150
+ * const e = toAgentError(new Error('HTTP 500: Internal Server Error'));
151
+ * e.kind; // 'server'
152
+ * e.retryable; // true
153
+ * e.status; // 500
154
+ * ```
155
+ */
156
+ function toAgentError(raw) {
157
+ if (raw instanceof AgentError)
158
+ return raw;
159
+ if (isAbortError(raw))
160
+ return make('aborted', false, raw);
161
+ // 1. Structured status (authoritative): raw.status or raw.cause.status.
162
+ const structured = structuredStatus(raw);
163
+ if (structured !== undefined)
164
+ return classifyByStatus(structured, raw);
165
+ // 2. Network/connection markers are definitive — before any loose text parsing.
166
+ if (isConnectionError(raw))
167
+ return make('connection', true, raw);
168
+ // 3. Best-effort: only an HTTP-shaped status token in the message counts.
169
+ const httpStatus = httpStatusFromMessage(raw);
170
+ if (httpStatus !== undefined)
171
+ return classifyByStatus(httpStatus, raw);
172
+ // 4. Fallback: unknown failure, assume transient.
173
+ const msg = raw instanceof Error && raw.message ? raw.message : 'Something went wrong. You can try again.';
174
+ return make('server', true, raw, undefined, msg);
175
+ }
176
+
15
177
  function isUserMessage(m) {
16
178
  return m.role === 'user';
17
179
  }
@@ -25,6 +187,24 @@ function isSystemMessage(m) {
25
187
  return m.role === 'system';
26
188
  }
27
189
 
190
+ // SPDX-License-Identifier: MIT
191
+ /**
192
+ * Create a typed agent handle.
193
+ *
194
+ * @param debugName Optional name shown in Angular DI error messages.
195
+ * @returns An {@link AgentRef} carrying a state-typed `InjectionToken`.
196
+ * @example
197
+ * ```ts
198
+ * interface TripState { day: number; places: string[]; }
199
+ * export const TRIP = createAgentRef<TripState>('trip');
200
+ * // app.config.ts: provideAgent(TRIP, { assistantId: 'trip' })
201
+ * // component: const agent = injectAgent(TRIP); // LangGraphAgent<TripState>
202
+ * ```
203
+ */
204
+ function createAgentRef(debugName) {
205
+ return { token: new InjectionToken(debugName ?? 'ThreadplaneAgent') };
206
+ }
207
+
28
208
  // SPDX-License-Identifier: MIT
29
209
  class MessageTemplateDirective {
30
210
  chatMessageTemplate = input.required(...(ngDevMode ? [{ debugName: "chatMessageTemplate" }] : []));
@@ -3852,6 +4032,26 @@ const CHAT_ERROR_STYLES = `
3852
4032
  }
3853
4033
  .chat-error__icon { flex-shrink: 0; width: 16px; height: 16px; margin-top: 2px; }
3854
4034
  .chat-error__msg { flex: 1; min-width: 0; word-break: break-word; }
4035
+ .chat-error__retry {
4036
+ flex-shrink: 0;
4037
+ background: transparent;
4038
+ color: var(--ngaf-chat-error-text);
4039
+ border: 1px solid var(--ngaf-chat-error-border);
4040
+ border-radius: var(--ngaf-chat-radius-card);
4041
+ padding: 2px 10px;
4042
+ font-size: var(--ngaf-chat-font-size-sm);
4043
+ cursor: pointer;
4044
+ transition: background 150ms ease, color 150ms ease;
4045
+ white-space: nowrap;
4046
+ }
4047
+ .chat-error__retry:hover {
4048
+ background: var(--ngaf-chat-error-border);
4049
+ color: var(--ngaf-chat-error-text);
4050
+ }
4051
+ .chat-error__retry:focus-visible {
4052
+ outline: 2px solid var(--ngaf-chat-error-border);
4053
+ outline-offset: 2px;
4054
+ }
3855
4055
  `;
3856
4056
 
3857
4057
  // libs/chat/src/lib/primitives/chat-error/chat-error.component.ts
@@ -3867,31 +4067,36 @@ function extractErrorMessage(error) {
3867
4067
  }
3868
4068
  class ChatErrorComponent {
3869
4069
  agent = input.required(...(ngDevMode ? [{ debugName: "agent" }] : []));
3870
- errorMessage = computed(() => extractErrorMessage(this.agent().error()), ...(ngDevMode ? [{ debugName: "errorMessage" }] : []));
3871
4070
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatErrorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3872
4071
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: ChatErrorComponent, isStandalone: true, selector: "chat-error", inputs: { agent: { classPropertyName: "agent", publicName: "agent", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
3873
- @if (errorMessage(); as msg) {
4072
+ @if (agent().error(); as err) {
3874
4073
  <div class="chat-error" role="alert">
3875
4074
  <svg class="chat-error__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3876
4075
  <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
3877
4076
  </svg>
3878
- <span class="chat-error__msg">{{ msg }}</span>
4077
+ <span class="chat-error__msg">{{ err.message }}</span>
4078
+ @if (err.retryable) {
4079
+ <button type="button" class="chat-error__retry" (click)="agent().retry()">Retry</button>
4080
+ }
3879
4081
  </div>
3880
4082
  }
3881
- `, isInline: true, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:block}.chat-error{display:flex;align-items:flex-start;gap:.5rem;background:var(--ngaf-chat-error-bg);border:1px solid var(--ngaf-chat-error-border);color:var(--ngaf-chat-error-text);border-radius:var(--ngaf-chat-radius-card);padding:8px 12px;font-size:var(--ngaf-chat-font-size-sm);margin:0 var(--ngaf-chat-space-6) var(--ngaf-chat-space-2)}.chat-error__icon{flex-shrink:0;width:16px;height:16px;margin-top:2px}.chat-error__msg{flex:1;min-width:0;word-break:break-word}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4083
+ `, isInline: true, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:block}.chat-error{display:flex;align-items:flex-start;gap:.5rem;background:var(--ngaf-chat-error-bg);border:1px solid var(--ngaf-chat-error-border);color:var(--ngaf-chat-error-text);border-radius:var(--ngaf-chat-radius-card);padding:8px 12px;font-size:var(--ngaf-chat-font-size-sm);margin:0 var(--ngaf-chat-space-6) var(--ngaf-chat-space-2)}.chat-error__icon{flex-shrink:0;width:16px;height:16px;margin-top:2px}.chat-error__msg{flex:1;min-width:0;word-break:break-word}.chat-error__retry{flex-shrink:0;background:transparent;color:var(--ngaf-chat-error-text);border:1px solid var(--ngaf-chat-error-border);border-radius:var(--ngaf-chat-radius-card);padding:2px 10px;font-size:var(--ngaf-chat-font-size-sm);cursor:pointer;transition:background .15s ease,color .15s ease;white-space:nowrap}.chat-error__retry:hover{background:var(--ngaf-chat-error-border);color:var(--ngaf-chat-error-text)}.chat-error__retry:focus-visible{outline:2px solid var(--ngaf-chat-error-border);outline-offset:2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3882
4084
  }
3883
4085
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatErrorComponent, decorators: [{
3884
4086
  type: Component,
3885
4087
  args: [{ selector: 'chat-error', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
3886
- @if (errorMessage(); as msg) {
4088
+ @if (agent().error(); as err) {
3887
4089
  <div class="chat-error" role="alert">
3888
4090
  <svg class="chat-error__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
3889
4091
  <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
3890
4092
  </svg>
3891
- <span class="chat-error__msg">{{ msg }}</span>
4093
+ <span class="chat-error__msg">{{ err.message }}</span>
4094
+ @if (err.retryable) {
4095
+ <button type="button" class="chat-error__retry" (click)="agent().retry()">Retry</button>
4096
+ }
3892
4097
  </div>
3893
4098
  }
3894
- `, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:block}.chat-error{display:flex;align-items:flex-start;gap:.5rem;background:var(--ngaf-chat-error-bg);border:1px solid var(--ngaf-chat-error-border);color:var(--ngaf-chat-error-text);border-radius:var(--ngaf-chat-radius-card);padding:8px 12px;font-size:var(--ngaf-chat-font-size-sm);margin:0 var(--ngaf-chat-space-6) var(--ngaf-chat-space-2)}.chat-error__icon{flex-shrink:0;width:16px;height:16px;margin-top:2px}.chat-error__msg{flex:1;min-width:0;word-break:break-word}\n"] }]
4099
+ `, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:block}.chat-error{display:flex;align-items:flex-start;gap:.5rem;background:var(--ngaf-chat-error-bg);border:1px solid var(--ngaf-chat-error-border);color:var(--ngaf-chat-error-text);border-radius:var(--ngaf-chat-radius-card);padding:8px 12px;font-size:var(--ngaf-chat-font-size-sm);margin:0 var(--ngaf-chat-space-6) var(--ngaf-chat-space-2)}.chat-error__icon{flex-shrink:0;width:16px;height:16px;margin-top:2px}.chat-error__msg{flex:1;min-width:0;word-break:break-word}.chat-error__retry{flex-shrink:0;background:transparent;color:var(--ngaf-chat-error-text);border:1px solid var(--ngaf-chat-error-border);border-radius:var(--ngaf-chat-radius-card);padding:2px 10px;font-size:var(--ngaf-chat-font-size-sm);cursor:pointer;transition:background .15s ease,color .15s ease;white-space:nowrap}.chat-error__retry:hover{background:var(--ngaf-chat-error-border);color:var(--ngaf-chat-error-text)}.chat-error__retry:focus-visible{outline:2px solid var(--ngaf-chat-error-border);outline-offset:2px}\n"] }]
3895
4100
  }], propDecorators: { agent: [{ type: i0.Input, args: [{ isSignal: true, alias: "agent", required: true }] }] } });
3896
4101
 
3897
4102
  // SPDX-License-Identifier: MIT
@@ -4600,46 +4805,70 @@ function statusToTraceState(s) {
4600
4805
  class ChatSubagentCardComponent {
4601
4806
  subagent = input.required(...(ngDevMode ? [{ debugName: "subagent" }] : []));
4602
4807
  state = computed(() => statusToTraceState(this.subagent().status()), ...(ngDevMode ? [{ debugName: "state" }] : []));
4603
- latestMessageContent = computed(() => {
4604
- const messages = this.subagent().messages();
4605
- if (messages.length === 0)
4606
- return '';
4607
- const last = messages[messages.length - 1];
4608
- const c = last.content;
4609
- return typeof c === 'string' ? c : JSON.stringify(c);
4610
- }, ...(ngDevMode ? [{ debugName: "latestMessageContent" }] : []));
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
+ }
4611
4822
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSubagentCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4612
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: `
4613
4824
  <chat-trace [state]="state()">
4614
4825
  <span traceLabel>
4615
- <span class="sac__name">Subagent</span>
4826
+ <span class="sac__name">{{ subagent().name ?? 'Subagent' }}</span>
4616
4827
  <span class="sac__id">{{ subagent().toolCallId }}</span>
4617
4828
  <span class="sac__pill" [attr.data-status]="subagent().status()">{{ subagent().status() }}</span>
4618
4829
  </span>
4619
4830
  <div class="sac__count">{{ subagent().messages().length }} message(s)</div>
4620
- @if (subagent().messages().length > 0) {
4621
- <p class="sac__latest-label">Latest message</p>
4622
- <pre class="sac__latest">{{ latestMessageContent() }}</pre>
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>
4623
4843
  }
4624
4844
  </chat-trace>
4625
- `, 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__latest-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ngaf-chat-text-muted);margin:8px 0 4px}.sac__latest{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"], dependencies: [{ kind: "component", type: ChatTraceComponent, selector: "chat-trace", inputs: ["state", "defaultExpanded"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
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 });
4626
4846
  }
4627
4847
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSubagentCardComponent, decorators: [{
4628
4848
  type: Component,
4629
- args: [{ selector: 'chat-subagent-card', standalone: true, imports: [ChatTraceComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
4849
+ args: [{ selector: 'chat-subagent-card', standalone: true, imports: [ChatTraceComponent, ChatToolCallCardComponent, ChatStreamingMdComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
4630
4850
  <chat-trace [state]="state()">
4631
4851
  <span traceLabel>
4632
- <span class="sac__name">Subagent</span>
4852
+ <span class="sac__name">{{ subagent().name ?? 'Subagent' }}</span>
4633
4853
  <span class="sac__id">{{ subagent().toolCallId }}</span>
4634
4854
  <span class="sac__pill" [attr.data-status]="subagent().status()">{{ subagent().status() }}</span>
4635
4855
  </span>
4636
4856
  <div class="sac__count">{{ subagent().messages().length }} message(s)</div>
4637
- @if (subagent().messages().length > 0) {
4638
- <p class="sac__latest-label">Latest message</p>
4639
- <pre class="sac__latest">{{ latestMessageContent() }}</pre>
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>
4640
4869
  }
4641
4870
  </chat-trace>
4642
- `, 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__latest-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--ngaf-chat-text-muted);margin:8px 0 4px}.sac__latest{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"] }]
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"] }]
4643
4872
  }], propDecorators: { subagent: [{ type: i0.Input, args: [{ isSignal: true, alias: "subagent", required: true }] }] } });
4644
4873
 
4645
4874
  // libs/chat/src/lib/primitives/chat-subagents/chat-subagents.component.ts
@@ -6193,10 +6422,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
6193
6422
  class ChatWelcomeSuggestionComponent {
6194
6423
  label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
6195
6424
  value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
6425
+ /** Optional short description, surfaced as a hover/focus tooltip on the chip. */
6426
+ description = input(...(ngDevMode ? [undefined, { debugName: "description" }] : []));
6196
6427
  selected = output();
6197
6428
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatWelcomeSuggestionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6198
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.6", type: ChatWelcomeSuggestionComponent, isStandalone: true, selector: "chat-welcome-suggestion", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { selected: "selected" }, ngImport: i0, template: `
6199
- <button type="button" class="chat-welcome-suggestion" (click)="selected.emit(value())">
6429
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.6", type: ChatWelcomeSuggestionComponent, isStandalone: true, selector: "chat-welcome-suggestion", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selected: "selected" }, ngImport: i0, template: `
6430
+ <button
6431
+ type="button"
6432
+ class="chat-welcome-suggestion"
6433
+ [attr.title]="description() || null"
6434
+ [attr.aria-description]="description() || null"
6435
+ (click)="selected.emit(value())"
6436
+ >
6200
6437
  <ng-content select="[chatWelcomeSuggestionIcon]" />
6201
6438
  <span class="chat-welcome-suggestion__label">{{ label() }}</span>
6202
6439
  <span class="chat-welcome-suggestion__chevron" aria-hidden="true">›</span>
@@ -6206,13 +6443,19 @@ class ChatWelcomeSuggestionComponent {
6206
6443
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatWelcomeSuggestionComponent, decorators: [{
6207
6444
  type: Component,
6208
6445
  args: [{ selector: 'chat-welcome-suggestion', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
6209
- <button type="button" class="chat-welcome-suggestion" (click)="selected.emit(value())">
6446
+ <button
6447
+ type="button"
6448
+ class="chat-welcome-suggestion"
6449
+ [attr.title]="description() || null"
6450
+ [attr.aria-description]="description() || null"
6451
+ (click)="selected.emit(value())"
6452
+ >
6210
6453
  <ng-content select="[chatWelcomeSuggestionIcon]" />
6211
6454
  <span class="chat-welcome-suggestion__label">{{ label() }}</span>
6212
6455
  <span class="chat-welcome-suggestion__chevron" aria-hidden="true">›</span>
6213
6456
  </button>
6214
6457
  `, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:inline-block}.chat-welcome-suggestion{display:inline-flex;align-items:center;gap:.5rem;padding:10px 16px;background:var(--ngaf-chat-surface);border:1px solid var(--ngaf-chat-separator);border-radius:9999px;color:var(--ngaf-chat-text);font-family:inherit;font-size:var(--ngaf-chat-font-size-sm);text-align:center;cursor:pointer;transition:background .15s ease,border-color .15s ease,transform .12s ease}.chat-welcome-suggestion:hover{background:var(--ngaf-chat-surface-alt);border-color:var(--ngaf-chat-text-muted)}.chat-welcome-suggestion:active{transform:scale(.98)}.chat-welcome-suggestion:focus-visible{outline:2px solid var(--ngaf-chat-text-muted);outline-offset:2px}.chat-welcome-suggestion__label{white-space:nowrap}.chat-welcome-suggestion__chevron{display:none}\n"] }]
6215
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], selected: [{ type: i0.Output, args: ["selected"] }] } });
6458
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], selected: [{ type: i0.Output, args: ["selected"] }] } });
6216
6459
 
6217
6460
  // libs/chat/src/lib/styles/chat-select.styles.ts
6218
6461
  // SPDX-License-Identifier: MIT
@@ -6268,7 +6511,10 @@ const CHAT_SELECT_STYLES = `
6268
6511
  z-index: 10;
6269
6512
  }
6270
6513
  .chat-select__option {
6271
- display: block;
6514
+ display: flex;
6515
+ flex-direction: column;
6516
+ align-items: flex-start;
6517
+ gap: 2px;
6272
6518
  width: 100%;
6273
6519
  text-align: left;
6274
6520
  border: 0;
@@ -6280,6 +6526,12 @@ const CHAT_SELECT_STYLES = `
6280
6526
  font-size: var(--ngaf-chat-font-size-sm);
6281
6527
  cursor: pointer;
6282
6528
  }
6529
+ .chat-select__option-desc {
6530
+ font-size: var(--ngaf-chat-font-size-xs);
6531
+ color: var(--ngaf-chat-text-muted);
6532
+ line-height: 1.3;
6533
+ white-space: normal;
6534
+ }
6283
6535
  .chat-select__option:hover:not(:disabled),
6284
6536
  .chat-select__option:focus-visible {
6285
6537
  background: var(--ngaf-chat-surface-alt);
@@ -6458,12 +6710,15 @@ class ChatSelectComponent {
6458
6710
  [attr.aria-selected]="opt.value === value()"
6459
6711
  (click)="selectOption(opt)"
6460
6712
  >
6461
- {{ opt.label }}
6713
+ <span class="chat-select__option-label">{{ opt.label }}</span>
6714
+ @if (opt.description) {
6715
+ <span class="chat-select__option-desc">{{ opt.description }}</span>
6716
+ }
6462
6717
  </button>
6463
6718
  }
6464
6719
  </div>
6465
6720
  }
6466
- `, isInline: true, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:inline-block;position:relative}.chat-select__trigger{height:32px;padding:0 10px;border:0;border-radius:9999px;background:transparent;color:var(--ngaf-chat-text-muted);font:inherit;font-size:var(--ngaf-chat-font-size-sm);display:inline-flex;align-items:center;gap:4px;cursor:pointer;transition:background .12s ease,color .12s ease}.chat-select__trigger:hover:not(:disabled){background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text)}.chat-select__trigger:disabled{opacity:.5;cursor:not-allowed}.chat-select__chevron{width:12px;height:12px;transition:transform .12s ease;flex:none}.chat-select__trigger.is-open .chat-select__chevron{transform:rotate(180deg)}.chat-select__menu{position:absolute;bottom:calc(100% + 8px);right:0;min-width:180px;max-height:320px;overflow-y:auto;background:var(--ngaf-chat-surface);border:1px solid var(--ngaf-chat-separator);border-radius:12px;box-shadow:var(--ngaf-chat-shadow-lg);padding:4px;z-index:10}.chat-select__option{display:block;width:100%;text-align:left;border:0;background:transparent;padding:8px 10px;border-radius:8px;color:var(--ngaf-chat-text);font:inherit;font-size:var(--ngaf-chat-font-size-sm);cursor:pointer}.chat-select__option:hover:not(:disabled),.chat-select__option:focus-visible{background:var(--ngaf-chat-surface-alt);outline:none}.chat-select__option.is-active{background:var(--ngaf-chat-surface-alt);font-weight:500}.chat-select__option:disabled{opacity:.4;cursor:not-allowed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6721
+ `, isInline: true, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:inline-block;position:relative}.chat-select__trigger{height:32px;padding:0 10px;border:0;border-radius:9999px;background:transparent;color:var(--ngaf-chat-text-muted);font:inherit;font-size:var(--ngaf-chat-font-size-sm);display:inline-flex;align-items:center;gap:4px;cursor:pointer;transition:background .12s ease,color .12s ease}.chat-select__trigger:hover:not(:disabled){background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text)}.chat-select__trigger:disabled{opacity:.5;cursor:not-allowed}.chat-select__chevron{width:12px;height:12px;transition:transform .12s ease;flex:none}.chat-select__trigger.is-open .chat-select__chevron{transform:rotate(180deg)}.chat-select__menu{position:absolute;bottom:calc(100% + 8px);right:0;min-width:180px;max-height:320px;overflow-y:auto;background:var(--ngaf-chat-surface);border:1px solid var(--ngaf-chat-separator);border-radius:12px;box-shadow:var(--ngaf-chat-shadow-lg);padding:4px;z-index:10}.chat-select__option{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;text-align:left;border:0;background:transparent;padding:8px 10px;border-radius:8px;color:var(--ngaf-chat-text);font:inherit;font-size:var(--ngaf-chat-font-size-sm);cursor:pointer}.chat-select__option-desc{font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted);line-height:1.3;white-space:normal}.chat-select__option:hover:not(:disabled),.chat-select__option:focus-visible{background:var(--ngaf-chat-surface-alt);outline:none}.chat-select__option.is-active{background:var(--ngaf-chat-surface-alt);font-weight:500}.chat-select__option:disabled{opacity:.4;cursor:not-allowed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6467
6722
  }
6468
6723
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: ChatSelectComponent, decorators: [{
6469
6724
  type: Component,
@@ -6501,17 +6756,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImpor
6501
6756
  [attr.aria-selected]="opt.value === value()"
6502
6757
  (click)="selectOption(opt)"
6503
6758
  >
6504
- {{ opt.label }}
6759
+ <span class="chat-select__option-label">{{ opt.label }}</span>
6760
+ @if (opt.description) {
6761
+ <span class="chat-select__option-desc">{{ opt.description }}</span>
6762
+ }
6505
6763
  </button>
6506
6764
  }
6507
6765
  </div>
6508
6766
  }
6509
- `, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:inline-block;position:relative}.chat-select__trigger{height:32px;padding:0 10px;border:0;border-radius:9999px;background:transparent;color:var(--ngaf-chat-text-muted);font:inherit;font-size:var(--ngaf-chat-font-size-sm);display:inline-flex;align-items:center;gap:4px;cursor:pointer;transition:background .12s ease,color .12s ease}.chat-select__trigger:hover:not(:disabled){background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text)}.chat-select__trigger:disabled{opacity:.5;cursor:not-allowed}.chat-select__chevron{width:12px;height:12px;transition:transform .12s ease;flex:none}.chat-select__trigger.is-open .chat-select__chevron{transform:rotate(180deg)}.chat-select__menu{position:absolute;bottom:calc(100% + 8px);right:0;min-width:180px;max-height:320px;overflow-y:auto;background:var(--ngaf-chat-surface);border:1px solid var(--ngaf-chat-separator);border-radius:12px;box-shadow:var(--ngaf-chat-shadow-lg);padding:4px;z-index:10}.chat-select__option{display:block;width:100%;text-align:left;border:0;background:transparent;padding:8px 10px;border-radius:8px;color:var(--ngaf-chat-text);font:inherit;font-size:var(--ngaf-chat-font-size-sm);cursor:pointer}.chat-select__option:hover:not(:disabled),.chat-select__option:focus-visible{background:var(--ngaf-chat-surface-alt);outline:none}.chat-select__option.is-active{background:var(--ngaf-chat-surface-alt);font-weight:500}.chat-select__option:disabled{opacity:.4;cursor:not-allowed}\n"] }]
6767
+ `, styles: [":host{font-family:var(--ngaf-chat-font-family);color:var(--ngaf-chat-text)}\n", ":host{display:inline-block;position:relative}.chat-select__trigger{height:32px;padding:0 10px;border:0;border-radius:9999px;background:transparent;color:var(--ngaf-chat-text-muted);font:inherit;font-size:var(--ngaf-chat-font-size-sm);display:inline-flex;align-items:center;gap:4px;cursor:pointer;transition:background .12s ease,color .12s ease}.chat-select__trigger:hover:not(:disabled){background:var(--ngaf-chat-surface-alt);color:var(--ngaf-chat-text)}.chat-select__trigger:disabled{opacity:.5;cursor:not-allowed}.chat-select__chevron{width:12px;height:12px;transition:transform .12s ease;flex:none}.chat-select__trigger.is-open .chat-select__chevron{transform:rotate(180deg)}.chat-select__menu{position:absolute;bottom:calc(100% + 8px);right:0;min-width:180px;max-height:320px;overflow-y:auto;background:var(--ngaf-chat-surface);border:1px solid var(--ngaf-chat-separator);border-radius:12px;box-shadow:var(--ngaf-chat-shadow-lg);padding:4px;z-index:10}.chat-select__option{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;text-align:left;border:0;background:transparent;padding:8px 10px;border-radius:8px;color:var(--ngaf-chat-text);font:inherit;font-size:var(--ngaf-chat-font-size-sm);cursor:pointer}.chat-select__option-desc{font-size:var(--ngaf-chat-font-size-xs);color:var(--ngaf-chat-text-muted);line-height:1.3;white-space:normal}.chat-select__option:hover:not(:disabled),.chat-select__option:focus-visible{background:var(--ngaf-chat-surface-alt);outline:none}.chat-select__option.is-active{background:var(--ngaf-chat-surface-alt);font-weight:500}.chat-select__option:disabled{opacity:.4;cursor:not-allowed}\n"] }]
6510
6768
  }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], menuLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuLabel", required: false }] }] } });
6511
6769
 
6512
6770
  // SPDX-License-Identifier: MIT
6513
6771
  const PACKAGE_NAME = '@threadplane/chat';
6514
6772
  const CHAT_CONFIG = new InjectionToken('CHAT_CONFIG');
6773
+ /**
6774
+ * Bootstrap `@threadplane/chat` in an Angular application or standalone
6775
+ * component tree.
6776
+ *
6777
+ * Call this once inside `bootstrapApplication` (or the `providers` array of a
6778
+ * root `ApplicationConfig`). It registers the shared {@link ChatConfig} token
6779
+ * so every chat component in the tree can read the render registry, avatar
6780
+ * label, and assistant display name without explicit prop threading.
6781
+ *
6782
+ * A license check is fired asynchronously on every call (it never throws; a
6783
+ * watermark is shown in non-commercial builds when no valid token is supplied).
6784
+ *
6785
+ * @param config Options bag that controls the chat feature set:
6786
+ * - `renderRegistry` — shared {@link AngularRegistry} wiring tool-view
6787
+ * components to their names; pass the value returned by
6788
+ * `defineAngularRegistry` from `\@threadplane/render`.
6789
+ * - `avatarLabel` — short label shown in the AI avatar bubble (default `"A"`).
6790
+ * - `assistantName` — display name shown above assistant messages
6791
+ * (default `"Assistant"`).
6792
+ * - `license` — signed token from threadplane.ai; omit in development.
6793
+ * @returns An `EnvironmentProviders` value suitable for the `providers` array
6794
+ * of `bootstrapApplication` or `ApplicationConfig`.
6795
+ * @example
6796
+ * ```ts
6797
+ * // main.ts
6798
+ * import { bootstrapApplication } from '@angular/platform-browser';
6799
+ * import { provideChat } from '@threadplane/chat';
6800
+ * import { defineAngularRegistry, provideRender } from '@threadplane/render';
6801
+ * import { DayCardComponent } from './day-card.component';
6802
+ *
6803
+ * const registry = defineAngularRegistry({ day_card: DayCardComponent });
6804
+ *
6805
+ * bootstrapApplication(AppComponent, {
6806
+ * providers: [
6807
+ * provideChat({ renderRegistry: registry, avatarLabel: 'AI' }),
6808
+ * provideRender({ registry }),
6809
+ * ],
6810
+ * });
6811
+ * ```
6812
+ */
6515
6813
  function provideChat(config) {
6516
6814
  void runLicenseCheck({
6517
6815
  package: PACKAGE_NAME,
@@ -6524,6 +6822,65 @@ function provideChat(config) {
6524
6822
  ]);
6525
6823
  }
6526
6824
 
6825
+ // SPDX-License-Identifier: MIT
6826
+ const defaultFromUrl = (url) => {
6827
+ const seg = url.split('?')[0].split('#')[0].split('/').filter(Boolean);
6828
+ return seg.length ? seg[seg.length - 1] : null;
6829
+ };
6830
+ const defaultToCommands = (id) => (id ? ['/', id] : ['/']);
6831
+ /**
6832
+ * Bind an app-owned `activeThreadId` signal to the URL — restore on load, stamp on change,
6833
+ * validate-or-redirect, with a bare URL meaning "no thread" (welcome). URL is the source of
6834
+ * truth; nothing is written to localStorage. Must be called in an injection context.
6835
+ *
6836
+ * @example
6837
+ * ```ts
6838
+ * export const ACTIVE_THREAD = signal<string | null>(null);
6839
+ * // providers: provideAgent({ threadId: ACTIVE_THREAD, onThreadId: id => ACTIVE_THREAD.set(id) })
6840
+ * const threads = inject(LangGraphThreadsAdapter);
6841
+ * injectThreadRouting({ threadId: ACTIVE_THREAD, validate: id => threads.getThread(id).then(Boolean) });
6842
+ * ```
6843
+ */
6844
+ function injectThreadRouting(config) {
6845
+ const router = inject(Router);
6846
+ const fromUrl = config.threadIdFromUrl ?? defaultFromUrl;
6847
+ const toCommands = config.toCommands ?? defaultToCommands;
6848
+ const extras = config.navigationExtras ?? { queryParamsHandling: 'preserve' };
6849
+ const urlThreadId = toSignal(router.events.pipe(filter((e) => e instanceof NavigationEnd), map((e) => fromUrl(e.urlAfterRedirects)), startWith(fromUrl(router.url))), { initialValue: fromUrl(router.url) });
6850
+ // Seed the signal from the URL once.
6851
+ config.threadId.set(urlThreadId());
6852
+ // URL → signal.
6853
+ effect(() => {
6854
+ const urlId = urlThreadId();
6855
+ if (urlId !== untracked(() => config.threadId()))
6856
+ config.threadId.set(urlId);
6857
+ });
6858
+ // signal → URL.
6859
+ effect(() => {
6860
+ const id = config.threadId();
6861
+ const urlId = untracked(() => urlThreadId());
6862
+ if (id !== urlId)
6863
+ void router.navigate(toCommands(id), extras);
6864
+ });
6865
+ // validate stale ids → redirect to bare. Memoize the last id checked so
6866
+ // re-visiting the same thread (A → B → A) doesn't re-hit the backend.
6867
+ if (config.validate) {
6868
+ const validate = config.validate;
6869
+ let lastValidated = null;
6870
+ effect(() => {
6871
+ const id = urlThreadId();
6872
+ if (!id || id === lastValidated)
6873
+ return;
6874
+ lastValidated = id;
6875
+ void validate(id).then((ok) => {
6876
+ if (!ok && untracked(() => urlThreadId()) === id) {
6877
+ void router.navigate(toCommands(null), { ...extras, replaceUrl: true });
6878
+ }
6879
+ });
6880
+ });
6881
+ }
6882
+ }
6883
+
6527
6884
  // SPDX-License-Identifier: MIT
6528
6885
  const CHAT_LIFECYCLE = new InjectionToken('CHAT_LIFECYCLE');
6529
6886
 
@@ -11888,19 +12245,115 @@ function a2uiBasicCatalog() {
11888
12245
  });
11889
12246
  }
11890
12247
 
11891
- /** Async function tool — its resolved return value becomes the tool result. */
12248
+ /**
12249
+ * Declare an async function tool the model can call; its resolved return value
12250
+ * becomes the tool result shipped back to the model.
12251
+ *
12252
+ * @param description Natural-language description the model sees.
12253
+ * @param schema Standard Schema (e.g. a Zod object) for the arguments; the
12254
+ * handler's argument type is inferred from it.
12255
+ * @param handler Runs in the browser when the model calls the tool; its return
12256
+ * type `R` is carried on the resulting {@link FunctionToolDef}.
12257
+ * @returns A {@link FunctionToolDef} for inclusion in {@link tools}.
12258
+ * @example
12259
+ * ```ts
12260
+ * const move = action('Move a stop', z.object({ fromDay: z.number() }), (a) => a.fromDay);
12261
+ * const registry = tools({ move_stop: move });
12262
+ * ```
12263
+ */
11892
12264
  function action(description, schema, handler) {
11893
12265
  return { kind: 'function', description, schema, handler };
11894
12266
  }
11895
- /** Render-only component tool — the model fills its props; auto-acknowledged. */
12267
+ /**
12268
+ * Render-only component tool — the model fills the component's props from the
12269
+ * schema's output; the tool call is auto-acknowledged once the component mounts.
12270
+ *
12271
+ * The component's signal inputs are checked against the schema output type
12272
+ * (strict-but-flexible: every schema key must be a declared input with an
12273
+ * assignable type; the component may declare extra inputs the schema doesn't fill).
12274
+ * Author the component with `ViewProps<typeof schema>` as the input type set to
12275
+ * guarantee the shapes stay aligned.
12276
+ *
12277
+ * @param description Natural-language description the model sees.
12278
+ * @param schema Standard Schema defining the props the model must supply.
12279
+ * @param component Angular component whose signal inputs must be compatible with
12280
+ * the schema output. A type-level error is reported here when they diverge.
12281
+ * @returns A {@link ViewToolDef} for inclusion in {@link tools}.
12282
+ * @example
12283
+ * ```ts
12284
+ * const schema = z.object({ label: z.string(), day: z.number() });
12285
+ * type Inputs = ViewProps<typeof schema>; // { label: string; day: number }
12286
+ *
12287
+ * \@Component({ ... })
12288
+ * class DayCardComponent {
12289
+ * label = input.required<string>();
12290
+ * day = input.required<number>();
12291
+ * }
12292
+ *
12293
+ * const dayCard = view('Show a day card', schema, DayCardComponent);
12294
+ * const registry = tools({ day_card: dayCard });
12295
+ * ```
12296
+ */
11896
12297
  function view(description, schema, component) {
11897
- return { kind: 'view', description, schema, component };
12298
+ return { kind: 'view', description, schema, component: component };
11898
12299
  }
11899
- /** Interactive (HITL) component tool — the value it emits becomes the result. */
12300
+ /**
12301
+ * Interactive (human-in-the-loop) component tool — the model fills the
12302
+ * component's props from the schema's output; the value the component emits
12303
+ * back to the framework becomes the tool result sent to the model.
12304
+ *
12305
+ * The component's signal inputs are checked against the schema output type
12306
+ * (strict-but-flexible: every schema key must be a declared input with an
12307
+ * assignable type; the component may declare extra inputs the schema doesn't
12308
+ * fill). Author the component with `ViewProps<typeof schema>` to derive input
12309
+ * prop types directly from the schema.
12310
+ *
12311
+ * @param description Natural-language description the model sees.
12312
+ * @param schema Standard Schema defining the props the model must supply.
12313
+ * @param component Angular component whose signal inputs must be compatible with
12314
+ * the schema output. A type-level error is reported here when they diverge.
12315
+ * @returns An {@link AskToolDef} for inclusion in {@link tools}.
12316
+ * @example
12317
+ * ```ts
12318
+ * const schema = z.object({ question: z.string(), options: z.array(z.string()) });
12319
+ * type Inputs = ViewProps<typeof schema>;
12320
+ *
12321
+ * \@Component({ ... })
12322
+ * class ChoiceCardComponent {
12323
+ * question = input.required<string>();
12324
+ * options = input.required<string[]>();
12325
+ * // Emits the chosen option back to the model.
12326
+ * }
12327
+ *
12328
+ * const choice = ask('Ask the user to choose', schema, ChoiceCardComponent);
12329
+ * const registry = tools({ pick_option: choice });
12330
+ * ```
12331
+ */
11900
12332
  function ask(description, schema, component) {
11901
- return { kind: 'ask', description, schema, component };
12333
+ return { kind: 'ask', description, schema, component: component };
11902
12334
  }
11903
- /** Collect named client tools into a frozen registry (the key is the tool name). */
12335
+ /**
12336
+ * Collect named client tools into a frozen, name-keyed registry.
12337
+ *
12338
+ * The overload is generic over the entire map (`const M`) so that each tool's
12339
+ * precise type ({@link FunctionToolDef}`<S,R>`, {@link ViewToolDef}`<S,C>`, or
12340
+ * {@link AskToolDef}`<S,C>`) and every literal key are preserved in the
12341
+ * {@link ClientToolRegistry} passed to `provideChat`. This lets downstream
12342
+ * consumers look up individual tools without losing generic information.
12343
+ *
12344
+ * @param map An object literal mapping tool names to tool definitions created
12345
+ * by {@link action}, {@link view}, or {@link ask}.
12346
+ * @returns A frozen `Readonly<M>` where `M` is the exact inferred map shape.
12347
+ * @example
12348
+ * ```ts
12349
+ * const move = action('Move a stop', z.object({ fromDay: z.number() }), (a) => a.fromDay);
12350
+ * const dayCard = view('Show a day card', z.object({ label: z.string() }), DayCardComponent);
12351
+ *
12352
+ * const registry = tools({ move_stop: move, day_card: dayCard });
12353
+ * // registry.move_stop is FunctionToolDef<...>
12354
+ * // registry.day_card is ViewToolDef<...>
12355
+ * ```
12356
+ */
11904
12357
  function tools(map) {
11905
12358
  return Object.freeze({ ...map });
11906
12359
  }
@@ -11910,7 +12363,7 @@ function mockAgent(opts = {}) {
11910
12363
  const messages = signal(opts.messages ?? [], ...(ngDevMode ? [{ debugName: "messages" }] : []));
11911
12364
  const status = signal(opts.status ?? 'idle', ...(ngDevMode ? [{ debugName: "status" }] : []));
11912
12365
  const isLoading = signal(opts.isLoading ?? false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
11913
- const error = signal(opts.error ?? null, ...(ngDevMode ? [{ debugName: "error" }] : []));
12366
+ const error = signal(opts.error ?? undefined, ...(ngDevMode ? [{ debugName: "error" }] : []));
11914
12367
  const toolCalls = signal(opts.toolCalls ?? [], ...(ngDevMode ? [{ debugName: "toolCalls" }] : []));
11915
12368
  const state = signal(opts.state ?? {}, ...(ngDevMode ? [{ debugName: "state" }] : []));
11916
12369
  const interrupt = opts.withInterrupt
@@ -11935,6 +12388,7 @@ function mockAgent(opts = {}) {
11935
12388
  events$: opts.events$ ?? EMPTY,
11936
12389
  submit: async (input, submitOpts) => { submitCalls.push({ input, opts: submitOpts }); },
11937
12390
  stop: async () => { stopCount++; },
12391
+ retry: async () => { return; },
11938
12392
  regenerate: async (assistantMessageIndex) => {
11939
12393
  // Truncate messages [N..end] and record the call as a synthetic submit so
11940
12394
  // tests can assert regenerate behavior via the same submitCalls log.
@@ -11956,5 +12410,5 @@ function mockAgent(opts = {}) {
11956
12410
  * Generated bundle index. Do not edit.
11957
12411
  */
11958
12412
 
11959
- export { A2uiAudioPlayerComponent, A2uiButtonComponent, A2uiCardComponent, A2uiCheckBoxComponent, A2uiColumnComponent, A2uiDateTimeInputComponent, A2uiDividerComponent, A2uiIconComponent, A2uiImageComponent, A2uiListComponent, A2uiModalComponent, A2uiMultipleChoiceComponent, A2uiRowComponent, A2uiSliderComponent, A2uiSurfaceComponent, A2uiTabsComponent, A2uiTextComponent, A2uiTextFieldComponent, A2uiVideoComponent, 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, createClientToolsCoordinator, createContentClassifier, createParseTreeStore, createPartialArgsBridge, deriveJsonSchema, emitBinding, executeFunctionTool, extractErrorMessage, formatDuration, getInterrupt, getMessageType, isAssistantMessage, isSystemMessage, isToolMessage, isTyping, isUserMessage, messageContent, mockAgent, normalizeEnvelopeArgs, normalizeViewEntry, provideChat, renderMarkdown, startClientToolExecutor, statusColor, submitMessage, surfaceToSpec, toClientToolSpecs, tools, validateArgs, view };
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 };
11960
12414
  //# sourceMappingURL=threadplane-chat.mjs.map