@adia-ai/web-modules 0.8.29 → 0.8.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog — @adia-ai/web-modules
2
2
 
3
+ ## [0.8.31] — 2026-08-07
4
+
5
+ ### Fixed
6
+ - **`chat-empty` is now registered** (gh#655). It had every sidecar (yaml/a2ui.json/examples) but no `defineIfFree('chat-empty', …)` anywhere — the tag never upgraded, so `customElements.whenDefined('chat-empty')` never resolved for any consumer awaiting it (light-DOM children still rendered; CSS tag-selectors still painted it, since neither cares about upgrade status — but the element itself stayed undefined). Added `chat-empty.js` with a minimal `UIElement` subclass (no reactive props, matching its CSS-only visibility contract) registered the way the sibling JS-bearing chat-cluster modules are (`chat-sidebar`, `chat-thread`, `chat-composer`); wired into `chat/index.js`'s barrel export.
7
+
8
+ ### Maintenance
9
+ - **`dist/` bundles rebuilt** in this cut's window (1 file(s)) — regenerated from the source changes described above, not independent edits.
10
+
11
+ ## [0.8.30] — 2026-08-07
12
+
13
+ ### Added
14
+ - **`chatShell.setStreamedText(text)`** — replaces the streaming assistant message's text outright, the counterpart to `appendChunk` for a producer that revises what it already streamed.
15
+
16
+ ### Fixed
17
+ - **`chat-composer` stops the inner `submit` event at its re-dispatch boundary (gh#676).** The re-emitted `composer-submit` was riding alongside the raw `submit` bubbling in from `chat-input-ui` unstopped — a host could receive both. `stopPropagation()` on the inner event before re-dispatching as `composer-submit` closes the second path.
18
+ - **A rewritten or withheld turn now leaves the screen agreeing with the transcript (gh#665).** `wireAgentEvents` treated every `text` event as an append, so an `@adia-ai/agent` output guardrail's re-snapshot (`{ text: '', snapshot: <rewritten> }`) appended nothing and the ORIGINAL text stayed on screen while the session recorded the rewrite — a redaction that redacted nothing visible. An empty delta is now a replace from `snapshot` (an empty snapshot clears the bubble), which is safe for any text event since the snapshot is always the authoritative text.
19
+
20
+ ### Maintenance
21
+ - **`billing/` touched in this release window** (1 file(s), e.g. `invoice-history/invoice-history.class.js`) — carried by the entries above.
22
+ - **`chat/` touched in this release window** (5 file(s), e.g. `chat/README.md`) — carried by the entries above.
23
+ - **`dist/` bundles rebuilt** in this cut's window (2 file(s)) — regenerated from the source changes described above, not independent edits.
24
+
3
25
  ## [0.8.29] — 2026-08-06
4
26
 
5
27
  ### Added
package/chat/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # `web-modules/chat`
2
+
3
+ The chat cluster: `<chat-shell>` (behavior-only orchestrator) composing
4
+ `<chat-header>` / `<chat-thread>` / `<chat-composer>` / `<chat-status>` /
5
+ `<chat-empty>`, plus `chat-surfaces.js` — the L3 seam that renders an
6
+ `@adia-ai/agent` `AgentEvent` stream (or a raw `@adia-ai/llm` chunk stream)
7
+ into the thread. See `chat-shell/chat-shell.js`'s header comment for the
8
+ bespoke structure vocabulary and the two operating modes (built-in LLM
9
+ streaming vs. external-drive).
10
+
11
+ ## `wireAgentEvents` — AgentEvent → rendering (gh#667)
12
+
13
+ `wireAgentEvents(chatShell, registry, { events, statusEl? })` folds ONE
14
+ `AsyncIterable<AgentEvent>` (`@adia-ai/agent`'s stream contract — see
15
+ [`packages/agent/README.md`](../../agent/README.md#events)) into the thread.
16
+ It never touches session state — `reduce(session, event)` is the caller's
17
+ job, the same event object just also flows through this function to paint.
18
+
19
+ | Event | Rendering |
20
+ | --- | --- |
21
+ | `text` | Appended to the streaming bubble (`chatShell.appendChunk`); an EMPTY delta REPLACES from `snapshot` (`chatShell.setStreamedText`) — the guardrail-rewrite case (see `@adia-ai/agent`'s README, "Output rewrite"). |
22
+ | `thinking` | `<agent-reasoning-ui>.addThought(text)` — folded into the turn's collapsed-by-default reasoning viewer. |
23
+ | `tool_use` | `<agent-reasoning-ui>.addStep({id, label})` (or `updateStep` if `tool_input_delta` already opened the step) — a running step in the reasoning viewer's timeline. |
24
+ | `tool_input_delta` | Cheap upsert on the running step's `label` (`updateStep`, or `addStep` if this is the first fragment) — progress only, nothing executes off a partial. |
25
+ | `tool_result` | `updateStep(id, {outcomes:[output]})` + `completeStep(id)`, or `failStep(id, output)` when `isError` — the step's status icon flips to done/error and the (truncated) output becomes the step's collapsible outcome. |
26
+ | `step` (`workflow: 'plan'`) | Feed-chrome prose via `describePlanManifest` / `describeTurnSynthesis` (gh#648) — unchanged, NOT routed through the reasoning viewer (a plan manifest/synthesis line is thread narration, not a pipeline step). |
27
+ | `step` (any other workflow) | One-shot attempt marker — `@adia-ai/agent`'s workflow loop emits this AFTER the step already ran, so it lands as an immediately-settled `addStep` + `completeStep`/`failStep` in the reasoning viewer. |
28
+ | `guardrail` | `addThought('Guardrail "<name>" <blocked\|rewrote> <target> — <reason>')` — a subtle note inside the (collapsed) reasoning viewer, never a thread interruption. |
29
+ | `progress` | `statusEl.textContent` from the code-owned `PROGRESS_LABELS` table (CHAT-HARNESS law 3 — never the raw `stage` string, never model text). |
30
+ | `surface` | Routed to the surface-host `registry` (WCH-4 / gh#599) — a generative-UI envelope line, mounted/updated/closed via `createSurfaceRegistry`. |
31
+ | `memory` | Render-silent — a session-state write, not a thread event. |
32
+ | `trace` | Render-silent by default — tracing is devtools' surface (a future inspector panel reads the `trace` event / `@adia-ai/agent`'s `trace` sink), never the thread. |
33
+ | `error` | `reasoning.fail(message)` (if a reasoning viewer is open) + an error bubble + `stopStreaming()`. |
34
+ | `done` | `reasoning.finish()` (or `.fail('Blocked by guardrail')` when `stopReason === 'guardrail_denied'`) + `stopStreaming()`. |
35
+
36
+ **Why `<agent-reasoning-ui>`, not a new primitive:** the primitive audit
37
+ (adia-author §0) found `agent-reasoning-ui` already built for exactly this
38
+ shape — "Agent inner monologue + pipeline viewer with steps, thoughts,
39
+ plans, and iterations," composing `<timeline-ui>` for step rows with a
40
+ collapsible per-step `outcomes` list (the "collapsible tool-result body"
41
+ requirement) and a `collapsed`/status-icon summary row (the "thinking
42
+ affordance" requirement). One viewer, mounted lazily above the streamed
43
+ answer on the first reasoning-worthy event per turn, covers tool activity,
44
+ workflow-step markers, thinking, and guardrail notes — no chip primitive,
45
+ no dedicated step-marker element, and no second collapsible region were
46
+ minted. `<chat-status>` (candidate considered) stays scoped to the header's
47
+ connection/streaming indicator — it is a single, module-scoped element, not
48
+ shaped for a per-event marker stream.
49
+
50
+ **Demo:** `playgrounds/agent-chat/app/agent-chat.contents.js` — the *"Ask
51
+ about a component"* prompt path drives a real `createAgent()` loop with a
52
+ keyless deterministic client (`simulatedClient()`, used whenever
53
+ `npm run proxy` isn't up) through `tool_use` → `tool_result` → final text,
54
+ rendered live via this module's `wireAgentEvents`.
@@ -49,6 +49,12 @@ class ChatComposer extends UIElement {
49
49
  this.#inputEl = this.querySelector('chat-input-ui') || this.querySelector('input-ui') || this.querySelector('textarea-ui');
50
50
 
51
51
  this.#onSubmit = (e) => {
52
+ // Stop the inner 'submit' event at this re-dispatch boundary too —
53
+ // this component re-emits it as 'composer-submit' below, so letting
54
+ // the raw 'submit' keep bubbling past chat-composer would let a
55
+ // host that (also) listens for raw 'submit' receive a duplicate
56
+ // (gh#676).
57
+ e.stopPropagation();
52
58
  // Re-emit as 'composer-submit' so host listens to one event
53
59
  // regardless of which inner input it is
54
60
  this.dispatchEvent(new CustomEvent('composer-submit', {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://adiaui.dev/a2ui/v0_9/components/ChatEmpty.json",
4
4
  "title": "ChatEmpty",
5
- "description": "Module-tier chat empty state. CSS-only no behavior, no JS. Sits\nas the first child of <chat-thread> as the empty state placeholder.\nVisibility is driven by <chat-thread>'s [empty] reflected attribute\nvia CSS (no JS toggling needed) when messages arrive, the parent\nthread loses [empty] and CSS hides this stub.\n\nReplaces the legacy <empty-state-ui data-chat-empty> child of the\nmessages container per ADR-0023. The legacy shape was retired in\nv0.4.0 per ADR-0024 and is no longer recognized.\n",
5
+ "description": "Module-tier chat empty state. No reactive props visibility is\ndriven by <chat-thread>'s [empty] reflected attribute via CSS (no\nJS toggling needed); when messages arrive, the parent thread loses\n[empty] and CSS hides this stub. Registered (defineIfFree) so the\ntag upgrades and resolves customElements.whenDefined('chat-empty')\n(gh#655) — sits as the first child of <chat-thread> as the empty\nstate placeholder.\n\nReplaces the legacy <empty-state-ui data-chat-empty> child of the\nmessages container per ADR-0023. The legacy shape was retired in\nv0.4.0 per ADR-0024 and is no longer recognized.\n",
6
6
  "type": "object",
7
7
  "allOf": [
8
8
  {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `<chat-empty>` — Module-tier chat empty state. No reactive props — visibility is
3
+ driven by <chat-thread>'s [empty] reflected attribute via CSS (no
4
+ JS toggling needed); when messages arrive, the parent thread loses
5
+ [empty] and CSS hides this stub. Registered (defineIfFree) so the
6
+ tag upgrades and resolves customElements.whenDefined('chat-empty')
7
+ (gh#655) — sits as the first child of <chat-thread> as the empty
8
+ state placeholder.
9
+
10
+ Replaces the legacy <empty-state-ui data-chat-empty> child of the
11
+ messages container per ADR-0023. The legacy shape was retired in
12
+ v0.4.0 per ADR-0024 and is no longer recognized.
13
+
14
+ *
15
+ * @see https://ui-kit.exe.xyz/site/components/chat-empty
16
+ *
17
+ * Type declarations generated by scripts/build/dts-codegen.mjs from
18
+ * the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
19
+ * run `npm run build:components`, then `npm run codegen:dts` to
20
+ * regenerate; or hand-author this file fully if rich event types are
21
+ * needed beyond what the yaml `events:` block can express.
22
+ */
23
+
24
+ import { UIElement } from '../../../web-components/core/element.js';
25
+
26
+ export class ChatEmpty extends UIElement {
27
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * <chat-empty>
3
+ * <empty-state-ui icon="chat-circle" heading="Hello!" description="Ask me anything."></empty-state-ui>
4
+ * </chat-empty>
5
+ *
6
+ * Module-tier chat empty state — the bespoke replacement for legacy
7
+ * `<empty-state-ui data-chat-empty>` per ADR-0023. Sits as the first
8
+ * child of <chat-thread>; visibility is driven entirely by CSS off
9
+ * <chat-thread>'s [empty] reflected attribute (see
10
+ * chat-shell/css/chat-shell.bespoke.css) — no JS toggling needed.
11
+ *
12
+ * Registered (gh#655) even though the element carries no reactive
13
+ * state: an unregistered custom element never resolves
14
+ * `customElements.whenDefined('chat-empty')` and never matches
15
+ * `:defined` — a host or test that waits on either hangs/mismatches
16
+ * forever, even though CSS tag-selectors keep painting it either way.
17
+ * Matches the sibling JS-bearing chat-cluster modules' own-file
18
+ * registration shape (chat-sidebar, chat-thread, chat-composer);
19
+ * chat-header/chat-status stay genuinely unregistered CSS-only markup
20
+ * wrappers — chat-empty is the one with a live tag reference in
21
+ * consumer surfaces (patient-visit), so it gets the class.
22
+ */
23
+
24
+ import { defineIfFree } from '../../../web-components/core/register.js';
25
+ import { UIElement } from '../../../web-components/core/element.js';
26
+
27
+ class ChatEmpty extends UIElement {
28
+ static properties = {};
29
+
30
+ static template = () => null;
31
+ }
32
+
33
+ defineIfFree('chat-empty', ChatEmpty);
34
+ export { ChatEmpty };
@@ -7,11 +7,13 @@ component: ChatEmpty
7
7
  category: feedback
8
8
  version: 1
9
9
  description: |
10
- Module-tier chat empty state. CSS-only no behavior, no JS. Sits
11
- as the first child of <chat-thread> as the empty state placeholder.
12
- Visibility is driven by <chat-thread>'s [empty] reflected attribute
13
- via CSS (no JS toggling needed) when messages arrive, the parent
14
- thread loses [empty] and CSS hides this stub.
10
+ Module-tier chat empty state. No reactive props visibility is
11
+ driven by <chat-thread>'s [empty] reflected attribute via CSS (no
12
+ JS toggling needed); when messages arrive, the parent thread loses
13
+ [empty] and CSS hides this stub. Registered (defineIfFree) so the
14
+ tag upgrades and resolves customElements.whenDefined('chat-empty')
15
+ (gh#655) — sits as the first child of <chat-thread> as the empty
16
+ state placeholder.
15
17
 
16
18
  Replaces the legacy <empty-state-ui data-chat-empty> child of the
17
19
  messages container per ADR-0023. The legacy shape was retired in
@@ -43,8 +45,8 @@ a2ui:
43
45
  <empty-state-ui data-chat-empty>. Place as the first child of
44
46
  <chat-thread>; visibility is automatic via the [empty]
45
47
  reflected attribute.
46
- - rule: 'CSS-only no JS module needed in shell HTML imports.'
47
- reason: 'CSS-only primitive.'
48
+ - rule: 'No reactive props — visibility is CSS-driven off the parent [empty] attribute, not a JS toggle.'
49
+ reason: 'State contract.'
48
50
  - rule: 'For non-chat empty states (lists, tables, canvas) use <empty-state-ui> primitive instead; chat-empty is chat-cluster-namespaced.'
49
51
  reason: 'Cluster vs primitive distinction.'
50
52
 
@@ -184,6 +184,25 @@ class ChatShell extends UIElement {
184
184
  this.#scrollToBottom();
185
185
  }
186
186
 
187
+ /**
188
+ * Replace the streaming assistant message's text outright, instead of
189
+ * appending to it. The counterpart to `appendChunk` for the case where a
190
+ * producer revises what it already streamed — an `@adia-ai/agent` output
191
+ * guardrail rewriting or withholding the turn's text is the real one.
192
+ * Appending cannot express that: what is on screen has to come off.
193
+ *
194
+ * @param {string} text — the full replacement text ('' clears the bubble).
195
+ */
196
+ setStreamedText(text) {
197
+ const last = this.#messages[this.#messages.length - 1];
198
+ if (!last || last.role !== 'assistant') return;
199
+ last.content = text;
200
+
201
+ const contentEl = this.#messagesEl?.querySelector('[data-role]:last-child [data-content]');
202
+ if (contentEl) contentEl.textContent = text;
203
+ this.#scrollToBottom();
204
+ }
205
+
187
206
  deleteMessage(id) {
188
207
  const idx = this.#messages.findIndex(m => m.id === id);
189
208
  if (idx === -1) return;
@@ -58,6 +58,9 @@ export function describeTurnSynthesis(synthesis: {
58
58
  export function wireAgentEvents(
59
59
  chatShell: Element & {
60
60
  appendChunk?(text: string): void;
61
+ /** Replace the streaming text outright — used when a text event carries
62
+ * an empty delta, which is a producer revising what it streamed. */
63
+ setStreamedText?(text: string): void;
61
64
  appendMessage?(msg: { role: string; content?: string; render?: boolean }): Element;
62
65
  startStreaming?(): void;
63
66
  stopStreaming?(): void;
@@ -24,7 +24,22 @@
24
24
  * specifier. The HOST (a demo page, a consumer app) builds the renderer
25
25
  * (`createRenderer` + its own `WidgetAdapter`) and hands it in; this module
26
26
  * only ever calls the five methods `Renderer` exposes.
27
+ *
28
+ * `@adia-ai/web-components` primitives ARE a declared dependency edge
29
+ * (unlike `@genui/*`), so `wireAgentEvents` imports `<agent-reasoning-ui>`
30
+ * directly (gh#667) — the "agent inner monologue + pipeline viewer with
31
+ * steps, thoughts, plans, and iterations" primitive already built for
32
+ * exactly this shape (primitive audit, adia-author §0 — no new primitive
33
+ * was minted). One reasoning viewer is lazily mounted per assistant turn
34
+ * and renders `thinking` (→ `addThought`), `tool_use`/`tool_input_delta`/
35
+ * `tool_result` (→ `addStep`/`updateStep`/`completeStep`/`failStep`, with
36
+ * the tool output riding the step's collapsible `outcomes`), non-'plan'
37
+ * `step` events (→ the same step API — one-shot attempt markers), and
38
+ * `guardrail` (→ `addThought`, a subtle note). `memory` and `trace` stay
39
+ * render-silent (trace is devtools' surface, not the thread's — see the
40
+ * event → rendering table in `packages/web-modules/chat/README.md`).
27
41
  */
42
+ import '../../../web-components/components/agent-reasoning/agent-reasoning.js';
28
43
 
29
44
  const CLOSED_ANNOTATION_TEXT = 'Closed.';
30
45
  const PROTOCOL_VERSION = 'v1.0';
@@ -220,10 +235,40 @@ export async function wireAgentEvents(chatShell, registry, opts) {
220
235
  return thread?.querySelector('[data-role="assistant"]:last-child [data-bubble]') ?? null;
221
236
  };
222
237
 
238
+ // One <agent-reasoning-ui> per turn (gh#667) — lazily mounted above the
239
+ // streamed answer on the first reasoning-worthy event (thinking, tool
240
+ // activity, a non-plan step, or a guardrail note), and re-used for the
241
+ // rest of the turn so tool steps/thoughts accumulate in ONE collapsible
242
+ // region instead of one element per event. `null` until then; a fresh
243
+ // `wireAgentEvents` call (one per turn, per this function's own contract
244
+ // above) starts a fresh viewer.
245
+ let reasoning = null;
246
+ const ensureReasoning = () => {
247
+ if (reasoning) return reasoning;
248
+ const bubble = currentBubble();
249
+ if (!bubble) return null;
250
+ reasoning = document.createElement('agent-reasoning-ui');
251
+ // Collapsed BY DEFAULT (gh#667 scope — thinking is a collapsed-by-default
252
+ // affordance, not a start-expanded one; the primitive's own default is
253
+ // expanded, so this is set explicitly).
254
+ reasoning.collapsed = true;
255
+ // Above the streamed text — the "showed its work, then answered" order.
256
+ bubble.insertBefore(reasoning, bubble.firstChild);
257
+ return reasoning;
258
+ };
259
+ const findStepEntry = (id) => reasoning?.entries?.find((e) => e.kind === 'step' && e.id === id);
260
+
223
261
  for await (const event of events) {
224
262
  switch (event.type) {
225
263
  case 'text':
226
- chatShell.appendChunk?.(event.text);
264
+ // An EMPTY delta is a replace, not a no-op: the snapshot is the
265
+ // authoritative text, and a producer sends this shape when it
266
+ // revises what it already streamed (an @adia-ai/agent output
267
+ // guardrail rewriting or withholding the turn). Resetting from the
268
+ // snapshot is safe for any text event — appending is only the
269
+ // cheaper path for the ordinary incremental case.
270
+ if (event.text === '') chatShell.setStreamedText?.(event.snapshot ?? '');
271
+ else chatShell.appendChunk?.(event.text);
227
272
  break;
228
273
 
229
274
  case 'progress': {
@@ -240,43 +285,96 @@ export async function wireAgentEvents(chatShell, registry, opts) {
240
285
 
241
286
  case 'step': {
242
287
  // Plan-envelope chrome (gh#648, ruling 1 — harness-level progress,
243
- // never wire; ruling 3 — feed chrome, never a canvas). Any other
244
- // `workflow` value falls through unrendered this switch only
245
- // owns the 'plan' workflow's two steps.
288
+ // never wire; ruling 3 — feed chrome, never a canvas) keeps its own
289
+ // rendering a manifest/synthesis summary is feed prose, not a
290
+ // pipeline step. Every OTHER workflow's step is a one-shot attempt
291
+ // marker (`@adia-ai/agent`'s workflow.ts emits it once, after the
292
+ // step already ran) — rendered as an immediately-settled step in
293
+ // the turn's reasoning viewer (gh#667).
246
294
  if (event.workflow === 'plan' && event.step === 'manifest') {
247
295
  chatShell.appendMessage?.({ role: 'assistant', content: describePlanManifest(event.data), render: true });
248
296
  chatShell.appendMessage?.({ role: 'assistant', content: '' });
249
297
  } else if (event.workflow === 'plan' && event.step === 'synthesis') {
250
298
  chatShell.appendMessage?.({ role: 'assistant', content: describeTurnSynthesis(event.data), render: true });
251
299
  chatShell.appendMessage?.({ role: 'assistant', content: '' });
300
+ } else {
301
+ const r = ensureReasoning();
302
+ if (r) {
303
+ const id = `${event.workflow}:${event.step}:${event.data?.round ?? 1}`;
304
+ const label = `${event.workflow}: ${event.step}`;
305
+ r.addStep({ id, label });
306
+ if (event.data?.accepted) r.completeStep(id);
307
+ else r.failStep(id, event.data?.reason ?? 'fell through');
308
+ }
252
309
  }
253
310
  break;
254
311
  }
255
312
 
256
- case 'tool_use':
257
- chatShell.appendMessage?.({
258
- role: 'assistant',
259
- content: `🔧 \`${event.name}(${JSON.stringify(event.input)})\``,
260
- render: true,
261
- });
262
- chatShell.appendMessage?.({ role: 'assistant', content: '' });
313
+ case 'tool_use': {
314
+ const r = ensureReasoning();
315
+ if (r) {
316
+ const label = `${event.name}(${JSON.stringify(event.input)})`;
317
+ if (findStepEntry(event.id)) r.updateStep(event.id, { label });
318
+ else r.addStep({ id: event.id, label });
319
+ }
263
320
  break;
321
+ }
264
322
 
265
- case 'tool_result':
266
- chatShell.appendMessage?.({
267
- role: event.isError ? 'error' : 'assistant',
268
- content: `↩ \`${event.name}\` ${String(event.output).slice(0, 300)}`,
269
- render: true,
270
- });
271
- chatShell.appendMessage?.({ role: 'assistant', content: '' });
323
+ case 'tool_input_delta': {
324
+ // Partial args — render progress on the running chip only; never
325
+ // executes off it (README's own rule — the matching tool_use is
326
+ // the call). Cheap upsert since addStep/updateStep already exist.
327
+ const r = ensureReasoning();
328
+ if (r) {
329
+ const label = `${event.name}(${event.partial})`;
330
+ if (findStepEntry(event.id)) r.updateStep(event.id, { label });
331
+ else r.addStep({ id: event.id, label });
332
+ }
272
333
  break;
334
+ }
335
+
336
+ case 'tool_result': {
337
+ const r = ensureReasoning();
338
+ if (r) {
339
+ const outcome = String(event.output).slice(0, 300);
340
+ if (event.isError) {
341
+ r.failStep(event.id, outcome);
342
+ } else {
343
+ r.updateStep(event.id, { outcomes: [outcome] });
344
+ r.completeStep(event.id);
345
+ }
346
+ }
347
+ break;
348
+ }
349
+
350
+ case 'guardrail': {
351
+ const r = ensureReasoning();
352
+ const verb = event.action === 'deny' ? 'blocked' : 'rewrote';
353
+ const target = event.toolName ? `tool call "${event.toolName}"` : 'the response';
354
+ r?.addThought?.(`Guardrail "${event.name}" ${verb} ${target}${event.reason ? ` — ${event.reason}` : ''}`);
355
+ break;
356
+ }
357
+
358
+ case 'thinking': {
359
+ const r = ensureReasoning();
360
+ r?.addThought?.(event.text);
361
+ break;
362
+ }
363
+
364
+ // 'memory' and 'trace' stay render-silent (see this module's header
365
+ // comment + the chat README's event table) — they fall to `default`.
273
366
 
274
367
  case 'error':
368
+ reasoning?.fail?.(event.error?.message ?? String(event.error));
275
369
  chatShell.appendMessage?.({ role: 'error', content: event.error?.message ?? String(event.error) });
276
370
  chatShell.stopStreaming?.();
277
371
  break;
278
372
 
279
373
  case 'done':
374
+ if (reasoning) {
375
+ if (event.stopReason === 'guardrail_denied') reasoning.fail('Blocked by guardrail');
376
+ else reasoning.finish();
377
+ }
280
378
  chatShell.stopStreaming?.();
281
379
  break;
282
380
 
package/chat/index.js CHANGED
@@ -2,4 +2,5 @@ export { ChatShell } from './chat-shell/chat-shell.js';
2
2
  export { ChatThread } from './chat-thread/chat-thread.js';
3
3
  export { ChatComposer } from './chat-composer/chat-composer.js';
4
4
  export { ChatSidebar } from './chat-sidebar/chat-sidebar.js';
5
+ export { ChatEmpty } from './chat-empty/chat-empty.js';
5
6
  export { createSurfaceRegistry, wireAgentEvents, PROGRESS_LABELS } from './chat-surfaces/chat-surfaces.js';
@@ -47,4 +47,4 @@ var RS=Object.defineProperty;var ee=(i,e,t)=>()=>{if(t)throw t[0];try{return i&&
47
47
  `):t}async#f(){if(this.#o||this.#e)return;let e=this.#i,t=Zu(this.language);this.#o=!0;try{let n=await Promise.resolve().then(()=>(Au(),Xu)),r=null;try{r=(await n.importWithTimeout(n.languages[t],`lang-${t}`))?.extension??null}catch(s){this.dispatchEvent(new CustomEvent("language-load-error",{bubbles:!0,detail:{phase:"language",language:t,error:s}}))}if(e!==this.#i||!this.isConnected)return;this.#O(n,r,t)}catch(n){this.dispatchEvent(new CustomEvent("language-load-error",{bubbles:!0,detail:{phase:"core",error:n}})),console.warn("[code-ui] CodeMirror failed to load; staying on static fallback.",n)}finally{this.#o=!1}}#O(e,t,n){let{EditorState:r,EditorView:s,Compartment:o,lineNumbers:l,placeholder:a,syntaxHighlighting:h,history:c,historyKeymap:f,defaultKeymap:u,indentWithTab:d,keymap:O,adiaBaseTheme:m,adiaHighlightStyle:g,lintGutter:b,jsonLinter:S}=e,y=this.querySelector(":scope > pre"),C=y?y.querySelector("code")?.textContent??"":"",x=this.text||C||"";this.#l=x;let k=document.createElement("div");k.setAttribute("data-cm-mount",""),y?y.replaceWith(k):this.appendChild(k);let w=[m,h(g)];this.editable&&w.push(c(),O.of([{key:"Mod-s",run:E=>(this.#g(E),!0)},...u,...f,d]),s.updateListener.of(E=>{if(!E.docChanged)return;let j=E.state.doc.toString();this.#d(j),this.dispatchEvent(new CustomEvent("input",{bubbles:!0,detail:{value:j}}))}),s.domEventHandlers({focus:(E,j)=>(this.#n=j.state.doc.toString(),!1),blur:(E,j)=>{let N=j.state.doc.toString();return this.#n!==null&&N!==this.#n&&this.dispatchEvent(new CustomEvent("change",{bubbles:!0,detail:{value:N}})),this.#n=null,!1}})),this.#s=new o,w.push(this.#s.of(this.#u(s,r))),t&&w.push(t),this.lineNumbers&&w.push(l()),this.placeholder&&w.push(a(this.placeholder)),this.editable&&n==="json"&&w.push(S,b()),this.#e=new s({parent:k,state:r.create({doc:x,extensions:w})}),this.editable&&this.#d(x)}#u(e,t){return this.editable&&!this.disabled&&!this.readonly?[]:[e.editable.of(!1),t.readOnly.of(!0)]}async#p(){if(!this.#e||!this.#s)return;let e=await Promise.resolve().then(()=>(Au(),Xu));this.#e.dispatch({effects:this.#s.reconfigure(this.#u(e.EditorView,e.EditorState))})}get form(){return this.internals.form}get labels(){return this.internals.labels}get validity(){return this.internals.validity}get validationMessage(){return this.internals.validationMessage}get willValidate(){return this.internals.willValidate}checkValidity(){return this.internals.checkValidity()}reportValidity(){return this.internals.reportValidity()}#d(e){this.editable&&(e=e??this.value??"",this.internals.setFormValue(e),this.#m(e))}#m(e){return e=e??"",this.required&&!e.trim()?(this.internals.setValidity({valueMissing:!0},this.getAttribute("data-msg-required")||"This field is required.",this),this.setAttribute("aria-invalid","true"),!1):(this.internals.setValidity({}),this.removeAttribute("aria-invalid"),!0)}formResetCallback(){this.#e?this.#e.dispatch({changes:{from:0,to:this.#e.state.doc.length,insert:this.#l}}):this.text=this.#l}formDisabledCallback(e){this.disabled=e}formStateRestoreCallback(e,t){typeof e=="string"&&(this.#e?this.#e.dispatch({changes:{from:0,to:this.#e.state.doc.length,insert:e}}):this.text=e)}#g(e){this.dispatchEvent(new CustomEvent("save",{bubbles:!0,cancelable:!0,detail:{value:e.state.doc.toString()}}))}get value(){return this.#e?this.#e.state.doc.toString():this.querySelector(":scope > pre > code")?.textContent??this.text??""}set value(e){this.text=String(e??"")}#b(){let e=this.#e?this.#e.state.doc.toString():this.querySelector(":scope > pre > code")?.textContent??"";e&&navigator.clipboard.writeText(e).then(()=>{let t=this.querySelector(':scope > header [slot="copy"]');if(!t)return;let n=t.textContent;t.textContent="Copied!",this.#r!=null&&clearTimeout(this.#r),this.#r=setTimeout(()=>{this.#r=null,t.textContent=n},1500)})}render(){let e=this.querySelector(':scope > header [slot="label"]');if(e&&this.language&&(e.textContent=this.language),this.#p(),!this.inline&&this.text!=="")if(this.#e){if(this.editable&&this.#e.hasFocus)return;let t=this.#e.state.doc.toString();t!==this.text&&this.#e.dispatch({changes:{from:0,to:t.length,insert:this.text}})}else{let t=this.querySelector(":scope > pre > code");t&&t.textContent!==this.text&&(t.textContent=this.text)}}};is("code-ui",es);function Ru(i){return i.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}var Dv=0,jl=class extends an{static properties={streaming:{type:Boolean,default:!1,reflect:!0},provider:{type:String,default:"",reflect:!0},model:{type:String,default:"",reflect:!0},system:{type:String,default:"",reflect:!0},proxyUrl:{type:String,default:"",attribute:"proxy-url",reflect:!0},thinking:{type:Boolean,default:!1,reflect:!0}};static template=()=>null;#t=[];#r=null;#e=null;#i=null;#o=null;#n=null;#l="";get messages(){return[...this.#t]}get conversation(){return this.#t.filter(e=>e.role==="user"||e.role==="assistant").map(e=>e.html?{role:e.role,content:e.content,html:!0}:{role:e.role,content:e.content})}set conversation(e){this.clear();for(let t of e)this.appendMessage({role:t.role,content:t.content,render:!t.html,html:!!t.html})}set apiKey(e){this.#l=e}connected(){this.#e=this.querySelector("chat-thread"),this.#i=this.querySelector("chat-composer"),this.#o=this.querySelector("chat-empty"),this.#n=this.querySelector("chat-status"),this.#i?.addEventListener("composer-submit",this.#a)}disconnected(){this.#i?.removeEventListener("composer-submit",this.#a),this.abort()}#s(e,t){this.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:t}))}#a=e=>{if(this.streaming)return;let{text:t,model:n}=e.detail||{};t&&(this.#i.clear(),this.#s("submit",{text:t,model:n}),(this.proxyUrl||this.#l)&&this.send(t,n?{model:n}:{}))};appendMessage({role:e,content:t="",render:n=!1,html:r=!1}){let s=`msg_${++Dv}`;this.#t.push({id:s,role:e,content:t,html:r});let o=document.createElement("div");if(o.setAttribute("data-role",e),o.setAttribute("data-id",s),e==="user")o.innerHTML=`<div data-bubble>${Ru(t)}</div>`;else if(e==="assistant"){let l=n||r,a=r?t:n&&t?ql(t):Ru(t);o.innerHTML=`
48
48
  <span data-avatar>AI</span>
49
49
  <div data-bubble><div data-content>${a}</div>${l?"":"<span data-cursor></span>"}</div>
50
- `}else e==="error"&&(o.innerHTML=`<icon-ui name="warning"></icon-ui><span>${Ru(t)}</span>`);return this.#e?.appendChild(o),this.#h(),this.#s("message",{id:s,role:e,content:t}),o}appendChunk(e){let t=this.#t[this.#t.length-1];if(!t||t.role!=="assistant")return;t.content+=e;let n=this.#e?.querySelector("[data-role]:last-child [data-content]");n&&n.insertAdjacentText("beforeend",e),this.#h()}deleteMessage(e){let t=this.#t.findIndex(n=>n.id===e);t!==-1&&(this.#t.splice(t,1),this.#e?.querySelector(`[data-id="${e}"]`)?.remove())}clear(){if(this.#t.length=0,this.#e){let e=this.#o;this.#e.innerHTML="",e&&this.#e.appendChild(e)}this.#s("clear")}startStreaming(){this.streaming=!0,this.#i&&(this.#i.disabled=!0),this.#n&&(this.#n.textContent="Typing..."),this.#e?.tagName?.toLowerCase()==="chat-thread"&&(this.#e.streaming=!0)}stopStreaming(){this.streaming=!1,this.#i&&(this.#i.disabled=!1),this.#n&&(this.#n.textContent=""),this.#e?.tagName?.toLowerCase()==="chat-thread"&&(this.#e.streaming=!1),this.#e?.querySelector("[data-role]:last-child [data-cursor]")?.remove();let e=this.#t[this.#t.length-1];if(e?.role==="assistant"&&e.content&&!e.html){let t=this.#e?.querySelector("[data-role]:last-child [data-content]");t&&(t.innerHTML=ql(e.content),this.#c(t))}this.#i?.focus()}abort(){this.#r&&(this.#r.abort(),this.#r=null,this.#s("abort")),this.streaming&&this.stopStreaming()}async send(e,t={}){let n=t.model||this.model||this.#i?.model;if(!n)throw new Error("No model specified");this.appendMessage({role:"user",content:e}),this.appendMessage({role:"assistant",content:""}),this.startStreaming(),this.#r=new AbortController;try{let r={provider:this.provider||void 0,apiKey:this.#l||void 0,model:n,system:this.system||void 0,proxyUrl:this.proxyUrl||void 0,thinking:this.thinking||void 0,messages:this.conversation.slice(0,-1),signal:this.#r.signal,...t},s;for(let o of["../../../llm/index.js","../../llm/index.js"])try{({streamChat:s}=await import(new URL(o,import.meta.url).href));break}catch{}if(!s){this.appendMessage({role:"error",content:"@adia-ai/llm is not available \u2014 in a fresh checkout run the packages/llm build (see gh#527/gh#541)."}),this.#s("error",{error:new Error("llm build artifact missing")});return}for await(let o of s(r))o.type==="text"?(this.appendChunk(o.text),this.#s("chunk",{text:o.text,snapshot:o.snapshot})):o.type==="thinking"?this.#s("thinking",{text:o.text}):o.type==="done"?this.#s("done",{text:o.text,usage:o.usage,stopReason:o.stopReason}):o.type==="error"&&(this.appendMessage({role:"error",content:o.error.message}),this.#s("error",{error:o.error}))}catch(r){r.name!=="AbortError"&&(this.appendMessage({role:"error",content:r.message}),this.#s("error",{error:r}))}this.#r=null,this.stopStreaming()}export(){return{messages:this.#t.map(e=>({role:e.role,content:e.content})),model:this.model,system:this.system}}import(e){e.model&&(this.model=e.model),e.system&&(this.system=e.system),e.messages&&(this.conversation=e.messages)}#h(){let e=this.#e;e&&requestAnimationFrame(()=>{e.scrollTop=e.scrollHeight})}#c(e){for(let t of e.querySelectorAll("pre")){let n=t.querySelector("code");if(!n)continue;let r=n.getAttribute("data-lang")||"",s=document.createElement("code-ui");r&&s.setAttribute("language",r),s.textContent=n.textContent,t.replaceWith(s)}}};is("chat-shell",jl);export{jl as ChatShell};
50
+ `}else e==="error"&&(o.innerHTML=`<icon-ui name="warning"></icon-ui><span>${Ru(t)}</span>`);return this.#e?.appendChild(o),this.#h(),this.#s("message",{id:s,role:e,content:t}),o}appendChunk(e){let t=this.#t[this.#t.length-1];if(!t||t.role!=="assistant")return;t.content+=e;let n=this.#e?.querySelector("[data-role]:last-child [data-content]");n&&n.insertAdjacentText("beforeend",e),this.#h()}setStreamedText(e){let t=this.#t[this.#t.length-1];if(!t||t.role!=="assistant")return;t.content=e;let n=this.#e?.querySelector("[data-role]:last-child [data-content]");n&&(n.textContent=e),this.#h()}deleteMessage(e){let t=this.#t.findIndex(n=>n.id===e);t!==-1&&(this.#t.splice(t,1),this.#e?.querySelector(`[data-id="${e}"]`)?.remove())}clear(){if(this.#t.length=0,this.#e){let e=this.#o;this.#e.innerHTML="",e&&this.#e.appendChild(e)}this.#s("clear")}startStreaming(){this.streaming=!0,this.#i&&(this.#i.disabled=!0),this.#n&&(this.#n.textContent="Typing..."),this.#e?.tagName?.toLowerCase()==="chat-thread"&&(this.#e.streaming=!0)}stopStreaming(){this.streaming=!1,this.#i&&(this.#i.disabled=!1),this.#n&&(this.#n.textContent=""),this.#e?.tagName?.toLowerCase()==="chat-thread"&&(this.#e.streaming=!1),this.#e?.querySelector("[data-role]:last-child [data-cursor]")?.remove();let e=this.#t[this.#t.length-1];if(e?.role==="assistant"&&e.content&&!e.html){let t=this.#e?.querySelector("[data-role]:last-child [data-content]");t&&(t.innerHTML=ql(e.content),this.#c(t))}this.#i?.focus()}abort(){this.#r&&(this.#r.abort(),this.#r=null,this.#s("abort")),this.streaming&&this.stopStreaming()}async send(e,t={}){let n=t.model||this.model||this.#i?.model;if(!n)throw new Error("No model specified");this.appendMessage({role:"user",content:e}),this.appendMessage({role:"assistant",content:""}),this.startStreaming(),this.#r=new AbortController;try{let r={provider:this.provider||void 0,apiKey:this.#l||void 0,model:n,system:this.system||void 0,proxyUrl:this.proxyUrl||void 0,thinking:this.thinking||void 0,messages:this.conversation.slice(0,-1),signal:this.#r.signal,...t},s;for(let o of["../../../llm/index.js","../../llm/index.js"])try{({streamChat:s}=await import(new URL(o,import.meta.url).href));break}catch{}if(!s){this.appendMessage({role:"error",content:"@adia-ai/llm is not available \u2014 in a fresh checkout run the packages/llm build (see gh#527/gh#541)."}),this.#s("error",{error:new Error("llm build artifact missing")});return}for await(let o of s(r))o.type==="text"?(this.appendChunk(o.text),this.#s("chunk",{text:o.text,snapshot:o.snapshot})):o.type==="thinking"?this.#s("thinking",{text:o.text}):o.type==="done"?this.#s("done",{text:o.text,usage:o.usage,stopReason:o.stopReason}):o.type==="error"&&(this.appendMessage({role:"error",content:o.error.message}),this.#s("error",{error:o.error}))}catch(r){r.name!=="AbortError"&&(this.appendMessage({role:"error",content:r.message}),this.#s("error",{error:r}))}this.#r=null,this.stopStreaming()}export(){return{messages:this.#t.map(e=>({role:e.role,content:e.content})),model:this.model,system:this.system}}import(e){e.model&&(this.model=e.model),e.system&&(this.system=e.system),e.messages&&(this.conversation=e.messages)}#h(){let e=this.#e;e&&requestAnimationFrame(()=>{e.scrollTop=e.scrollHeight})}#c(e){for(let t of e.querySelectorAll("pre")){let n=t.querySelector("code");if(!n)continue;let r=n.getAttribute("data-lang")||"",s=document.createElement("code-ui");r&&s.setAttribute("language",r),s.textContent=n.textContent,t.replaceWith(s)}}};is("chat-shell",jl);export{jl as ChatShell};