@smooai/chat-widget 0.11.0 → 0.13.0

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/dist/index.js CHANGED
@@ -342,6 +342,7 @@ function resolveConfig(config) {
342
342
  collectConsent: config.collectConsent ?? true,
343
343
  allowChatRestore: config.allowChatRestore ?? true,
344
344
  allowAnonymous: config.allowAnonymous ?? false,
345
+ showToolActivity: config.showToolActivity ?? false,
345
346
  theme: {
346
347
  text: theme.text ?? "#f8fafc",
347
348
  background: theme.background ?? "#040d30",
@@ -735,6 +736,13 @@ function httpBaseFromWsEndpoint(endpoint) {
735
736
  return null;
736
737
  }
737
738
  }
739
+ /**
740
+ * The Rich-Interaction render capabilities this widget declares at session
741
+ * create (`supports`). Must stay aligned with the card registry in
742
+ * `element.ts` (`INTERACTION_CARDS`) — registering a card IS declaring its
743
+ * capability; a test asserts the two match.
744
+ */
745
+ const SUPPORTED_INTERACTION_CAPABILITIES = ["identity_form"];
738
746
  /** Pull the final assistant text out of an `eventual_response` data payload. */
739
747
  function extractFinalText(response) {
740
748
  if (!response || typeof response !== "object") return null;
@@ -800,6 +808,59 @@ function wireMessageToChat(m, idx) {
800
808
  streaming: false
801
809
  };
802
810
  }
811
+ let toolSeq = 0;
812
+ const nextToolId = () => `tool-${++toolSeq}`;
813
+ /** Grow the trailing text block, or open a new one if the last block was a tool. */
814
+ function growTextBlock(blocks, text) {
815
+ if (!text) return;
816
+ const last = blocks[blocks.length - 1];
817
+ if (last && last.kind === "text") last.text += text;
818
+ else blocks.push({
819
+ kind: "text",
820
+ text
821
+ });
822
+ }
823
+ /**
824
+ * Fold a `stream_chunk` node-state into the ordered block list, returning `true`
825
+ * when the chunk carried tool activity.
826
+ *
827
+ * Tool activity rides `state.rawResponse.toolCall` / `state.rawResponse.toolResult`
828
+ * — **NOT** `state.toolResult`. Reading the wrong path leaves every chip stuck on
829
+ * "running…" forever (the exact bug the daemon SPA hit and this mirror avoids).
830
+ */
831
+ function applyToolChunk(blocks, state) {
832
+ const raw = state?.rawResponse;
833
+ if (!raw || typeof raw !== "object") return false;
834
+ const call = raw.toolCall;
835
+ const res = raw.toolResult;
836
+ if (call) {
837
+ const args = typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments ?? {});
838
+ blocks.push({
839
+ kind: "tool",
840
+ tool: {
841
+ id: nextToolId(),
842
+ name: call.name ?? "",
843
+ args,
844
+ done: false
845
+ }
846
+ });
847
+ return true;
848
+ }
849
+ if (res) {
850
+ const result = typeof res.result === "string" ? res.result : JSON.stringify(res.result ?? "");
851
+ for (let i = blocks.length - 1; i >= 0; i--) {
852
+ const b = blocks[i];
853
+ if (b && b.kind === "tool" && b.tool.name === (res.name ?? "") && !b.tool.done) {
854
+ b.tool.done = true;
855
+ b.tool.isError = !!res.isError;
856
+ b.tool.result = result;
857
+ break;
858
+ }
859
+ }
860
+ return true;
861
+ }
862
+ return false;
863
+ }
803
864
  var ConversationController = class {
804
865
  constructor(config, events, store) {
805
866
  _defineProperty(this, "config", void 0);
@@ -812,6 +873,7 @@ var ConversationController = class {
812
873
  _defineProperty(this, "seq", 0);
813
874
  _defineProperty(this, "activeRequestId", null);
814
875
  _defineProperty(this, "interrupt", null);
876
+ _defineProperty(this, "pendingInteractionValues", null);
815
877
  _defineProperty(this, "identityRestore", { phase: "idle" });
816
878
  _defineProperty(this, "resumeAttempted", false);
817
879
  _defineProperty(this, "httpBase", void 0);
@@ -890,6 +952,40 @@ var ConversationController = class {
890
952
  });
891
953
  this.setInterrupt(null);
892
954
  }
955
+ /**
956
+ * Submit a Rich Interaction card to resume the parked turn. The server
957
+ * routes to the kind's validator: invalid values come back as an
958
+ * `interaction_invalid` event (the card re-renders with per-field errors —
959
+ * the turn stays parked); a valid submit is acked and the turn resumes.
960
+ * No-op if not awaiting an interaction.
961
+ */
962
+ submitInteraction(values) {
963
+ if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== "interaction") return;
964
+ this.pendingInteractionValues = {
965
+ kind: this.interrupt.interactionKind,
966
+ values
967
+ };
968
+ this.client.submitInteraction({
969
+ sessionId: this.sessionId,
970
+ requestId: this.activeRequestId,
971
+ interactionId: this.interrupt.interactionId,
972
+ kind: this.interrupt.interactionKind,
973
+ values
974
+ });
975
+ }
976
+ /** Decline the pending Rich Interaction; the agent continues without it. */
977
+ declineInteraction() {
978
+ if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== "interaction") return;
979
+ this.client.submitInteraction({
980
+ sessionId: this.sessionId,
981
+ requestId: this.activeRequestId,
982
+ interactionId: this.interrupt.interactionId,
983
+ kind: this.interrupt.interactionKind,
984
+ declined: true
985
+ });
986
+ this.pendingInteractionValues = null;
987
+ this.setInterrupt(null);
988
+ }
893
989
  nextId(prefix) {
894
990
  this.seq += 1;
895
991
  return `${prefix}-${this.seq}-${Date.now().toString(36)}`;
@@ -1054,6 +1150,7 @@ var ConversationController = class {
1054
1150
  userName: state.identity.name,
1055
1151
  userEmail: state.identity.email,
1056
1152
  browserFingerprint: this.fingerprint(),
1153
+ supports: [...SUPPORTED_INTERACTION_CAPABILITIES],
1057
1154
  ...metadata ? { metadata } : {}
1058
1155
  });
1059
1156
  this.sessionId = session.sessionId;
@@ -1110,11 +1207,13 @@ var ConversationController = class {
1110
1207
  text: trimmed,
1111
1208
  streaming: false
1112
1209
  });
1210
+ const showTools = this.config.showToolActivity === true;
1113
1211
  const assistant = {
1114
1212
  id: this.nextId("a"),
1115
1213
  role: "assistant",
1116
1214
  text: "",
1117
- streaming: true
1215
+ streaming: true,
1216
+ blocks: showTools ? [] : void 0
1118
1217
  };
1119
1218
  this.messages.push(assistant);
1120
1219
  this.emitMessages();
@@ -1129,8 +1228,11 @@ var ConversationController = class {
1129
1228
  const token = event.token ?? event.data?.token ?? "";
1130
1229
  if (token) {
1131
1230
  assistant.text += token;
1231
+ if (showTools && assistant.blocks) growTextBlock(assistant.blocks, token);
1132
1232
  this.emitMessages();
1133
1233
  }
1234
+ } else if (showTools && event.type === "stream_chunk") {
1235
+ if (assistant.blocks && applyToolChunk(assistant.blocks, event.data?.state)) this.emitMessages();
1134
1236
  } else this.handleTurnEvent(event);
1135
1237
  const inner = (await turn).data?.data;
1136
1238
  const finalText = extractFinalText(inner?.response);
@@ -1140,6 +1242,7 @@ var ConversationController = class {
1140
1242
  if (citations.length > 0) assistant.citations = citations;
1141
1243
  const suggestions = extractSuggestions(inner?.response);
1142
1244
  if (suggestions.length > 0) assistant.suggestions = suggestions;
1245
+ if (assistant.blocks && !assistant.blocks.some((b) => b.kind === "tool")) assistant.blocks = void 0;
1143
1246
  assistant.streaming = false;
1144
1247
  this.emitMessages();
1145
1248
  } catch (err) {
@@ -1196,6 +1299,55 @@ var ConversationController = class {
1196
1299
  actionDescription: str(inner.actionDescription)
1197
1300
  });
1198
1301
  break;
1302
+ case "interaction_required": {
1303
+ const interactionId = str(inner.interactionId);
1304
+ const kind = str(inner.kind);
1305
+ const spec = inner.spec && typeof inner.spec === "object" ? inner.spec : {};
1306
+ if (!interactionId || !kind) break;
1307
+ this.pendingInteractionValues = null;
1308
+ this.setInterrupt({
1309
+ kind: "interaction",
1310
+ interactionId,
1311
+ interactionKind: kind,
1312
+ spec,
1313
+ reason: str(inner.reason)
1314
+ });
1315
+ break;
1316
+ }
1317
+ case "interaction_invalid":
1318
+ if (this.interrupt?.kind === "interaction" && this.interrupt.interactionId === str(inner.interactionId)) {
1319
+ const errors = [];
1320
+ if (Array.isArray(inner.errors)) for (const e of inner.errors) {
1321
+ if (!e || typeof e !== "object") continue;
1322
+ const o = e;
1323
+ const field = str(o.field);
1324
+ if (field) errors.push({
1325
+ field,
1326
+ message: str(o.message) ?? "Invalid value"
1327
+ });
1328
+ }
1329
+ this.pendingInteractionValues = null;
1330
+ this.setInterrupt({
1331
+ ...this.interrupt,
1332
+ errors
1333
+ });
1334
+ }
1335
+ break;
1336
+ case "immediate_response":
1337
+ if (this.interrupt?.kind === "interaction") {
1338
+ const pending = this.pendingInteractionValues;
1339
+ if (pending && pending.kind === "identity_intake") {
1340
+ const v = pending.values;
1341
+ this.store.getState().mergeIdentity({
1342
+ name: v.name,
1343
+ email: v.email,
1344
+ phone: v.phone
1345
+ });
1346
+ }
1347
+ this.pendingInteractionValues = null;
1348
+ this.setInterrupt(null);
1349
+ }
1350
+ break;
1199
1351
  default: break;
1200
1352
  }
1201
1353
  }
@@ -1402,6 +1554,7 @@ function buildStyles(theme, mode = "popover") {
1402
1554
  --sac-bg: ${theme.background};
1403
1555
  --sac-primary: ${theme.primary};
1404
1556
  --sac-primary-text: ${theme.primaryText};
1557
+ --sac-secondary: ${theme.secondary};
1405
1558
  --sac-assistant-bubble: ${theme.assistantBubble};
1406
1559
  --sac-assistant-bubble-text: ${theme.assistantBubbleText};
1407
1560
  --sac-user-bubble: ${theme.userBubble};
@@ -1714,6 +1867,47 @@ ${mode === "fullpage" ? `
1714
1867
  }
1715
1868
  @keyframes sac-blink { to { opacity: 0 } }
1716
1869
 
1870
+ /* Interleaved tool-activity strip (gated by showToolActivity): prose bubbles
1871
+ and inline tool chips stacked in the order the model produced them. */
1872
+ .blocks { display: flex; flex-direction: column; align-items: flex-start; gap: 7px; }
1873
+ .blocks .bubble { align-self: flex-start; }
1874
+ .toolchip {
1875
+ display: inline-flex;
1876
+ align-items: center;
1877
+ gap: 6px;
1878
+ max-width: 100%;
1879
+ padding: 5px 10px;
1880
+ border-radius: 99px;
1881
+ font-size: 12px;
1882
+ line-height: 1.3;
1883
+ color: color-mix(in srgb, var(--sac-text) 78%, transparent);
1884
+ background: color-mix(in srgb, var(--sac-text) 6%, transparent);
1885
+ border: 1px solid color-mix(in srgb, var(--sac-text) 12%, transparent);
1886
+ animation: sac-msg-in .3s var(--sac-ease) both;
1887
+ }
1888
+ .toolchip .ti { display: inline-flex; flex: none; opacity: .8; }
1889
+ .toolchip .ti svg { width: 13px; height: 13px; }
1890
+ .toolchip .tn { font-weight: 600; letter-spacing: .01em; }
1891
+ .toolchip .ts { opacity: .7; }
1892
+ .toolchip .ta {
1893
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1894
+ font-size: 11px;
1895
+ opacity: .6;
1896
+ overflow: hidden;
1897
+ text-overflow: ellipsis;
1898
+ white-space: nowrap;
1899
+ min-width: 0;
1900
+ }
1901
+ .toolchip.running { border-color: color-mix(in srgb, var(--sac-primary) 45%, transparent); }
1902
+ .toolchip.running .ts { color: var(--sac-primary); opacity: 1; animation: sac-typing 1.3s ease-in-out infinite; }
1903
+ .toolchip.done .ts::before { content: '✓ '; }
1904
+ .toolchip.error {
1905
+ color: var(--sac-secondary);
1906
+ border-color: color-mix(in srgb, var(--sac-secondary) 50%, transparent);
1907
+ background: color-mix(in srgb, var(--sac-secondary) 10%, transparent);
1908
+ }
1909
+ .toolchip.error .ts::before { content: '! '; }
1910
+
1717
1911
  /* ─────────────── Rendered markdown (assistant bubbles / snippets) ─────────── */
1718
1912
  /* The renderer (markdown.ts) emits a small allowlisted set of tags; these rules
1719
1913
  keep them legible inside the tight Aurora-Glass bubble + citation card. */
@@ -2008,6 +2202,9 @@ ${mode === "fullpage" ? `
2008
2202
  border-color: color-mix(in srgb, var(--sac-primary) 60%, transparent);
2009
2203
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--sac-primary) 14%, transparent);
2010
2204
  }
2205
+ .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
2206
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
2207
+ .int-form .int-error { margin-top: 2px; }
2011
2208
  .int-btn {
2012
2209
  border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
2013
2210
  background: var(--sac-surface-2);
@@ -2027,8 +2224,14 @@ ${mode === "fullpage" ? `
2027
2224
  color: var(--sac-primary-text);
2028
2225
  box-shadow: 0 6px 14px -6px color-mix(in srgb, var(--sac-primary) 65%, transparent);
2029
2226
  }
2030
- .int-row .int-btn { flex: 1; }
2031
- .int-row .int-input + .int-btn { flex: 0 0 auto; }
2227
+ .int-row .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
2228
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
2229
+ .int-form .int-error { margin-top: 2px; }
2230
+ .int-btn { flex: 1; }
2231
+ .int-row .int-input + .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
2232
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
2233
+ .int-form .int-error { margin-top: 2px; }
2234
+ .int-btn { flex: 0 0 auto; }
2032
2235
  .int-error { margin-top: 8px; font-size: 12px; color: #f87171; }
2033
2236
  .int-card { position: relative; }
2034
2237
  .int-close {
@@ -2147,7 +2350,8 @@ const OBSERVED = [
2147
2350
  "greeting",
2148
2351
  "start-open",
2149
2352
  "mode",
2150
- "hide-branding"
2353
+ "hide-branding",
2354
+ "show-tool-activity"
2151
2355
  ];
2152
2356
  /**
2153
2357
  * Inline SVG icons (static, trusted strings — never interpolated with user data).
@@ -2166,9 +2370,141 @@ const ICON = {
2166
2370
  chev: `<svg width="11" height="11" viewBox="0 0 24 24" fill="none"><path d="m9 6 6 6-6 6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
2167
2371
  /** OTP interrupt — a padlock. */
2168
2372
  lock: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="5" y="10.5" width="14" height="9.5" rx="2.2" stroke="currentColor" stroke-width="1.7"/><path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" stroke="currentColor" stroke-width="1.7"/></svg>`,
2373
+ /** Identity-intake interrupt — a person. */
2374
+ user: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="8.2" r="3.4" stroke="currentColor" stroke-width="1.7"/><path d="M5.5 19.5c.8-3.1 3.4-4.8 6.5-4.8s5.7 1.7 6.5 4.8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>`,
2169
2375
  /** Tool-confirmation interrupt — a shield. */
2170
- shield: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3 5 6v5c0 4.4 3 7.2 7 8.5 4-1.3 7-4.1 7-8.5V6l-7-3Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="m9 11.5 2 2 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>`
2376
+ shield: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3 5 6v5c0 4.4 3 7.2 7 8.5 4-1.3 7-4.1 7-8.5V6l-7-3Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="m9 11.5 2 2 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
2377
+ /** Tool-activity chip — a wrench. */
2378
+ tool: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.7 6.3a3.5 3.5 0 0 0-4.6 4.3l-5 5a1.6 1.6 0 0 0 2.3 2.3l5-5a3.5 3.5 0 0 0 4.3-4.6l-2 2-1.7-.3-.3-1.7 2-2Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`
2171
2379
  };
2380
+ /**
2381
+ * The identity_intake card: the fields the agent requested (pre-chat form field
2382
+ * pattern — same classes, same phone formatting), per-field server errors, a
2383
+ * submit and a decline affordance. Server-supplied text (reason, labels, error
2384
+ * messages) is set via `textContent` — never innerHTML.
2385
+ */
2386
+ function buildIdentityIntakeCard(it, ctx) {
2387
+ const form = document.createElement("form");
2388
+ form.className = "int-form";
2389
+ form.noValidate = true;
2390
+ if (it.reason) {
2391
+ const desc = document.createElement("div");
2392
+ desc.className = "int-desc";
2393
+ desc.textContent = it.reason;
2394
+ form.appendChild(desc);
2395
+ }
2396
+ const DEFAULTS = {
2397
+ name: {
2398
+ label: "Name",
2399
+ type: "text",
2400
+ autocomplete: "name"
2401
+ },
2402
+ email: {
2403
+ label: "Email",
2404
+ type: "email",
2405
+ autocomplete: "email"
2406
+ },
2407
+ phone: {
2408
+ label: "Phone",
2409
+ type: "tel",
2410
+ autocomplete: "tel"
2411
+ }
2412
+ };
2413
+ const rawFields = Array.isArray(it.spec.fields) ? it.spec.fields : [];
2414
+ const fields = [];
2415
+ for (const f of rawFields) {
2416
+ if (!f || typeof f !== "object") continue;
2417
+ const o = f;
2418
+ const key = typeof o.key === "string" ? o.key : "";
2419
+ if (key !== "name" && key !== "email" && key !== "phone") continue;
2420
+ fields.push({
2421
+ key,
2422
+ required: o.required === true,
2423
+ label: typeof o.label === "string" ? o.label : void 0
2424
+ });
2425
+ }
2426
+ if (fields.length === 0) fields.push({
2427
+ key: "email",
2428
+ required: true
2429
+ });
2430
+ for (const f of fields) {
2431
+ const d = DEFAULTS[f.key];
2432
+ const label = document.createElement("label");
2433
+ label.className = "pc-field";
2434
+ const caption = document.createElement("span");
2435
+ caption.textContent = f.label ?? d.label;
2436
+ const input = document.createElement("input");
2437
+ input.name = f.key;
2438
+ input.type = d.type;
2439
+ input.setAttribute("autocomplete", d.autocomplete);
2440
+ input.required = f.required;
2441
+ const prefill = ctx.prefill[f.key];
2442
+ if (prefill) input.value = prefill;
2443
+ label.append(caption, input);
2444
+ if (f.key === "phone") {
2445
+ const hint = document.createElement("span");
2446
+ hint.className = "pc-hint";
2447
+ hint.setAttribute("aria-live", "polite");
2448
+ label.appendChild(hint);
2449
+ }
2450
+ const serverError = it.errors?.find((e) => e.field === f.key);
2451
+ if (serverError) {
2452
+ label.classList.add("invalid");
2453
+ const err = document.createElement("span");
2454
+ err.className = "int-error";
2455
+ err.textContent = serverError.message;
2456
+ label.appendChild(err);
2457
+ }
2458
+ form.appendChild(label);
2459
+ }
2460
+ const row = document.createElement("div");
2461
+ row.className = "int-row";
2462
+ const decline = document.createElement("button");
2463
+ decline.className = "int-btn";
2464
+ decline.type = "button";
2465
+ decline.textContent = "No thanks";
2466
+ decline.addEventListener("click", () => ctx.decline());
2467
+ const share = document.createElement("button");
2468
+ share.className = "int-btn primary";
2469
+ share.type = "submit";
2470
+ share.textContent = "Share details";
2471
+ row.append(decline, share);
2472
+ form.appendChild(row);
2473
+ form.addEventListener("submit", (ev) => {
2474
+ ev.preventDefault();
2475
+ if (!form.reportValidity()) return;
2476
+ const data = new FormData(form);
2477
+ const val = (k) => data.get(k)?.trim() || void 0;
2478
+ const rawPhone = val("phone");
2479
+ const phoneInput = form.querySelector("input[name=\"phone\"]");
2480
+ if (rawPhone && phoneInput && !isValidPhoneNumber(rawPhone, PHONE_DEFAULT_REGION)) {
2481
+ const field = phoneInput.closest(".pc-field");
2482
+ field?.classList.add("invalid");
2483
+ const hint = field?.querySelector(".pc-hint");
2484
+ if (hint) hint.textContent = "Enter a valid phone number";
2485
+ if (phoneInput.hasAttribute("required")) {
2486
+ phoneInput.focus();
2487
+ return;
2488
+ }
2489
+ }
2490
+ const phone = rawPhone ? phoneToE164(rawPhone) ?? rawPhone : void 0;
2491
+ ctx.submit({
2492
+ name: val("name"),
2493
+ email: val("email"),
2494
+ phone
2495
+ });
2496
+ });
2497
+ ctx.wirePhoneField(form);
2498
+ queueMicrotask(() => form.querySelector("input")?.focus());
2499
+ return form;
2500
+ }
2501
+ /** Kind → card. See the registry doc above. */
2502
+ const INTERACTION_CARDS = { identity_intake: {
2503
+ capability: "identity_form",
2504
+ title: "Share your details",
2505
+ icon: ICON.user,
2506
+ build: buildIdentityIntakeCard
2507
+ } };
2172
2508
  var SmoothAgentChatElement = class extends HTMLElement {
2173
2509
  static get observedAttributes() {
2174
2510
  return OBSERVED;
@@ -2205,6 +2541,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
2205
2541
  _defineProperty(this, "streamTarget", "");
2206
2542
  _defineProperty(this, "displayedLength", 0);
2207
2543
  _defineProperty(this, "rafId", 0);
2544
+ _defineProperty(this, "prevBlockSig", "");
2208
2545
  this.root = this.attachShadow({ mode: "open" });
2209
2546
  }
2210
2547
  connectedCallback() {
@@ -2276,6 +2613,7 @@ var SmoothAgentChatElement = class extends HTMLElement {
2276
2613
  collectConsent: this.overrides.collectConsent,
2277
2614
  allowChatRestore: this.overrides.allowChatRestore,
2278
2615
  allowAnonymous: this.overrides.allowAnonymous,
2616
+ showToolActivity: this.overrides.showToolActivity ?? this.hasAttribute("show-tool-activity"),
2279
2617
  theme
2280
2618
  };
2281
2619
  }
@@ -2451,19 +2789,31 @@ var SmoothAgentChatElement = class extends HTMLElement {
2451
2789
  head.className = "int-head";
2452
2790
  const ico = document.createElement("span");
2453
2791
  ico.className = "int-ico";
2454
- ico.innerHTML = it.kind === "otp" ? ICON.lock : ICON.shield;
2792
+ const card_meta = it.kind === "interaction" ? INTERACTION_CARDS[it.interactionKind] : void 0;
2793
+ if (it.kind === "interaction" && !card_meta) {
2794
+ this.controller?.declineInteraction();
2795
+ el.classList.add("hidden");
2796
+ return;
2797
+ }
2798
+ ico.innerHTML = it.kind === "otp" ? ICON.lock : it.kind === "interaction" ? card_meta?.icon ?? ICON.user : ICON.shield;
2455
2799
  const title = document.createElement("span");
2456
2800
  title.className = "int-title";
2457
- title.textContent = it.kind === "otp" ? "Verification required" : "Confirm this action";
2801
+ title.textContent = it.kind === "otp" ? "Verification required" : it.kind === "interaction" ? card_meta?.title ?? "One more thing" : "Confirm this action";
2458
2802
  head.append(ico, title);
2459
2803
  card.appendChild(head);
2460
- if (it.actionDescription) {
2804
+ if (it.kind !== "interaction" && it.actionDescription) {
2461
2805
  const desc = document.createElement("div");
2462
2806
  desc.className = "int-desc";
2463
2807
  desc.textContent = it.actionDescription;
2464
2808
  card.appendChild(desc);
2465
2809
  }
2466
- if (it.kind === "otp") {
2810
+ if (it.kind === "interaction") card.appendChild(card_meta.build(it, {
2811
+ submit: (values) => this.controller?.submitInteraction(values),
2812
+ decline: () => this.controller?.declineInteraction(),
2813
+ prefill: this.controller?.getStore().getState().identity ?? {},
2814
+ wirePhoneField: (form) => this.wirePhoneField(form)
2815
+ }));
2816
+ else if (it.kind === "otp") {
2467
2817
  if (it.sent?.maskedDestination) {
2468
2818
  const sent = document.createElement("div");
2469
2819
  sent.className = "int-sent";
@@ -2803,15 +3153,38 @@ var SmoothAgentChatElement = class extends HTMLElement {
2803
3153
  const prev = this.messages;
2804
3154
  const last = messages[messages.length - 1];
2805
3155
  const prevLast = prev[prev.length - 1];
2806
- const structural = messages.length !== prev.length || !last || !prevLast || last.id !== prevLast.id || last.role !== prevLast.role || last.streaming !== prevLast.streaming || !!last.streaming && !prevLast.text && !!last.text;
3156
+ const structural = messages.length !== prev.length || !last || !prevLast || last.id !== prevLast.id || last.role !== prevLast.role || last.streaming !== prevLast.streaming || !!last.streaming && !prevLast.text && !!last.text || this.blockSig(last) !== this.prevBlockSig;
2807
3157
  this.messages = messages;
2808
- if (!structural && last && last.streaming && last.id === this.streamMsgId) {
2809
- this.streamTarget = last.text;
3158
+ this.prevBlockSig = this.blockSig(last);
3159
+ if (!structural && last && last.streaming && this.tailKey(last) === this.streamMsgId) {
3160
+ this.streamTarget = this.tailText(last);
2810
3161
  this.ensureRevealLoop();
2811
3162
  return;
2812
3163
  }
2813
3164
  this.renderMessages();
2814
3165
  }
3166
+ /** True when an assistant message's turn invoked at least one tool. */
3167
+ hasToolBlocks(m) {
3168
+ return !!m?.blocks?.some((b) => b.kind === "tool");
3169
+ }
3170
+ /** Signature that changes only when a tool chip is added or resolved (text growth is 'x'). */
3171
+ blockSig(m) {
3172
+ if (!m?.blocks) return "";
3173
+ return m.blocks.map((b) => b.kind === "tool" ? `t:${b.tool.id}:${b.tool.done ? 1 : 0}` : "x").join("|");
3174
+ }
3175
+ /** The live (last) text block for a tool turn, else the whole message text. */
3176
+ tailText(m) {
3177
+ if (this.hasToolBlocks(m) && m.blocks) {
3178
+ const last = m.blocks[m.blocks.length - 1];
3179
+ return last?.kind === "text" ? last.text : "";
3180
+ }
3181
+ return m.text;
3182
+ }
3183
+ /** Reveal-binding key — composite for a tool-turn tail (so a new trailing block rebinds), else msg id. */
3184
+ tailKey(m) {
3185
+ if (this.hasToolBlocks(m) && m.blocks) return `${m.id}#${m.blocks.length - 1}`;
3186
+ return m.id;
3187
+ }
2815
3188
  renderMessages() {
2816
3189
  if (!this.messagesEl) return;
2817
3190
  this.resetReveal();
@@ -2832,6 +3205,11 @@ var SmoothAgentChatElement = class extends HTMLElement {
2832
3205
  this.messagesEl.appendChild(chips);
2833
3206
  }
2834
3207
  for (const msg of this.messages) {
3208
+ if (msg.role === "assistant" && this.hasToolBlocks(msg)) {
3209
+ this.messagesEl.appendChild(this.buildRow("assistant", this.renderAssistantBlocks(msg)));
3210
+ if (!msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
3211
+ continue;
3212
+ }
2835
3213
  const bubble = document.createElement("div");
2836
3214
  bubble.className = `bubble ${msg.role}`;
2837
3215
  if (msg.role === "assistant" && msg.streaming && !msg.text) {
@@ -2889,19 +3267,77 @@ var SmoothAgentChatElement = class extends HTMLElement {
2889
3267
  * doesn't restart the reveal from zero), then resumes the loop.
2890
3268
  */
2891
3269
  bindReveal(msg, bubble) {
2892
- const carryOver = msg.id === this.streamMsgId ? Math.min(this.displayedLength, msg.text.length) : 0;
3270
+ const key = this.tailKey(msg);
3271
+ const target = this.tailText(msg);
3272
+ const carryOver = key === this.streamMsgId ? Math.min(this.displayedLength, target.length) : 0;
2893
3273
  this.streamBubbleEl = bubble;
2894
- this.streamMsgId = msg.id;
2895
- this.streamTarget = msg.text;
3274
+ this.streamMsgId = key;
3275
+ this.streamTarget = target;
2896
3276
  this.displayedLength = carryOver;
2897
3277
  if (this.prefersReducedMotion()) {
2898
- this.displayedLength = msg.text.length;
2899
- bubble.textContent = msg.text;
3278
+ this.displayedLength = target.length;
3279
+ bubble.textContent = target;
2900
3280
  return;
2901
3281
  }
2902
- bubble.textContent = msg.text.slice(0, this.displayedLength);
3282
+ bubble.textContent = target.slice(0, this.displayedLength);
2903
3283
  this.ensureRevealLoop();
2904
3284
  }
3285
+ /**
3286
+ * Render a tool-activity assistant turn as an ordered strip: prose bubbles and
3287
+ * inline tool chips in the order the model produced them (mirrors the daemon
3288
+ * SPA's `blocks`). The live trailing text block (while streaming) binds to the
3289
+ * rAF reveal; earlier/finalized text blocks render as sanitized markdown.
3290
+ */
3291
+ renderAssistantBlocks(msg) {
3292
+ const wrap = document.createElement("div");
3293
+ wrap.className = "blocks";
3294
+ const blocks = msg.blocks ?? [];
3295
+ const lastIdx = blocks.length - 1;
3296
+ blocks.forEach((block, i) => {
3297
+ if (block.kind === "tool") {
3298
+ wrap.appendChild(this.buildToolChip(block.tool));
3299
+ return;
3300
+ }
3301
+ const bubble = document.createElement("div");
3302
+ bubble.className = "bubble assistant";
3303
+ if (msg.streaming && i === lastIdx) {
3304
+ bubble.classList.add("cursor");
3305
+ this.bindReveal(msg, bubble);
3306
+ } else {
3307
+ bubble.classList.add("md");
3308
+ bubble.innerHTML = renderMarkdown(block.text);
3309
+ }
3310
+ wrap.appendChild(bubble);
3311
+ });
3312
+ return wrap;
3313
+ }
3314
+ /**
3315
+ * A single tool-activity chip: icon + tool name + status (running… / done / error),
3316
+ * with a truncated args preview. Tool name/args are set via `textContent` so a
3317
+ * tool payload can never inject markup.
3318
+ */
3319
+ buildToolChip(tool) {
3320
+ const chip = document.createElement("div");
3321
+ chip.className = `toolchip ${tool.done ? tool.isError ? "error" : "done" : "running"}`;
3322
+ chip.setAttribute("part", "tool-chip");
3323
+ const icon = document.createElement("span");
3324
+ icon.className = "ti";
3325
+ icon.innerHTML = ICON.tool;
3326
+ const name = document.createElement("span");
3327
+ name.className = "tn";
3328
+ name.textContent = tool.name || "tool";
3329
+ const status = document.createElement("span");
3330
+ status.className = "ts";
3331
+ status.textContent = tool.done ? tool.isError ? "error" : "done" : "running…";
3332
+ chip.append(icon, name, status);
3333
+ if (tool.args && tool.args !== "{}" && tool.args !== "\"\"") {
3334
+ const args = document.createElement("span");
3335
+ args.className = "ta";
3336
+ args.textContent = tool.args.length > 80 ? `${tool.args.slice(0, 80)}…` : tool.args;
3337
+ chip.appendChild(args);
3338
+ }
3339
+ return chip;
3340
+ }
2905
3341
  /** Start the rAF loop if it isn't already running. */
2906
3342
  ensureRevealLoop() {
2907
3343
  if (this.prefersReducedMotion() || typeof requestAnimationFrame !== "function") {