@adia-ai/web-modules 0.8.28 → 0.8.30

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,26 @@
1
1
  # Changelog — @adia-ai/web-modules
2
2
 
3
+ ## [0.8.30] — 2026-08-07
4
+
5
+ ### Added
6
+ - **`chatShell.setStreamedText(text)`** — replaces the streaming assistant message's text outright, the counterpart to `appendChunk` for a producer that revises what it already streamed.
7
+
8
+ ### Fixed
9
+ - **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.
10
+
11
+ ### Maintenance
12
+ - **`billing/` touched in this release window** (1 file(s), e.g. `invoice-history/invoice-history.class.js`) — carried by the entries above.
13
+ - **`chat/` touched in this release window** (5 file(s), e.g. `chat/README.md`) — carried by the entries above.
14
+ - **`dist/` bundles rebuilt** in this cut's window (2 file(s)) — regenerated from the source changes described above, not independent edits.
15
+
16
+ ## [0.8.29] — 2026-08-06
17
+
18
+ ### Added
19
+ - **chat-surfaces renders plan-envelope chrome** — `wireAgentEvents` gains a `step` case rendering the turn's surface manifest and "what changed" synthesis as feed chrome (workflow:'plan'), riding the existing AgentEvent `step` kind — no event-vocabulary change (gh#648, PR #654).
20
+
21
+ ### Maintenance
22
+ - **`dist/` bundles rebuilt** in this cut's window (1 file(s)) — regenerated from the source changes described above, not independent edits.
23
+
3
24
  ## [0.8.28] — 2026-08-05
4
25
 
5
26
  ### Fixed
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`.
@@ -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;
@@ -39,9 +39,28 @@ export function createSurfaceRegistry(opts: {
39
39
  /** CHAT-HARNESS law 3 — code-owned progress labels, never model text. */
40
40
  export const PROGRESS_LABELS: Record<string, string>;
41
41
 
42
+ /** Plan-envelope feed chrome (gh#648) — code-owned sentence around a
43
+ * `PlanEnvelope`'s per-surface rationale. Ruling 3: feed chrome, never a canvas. */
44
+ export function describePlanManifest(plan: {
45
+ turnId: string;
46
+ surfaces: { surfaceId: string; rationale: string }[];
47
+ }): string;
48
+
49
+ /** Plan-envelope feed chrome (gh#648) — code-owned sentence around a
50
+ * `TurnSynthesis`'s new/updated/closed surface lists. */
51
+ export function describeTurnSynthesis(synthesis: {
52
+ turnId: string;
53
+ new: string[];
54
+ updated: string[];
55
+ closed: string[];
56
+ }): string;
57
+
42
58
  export function wireAgentEvents(
43
59
  chatShell: Element & {
44
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;
45
64
  appendMessage?(msg: { role: string; content?: string; render?: boolean }): Element;
46
65
  startStreaming?(): void;
47
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';
@@ -163,6 +178,38 @@ export const PROGRESS_LABELS = {
163
178
  done: 'Done',
164
179
  };
165
180
 
181
+ /**
182
+ * Plan-envelope feed chrome (gh#648, docs/ORCHESTRATION.md §Plan envelope
183
+ * ruling 3 — "what changed" is feed chrome, never a canvas). These render
184
+ * from the `{ type: 'step', workflow: 'plan', step: 'manifest'|'synthesis' }`
185
+ * AgentEvent — the EXISTING `step` kind (`@adia-ai/agent`'s `AgentEvent`
186
+ * union already carries `workflow`/`step`/`data`), so no closed-vocabulary
187
+ * `type` amendment is needed to carry this: `workflow: 'plan'` is just a
188
+ * new value inside an already-open string field, the same way
189
+ * `packages/agent/src/workflow.ts`'s own step events use `workflow` for a
190
+ * different named workflow.
191
+ *
192
+ * Code-owned text (CHAT-HARNESS law 3's spirit, applied to `step` the way
193
+ * PROGRESS_LABELS applies it to `progress`): the rationale strings inside
194
+ * a PlanEnvelope ARE planner-authored copy (not raw model text — R-O10's
195
+ * `rationale` is a structured field, one line per surface), so they render
196
+ * verbatim; the surrounding sentence is code-owned.
197
+ */
198
+ export function describePlanManifest(plan) {
199
+ const n = plan.surfaces.length;
200
+ const list = plan.surfaces.map((s) => `${s.surfaceId} — ${s.rationale}`).join('; ');
201
+ return `Planning ${n} surface${n === 1 ? '' : 's'}: ${list}`;
202
+ }
203
+
204
+ const SYNTHESIS_LABELS = { new: 'New', updated: 'Updated', closed: 'Closed' };
205
+
206
+ export function describeTurnSynthesis(synthesis) {
207
+ const parts = ['new', 'updated', 'closed']
208
+ .filter((key) => synthesis[key].length > 0)
209
+ .map((key) => `${SYNTHESIS_LABELS[key]}: ${synthesis[key].join(', ')}`);
210
+ return parts.length ? `What changed — ${parts.join(' · ')}` : 'What changed — nothing this turn';
211
+ }
212
+
166
213
  /**
167
214
  * Consumes ONE `AsyncIterable<AgentEvent>` for the turn already opened on
168
215
  * `chatShell` (the caller has already appended the user message and an
@@ -188,10 +235,40 @@ export async function wireAgentEvents(chatShell, registry, opts) {
188
235
  return thread?.querySelector('[data-role="assistant"]:last-child [data-bubble]') ?? null;
189
236
  };
190
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
+
191
261
  for await (const event of events) {
192
262
  switch (event.type) {
193
263
  case 'text':
194
- 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);
195
272
  break;
196
273
 
197
274
  case 'progress': {
@@ -206,30 +283,98 @@ export async function wireAgentEvents(chatShell, registry, opts) {
206
283
  break;
207
284
  }
208
285
 
209
- case 'tool_use':
210
- chatShell.appendMessage?.({
211
- role: 'assistant',
212
- content: `🔧 \`${event.name}(${JSON.stringify(event.input)})\``,
213
- render: true,
214
- });
215
- chatShell.appendMessage?.({ role: 'assistant', content: '' });
286
+ case 'step': {
287
+ // Plan-envelope chrome (gh#648, ruling 1 — harness-level progress,
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).
294
+ if (event.workflow === 'plan' && event.step === 'manifest') {
295
+ chatShell.appendMessage?.({ role: 'assistant', content: describePlanManifest(event.data), render: true });
296
+ chatShell.appendMessage?.({ role: 'assistant', content: '' });
297
+ } else if (event.workflow === 'plan' && event.step === 'synthesis') {
298
+ chatShell.appendMessage?.({ role: 'assistant', content: describeTurnSynthesis(event.data), render: true });
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
+ }
309
+ }
310
+ break;
311
+ }
312
+
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
+ }
216
320
  break;
321
+ }
217
322
 
218
- case 'tool_result':
219
- chatShell.appendMessage?.({
220
- role: event.isError ? 'error' : 'assistant',
221
- content: `↩ \`${event.name}\` ${String(event.output).slice(0, 300)}`,
222
- render: true,
223
- });
224
- 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
+ }
225
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`.
226
366
 
227
367
  case 'error':
368
+ reasoning?.fail?.(event.error?.message ?? String(event.error));
228
369
  chatShell.appendMessage?.({ role: 'error', content: event.error?.message ?? String(event.error) });
229
370
  chatShell.stopStreaming?.();
230
371
  break;
231
372
 
232
373
  case 'done':
374
+ if (reasoning) {
375
+ if (event.stopReason === 'guardrail_denied') reasoning.fail('Blocked by guardrail');
376
+ else reasoning.finish();
377
+ }
233
378
  chatShell.stopStreaming?.();
234
379
  break;
235
380
 
@@ -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};