@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.
@@ -11782,6 +11782,7 @@ var SmoothAgentChat = (function(exports) {
11782
11782
  collectConsent: config.collectConsent ?? true,
11783
11783
  allowChatRestore: config.allowChatRestore ?? true,
11784
11784
  allowAnonymous: config.allowAnonymous ?? false,
11785
+ showToolActivity: config.showToolActivity ?? false,
11785
11786
  theme: {
11786
11787
  text: theme.text ?? "#f8fafc",
11787
11788
  background: theme.background ?? "#040d30",
@@ -11842,7 +11843,7 @@ var SmoothAgentChat = (function(exports) {
11842
11843
  }) : e[r] = t, e;
11843
11844
  }
11844
11845
  //#endregion
11845
- //#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/transport.js
11846
+ //#region node_modules/.pnpm/@smooai+smooth-operator@1.21.1/node_modules/@smooai/smooth-operator/dist/transport.js
11846
11847
  /**
11847
11848
  * Transport abstraction for the client.
11848
11849
  *
@@ -11967,7 +11968,7 @@ var SmoothAgentChat = (function(exports) {
11967
11968
  }
11968
11969
  };
11969
11970
  //#endregion
11970
- //#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/types.js
11971
+ //#region node_modules/.pnpm/@smooai+smooth-operator@1.21.1/node_modules/@smooai/smooth-operator/dist/types.js
11971
11972
  /** Every server→client `type` discriminator value. */
11972
11973
  const EVENT_TYPES = [
11973
11974
  "immediate_response",
@@ -11980,6 +11981,8 @@ var SmoothAgentChat = (function(exports) {
11980
11981
  "otp_sent",
11981
11982
  "otp_verified",
11982
11983
  "otp_invalid",
11984
+ "interaction_required",
11985
+ "interaction_invalid",
11983
11986
  "error",
11984
11987
  "pong"
11985
11988
  ];
@@ -11988,7 +11991,7 @@ var SmoothAgentChat = (function(exports) {
11988
11991
  return typeof frame === "object" && frame !== null && "type" in frame && typeof frame.type === "string" && EVENT_TYPES.includes(frame.type);
11989
11992
  }
11990
11993
  //#endregion
11991
- //#region node_modules/.pnpm/@smooai+smooth-operator@1.8.0/node_modules/@smooai/smooth-operator/dist/client.js
11994
+ //#region node_modules/.pnpm/@smooai+smooth-operator@1.21.1/node_modules/@smooai/smooth-operator/dist/client.js
11992
11995
  /**
11993
11996
  * SmoothAgentClient — a minimal, idiomatic, transport-agnostic client for the
11994
11997
  * smooth-operator WebSocket protocol.
@@ -12273,6 +12276,20 @@ var SmoothAgentChat = (function(exports) {
12273
12276
  ...req
12274
12277
  }));
12275
12278
  }
12279
+ /**
12280
+ * Submit (or decline) a Rich Interaction, resuming the turn parked by an
12281
+ * `interaction_required` event. This ONE verb serves every interaction kind
12282
+ * (identity intake, future date pickers, choice chips, …) — adding a kind
12283
+ * needs no new client method. Server-side validation may reply with an
12284
+ * `interaction_invalid` event (the turn stays parked — resubmit); a valid
12285
+ * submit resumes the stream back into the original {@link MessageTurn}.
12286
+ */
12287
+ submitInteraction(req) {
12288
+ this.transport.send(JSON.stringify({
12289
+ action: "submit_interaction",
12290
+ ...req
12291
+ }));
12292
+ }
12276
12293
  /** Send an action that expects a single correlated response event. */
12277
12294
  request(action) {
12278
12295
  const requestId = action.requestId ?? this.generateRequestId();
@@ -12855,6 +12872,13 @@ var SmoothAgentChat = (function(exports) {
12855
12872
  return null;
12856
12873
  }
12857
12874
  }
12875
+ /**
12876
+ * The Rich-Interaction render capabilities this widget declares at session
12877
+ * create (`supports`). Must stay aligned with the card registry in
12878
+ * `element.ts` (`INTERACTION_CARDS`) — registering a card IS declaring its
12879
+ * capability; a test asserts the two match.
12880
+ */
12881
+ const SUPPORTED_INTERACTION_CAPABILITIES = ["identity_form"];
12858
12882
  /** Pull the final assistant text out of an `eventual_response` data payload. */
12859
12883
  function extractFinalText(response) {
12860
12884
  if (!response || typeof response !== "object") return null;
@@ -12920,6 +12944,59 @@ var SmoothAgentChat = (function(exports) {
12920
12944
  streaming: false
12921
12945
  };
12922
12946
  }
12947
+ let toolSeq = 0;
12948
+ const nextToolId = () => `tool-${++toolSeq}`;
12949
+ /** Grow the trailing text block, or open a new one if the last block was a tool. */
12950
+ function growTextBlock(blocks, text) {
12951
+ if (!text) return;
12952
+ const last = blocks[blocks.length - 1];
12953
+ if (last && last.kind === "text") last.text += text;
12954
+ else blocks.push({
12955
+ kind: "text",
12956
+ text
12957
+ });
12958
+ }
12959
+ /**
12960
+ * Fold a `stream_chunk` node-state into the ordered block list, returning `true`
12961
+ * when the chunk carried tool activity.
12962
+ *
12963
+ * Tool activity rides `state.rawResponse.toolCall` / `state.rawResponse.toolResult`
12964
+ * — **NOT** `state.toolResult`. Reading the wrong path leaves every chip stuck on
12965
+ * "running…" forever (the exact bug the daemon SPA hit and this mirror avoids).
12966
+ */
12967
+ function applyToolChunk(blocks, state) {
12968
+ const raw = state?.rawResponse;
12969
+ if (!raw || typeof raw !== "object") return false;
12970
+ const call = raw.toolCall;
12971
+ const res = raw.toolResult;
12972
+ if (call) {
12973
+ const args = typeof call.arguments === "string" ? call.arguments : JSON.stringify(call.arguments ?? {});
12974
+ blocks.push({
12975
+ kind: "tool",
12976
+ tool: {
12977
+ id: nextToolId(),
12978
+ name: call.name ?? "",
12979
+ args,
12980
+ done: false
12981
+ }
12982
+ });
12983
+ return true;
12984
+ }
12985
+ if (res) {
12986
+ const result = typeof res.result === "string" ? res.result : JSON.stringify(res.result ?? "");
12987
+ for (let i = blocks.length - 1; i >= 0; i--) {
12988
+ const b = blocks[i];
12989
+ if (b && b.kind === "tool" && b.tool.name === (res.name ?? "") && !b.tool.done) {
12990
+ b.tool.done = true;
12991
+ b.tool.isError = !!res.isError;
12992
+ b.tool.result = result;
12993
+ break;
12994
+ }
12995
+ }
12996
+ return true;
12997
+ }
12998
+ return false;
12999
+ }
12923
13000
  var ConversationController = class {
12924
13001
  constructor(config, events, store) {
12925
13002
  _defineProperty(this, "config", void 0);
@@ -12932,6 +13009,7 @@ var SmoothAgentChat = (function(exports) {
12932
13009
  _defineProperty(this, "seq", 0);
12933
13010
  _defineProperty(this, "activeRequestId", null);
12934
13011
  _defineProperty(this, "interrupt", null);
13012
+ _defineProperty(this, "pendingInteractionValues", null);
12935
13013
  _defineProperty(this, "identityRestore", { phase: "idle" });
12936
13014
  _defineProperty(this, "resumeAttempted", false);
12937
13015
  _defineProperty(this, "httpBase", void 0);
@@ -13010,6 +13088,40 @@ var SmoothAgentChat = (function(exports) {
13010
13088
  });
13011
13089
  this.setInterrupt(null);
13012
13090
  }
13091
+ /**
13092
+ * Submit a Rich Interaction card to resume the parked turn. The server
13093
+ * routes to the kind's validator: invalid values come back as an
13094
+ * `interaction_invalid` event (the card re-renders with per-field errors —
13095
+ * the turn stays parked); a valid submit is acked and the turn resumes.
13096
+ * No-op if not awaiting an interaction.
13097
+ */
13098
+ submitInteraction(values) {
13099
+ if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== "interaction") return;
13100
+ this.pendingInteractionValues = {
13101
+ kind: this.interrupt.interactionKind,
13102
+ values
13103
+ };
13104
+ this.client.submitInteraction({
13105
+ sessionId: this.sessionId,
13106
+ requestId: this.activeRequestId,
13107
+ interactionId: this.interrupt.interactionId,
13108
+ kind: this.interrupt.interactionKind,
13109
+ values
13110
+ });
13111
+ }
13112
+ /** Decline the pending Rich Interaction; the agent continues without it. */
13113
+ declineInteraction() {
13114
+ if (!this.client || !this.sessionId || !this.activeRequestId || this.interrupt?.kind !== "interaction") return;
13115
+ this.client.submitInteraction({
13116
+ sessionId: this.sessionId,
13117
+ requestId: this.activeRequestId,
13118
+ interactionId: this.interrupt.interactionId,
13119
+ kind: this.interrupt.interactionKind,
13120
+ declined: true
13121
+ });
13122
+ this.pendingInteractionValues = null;
13123
+ this.setInterrupt(null);
13124
+ }
13013
13125
  nextId(prefix) {
13014
13126
  this.seq += 1;
13015
13127
  return `${prefix}-${this.seq}-${Date.now().toString(36)}`;
@@ -13174,6 +13286,7 @@ var SmoothAgentChat = (function(exports) {
13174
13286
  userName: state.identity.name,
13175
13287
  userEmail: state.identity.email,
13176
13288
  browserFingerprint: this.fingerprint(),
13289
+ supports: [...SUPPORTED_INTERACTION_CAPABILITIES],
13177
13290
  ...metadata ? { metadata } : {}
13178
13291
  });
13179
13292
  this.sessionId = session.sessionId;
@@ -13230,11 +13343,13 @@ var SmoothAgentChat = (function(exports) {
13230
13343
  text: trimmed,
13231
13344
  streaming: false
13232
13345
  });
13346
+ const showTools = this.config.showToolActivity === true;
13233
13347
  const assistant = {
13234
13348
  id: this.nextId("a"),
13235
13349
  role: "assistant",
13236
13350
  text: "",
13237
- streaming: true
13351
+ streaming: true,
13352
+ blocks: showTools ? [] : void 0
13238
13353
  };
13239
13354
  this.messages.push(assistant);
13240
13355
  this.emitMessages();
@@ -13249,8 +13364,11 @@ var SmoothAgentChat = (function(exports) {
13249
13364
  const token = event.token ?? event.data?.token ?? "";
13250
13365
  if (token) {
13251
13366
  assistant.text += token;
13367
+ if (showTools && assistant.blocks) growTextBlock(assistant.blocks, token);
13252
13368
  this.emitMessages();
13253
13369
  }
13370
+ } else if (showTools && event.type === "stream_chunk") {
13371
+ if (assistant.blocks && applyToolChunk(assistant.blocks, event.data?.state)) this.emitMessages();
13254
13372
  } else this.handleTurnEvent(event);
13255
13373
  const inner = (await turn).data?.data;
13256
13374
  const finalText = extractFinalText(inner?.response);
@@ -13260,6 +13378,7 @@ var SmoothAgentChat = (function(exports) {
13260
13378
  if (citations.length > 0) assistant.citations = citations;
13261
13379
  const suggestions = extractSuggestions(inner?.response);
13262
13380
  if (suggestions.length > 0) assistant.suggestions = suggestions;
13381
+ if (assistant.blocks && !assistant.blocks.some((b) => b.kind === "tool")) assistant.blocks = void 0;
13263
13382
  assistant.streaming = false;
13264
13383
  this.emitMessages();
13265
13384
  } catch (err) {
@@ -13316,6 +13435,55 @@ var SmoothAgentChat = (function(exports) {
13316
13435
  actionDescription: str(inner.actionDescription)
13317
13436
  });
13318
13437
  break;
13438
+ case "interaction_required": {
13439
+ const interactionId = str(inner.interactionId);
13440
+ const kind = str(inner.kind);
13441
+ const spec = inner.spec && typeof inner.spec === "object" ? inner.spec : {};
13442
+ if (!interactionId || !kind) break;
13443
+ this.pendingInteractionValues = null;
13444
+ this.setInterrupt({
13445
+ kind: "interaction",
13446
+ interactionId,
13447
+ interactionKind: kind,
13448
+ spec,
13449
+ reason: str(inner.reason)
13450
+ });
13451
+ break;
13452
+ }
13453
+ case "interaction_invalid":
13454
+ if (this.interrupt?.kind === "interaction" && this.interrupt.interactionId === str(inner.interactionId)) {
13455
+ const errors = [];
13456
+ if (Array.isArray(inner.errors)) for (const e of inner.errors) {
13457
+ if (!e || typeof e !== "object") continue;
13458
+ const o = e;
13459
+ const field = str(o.field);
13460
+ if (field) errors.push({
13461
+ field,
13462
+ message: str(o.message) ?? "Invalid value"
13463
+ });
13464
+ }
13465
+ this.pendingInteractionValues = null;
13466
+ this.setInterrupt({
13467
+ ...this.interrupt,
13468
+ errors
13469
+ });
13470
+ }
13471
+ break;
13472
+ case "immediate_response":
13473
+ if (this.interrupt?.kind === "interaction") {
13474
+ const pending = this.pendingInteractionValues;
13475
+ if (pending && pending.kind === "identity_intake") {
13476
+ const v = pending.values;
13477
+ this.store.getState().mergeIdentity({
13478
+ name: v.name,
13479
+ email: v.email,
13480
+ phone: v.phone
13481
+ });
13482
+ }
13483
+ this.pendingInteractionValues = null;
13484
+ this.setInterrupt(null);
13485
+ }
13486
+ break;
13319
13487
  default: break;
13320
13488
  }
13321
13489
  }
@@ -13522,6 +13690,7 @@ var SmoothAgentChat = (function(exports) {
13522
13690
  --sac-bg: ${theme.background};
13523
13691
  --sac-primary: ${theme.primary};
13524
13692
  --sac-primary-text: ${theme.primaryText};
13693
+ --sac-secondary: ${theme.secondary};
13525
13694
  --sac-assistant-bubble: ${theme.assistantBubble};
13526
13695
  --sac-assistant-bubble-text: ${theme.assistantBubbleText};
13527
13696
  --sac-user-bubble: ${theme.userBubble};
@@ -13834,6 +14003,47 @@ ${mode === "fullpage" ? `
13834
14003
  }
13835
14004
  @keyframes sac-blink { to { opacity: 0 } }
13836
14005
 
14006
+ /* Interleaved tool-activity strip (gated by showToolActivity): prose bubbles
14007
+ and inline tool chips stacked in the order the model produced them. */
14008
+ .blocks { display: flex; flex-direction: column; align-items: flex-start; gap: 7px; }
14009
+ .blocks .bubble { align-self: flex-start; }
14010
+ .toolchip {
14011
+ display: inline-flex;
14012
+ align-items: center;
14013
+ gap: 6px;
14014
+ max-width: 100%;
14015
+ padding: 5px 10px;
14016
+ border-radius: 99px;
14017
+ font-size: 12px;
14018
+ line-height: 1.3;
14019
+ color: color-mix(in srgb, var(--sac-text) 78%, transparent);
14020
+ background: color-mix(in srgb, var(--sac-text) 6%, transparent);
14021
+ border: 1px solid color-mix(in srgb, var(--sac-text) 12%, transparent);
14022
+ animation: sac-msg-in .3s var(--sac-ease) both;
14023
+ }
14024
+ .toolchip .ti { display: inline-flex; flex: none; opacity: .8; }
14025
+ .toolchip .ti svg { width: 13px; height: 13px; }
14026
+ .toolchip .tn { font-weight: 600; letter-spacing: .01em; }
14027
+ .toolchip .ts { opacity: .7; }
14028
+ .toolchip .ta {
14029
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
14030
+ font-size: 11px;
14031
+ opacity: .6;
14032
+ overflow: hidden;
14033
+ text-overflow: ellipsis;
14034
+ white-space: nowrap;
14035
+ min-width: 0;
14036
+ }
14037
+ .toolchip.running { border-color: color-mix(in srgb, var(--sac-primary) 45%, transparent); }
14038
+ .toolchip.running .ts { color: var(--sac-primary); opacity: 1; animation: sac-typing 1.3s ease-in-out infinite; }
14039
+ .toolchip.done .ts::before { content: '✓ '; }
14040
+ .toolchip.error {
14041
+ color: var(--sac-secondary);
14042
+ border-color: color-mix(in srgb, var(--sac-secondary) 50%, transparent);
14043
+ background: color-mix(in srgb, var(--sac-secondary) 10%, transparent);
14044
+ }
14045
+ .toolchip.error .ts::before { content: '! '; }
14046
+
13837
14047
  /* ─────────────── Rendered markdown (assistant bubbles / snippets) ─────────── */
13838
14048
  /* The renderer (markdown.ts) emits a small allowlisted set of tags; these rules
13839
14049
  keep them legible inside the tight Aurora-Glass bubble + citation card. */
@@ -14128,6 +14338,9 @@ ${mode === "fullpage" ? `
14128
14338
  border-color: color-mix(in srgb, var(--sac-primary) 60%, transparent);
14129
14339
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--sac-primary) 14%, transparent);
14130
14340
  }
14341
+ .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
14342
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
14343
+ .int-form .int-error { margin-top: 2px; }
14131
14344
  .int-btn {
14132
14345
  border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
14133
14346
  background: var(--sac-surface-2);
@@ -14147,8 +14360,14 @@ ${mode === "fullpage" ? `
14147
14360
  color: var(--sac-primary-text);
14148
14361
  box-shadow: 0 6px 14px -6px color-mix(in srgb, var(--sac-primary) 65%, transparent);
14149
14362
  }
14150
- .int-row .int-btn { flex: 1; }
14151
- .int-row .int-input + .int-btn { flex: 0 0 auto; }
14363
+ .int-row .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
14364
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
14365
+ .int-form .int-error { margin-top: 2px; }
14366
+ .int-btn { flex: 1; }
14367
+ .int-row .int-input + .int-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
14368
+ .int-form .pc-field input { width: 100%; box-sizing: border-box; }
14369
+ .int-form .int-error { margin-top: 2px; }
14370
+ .int-btn { flex: 0 0 auto; }
14152
14371
  .int-error { margin-top: 8px; font-size: 12px; color: #f87171; }
14153
14372
  .int-card { position: relative; }
14154
14373
  .int-close {
@@ -14267,7 +14486,8 @@ ${mode === "fullpage" ? `
14267
14486
  "greeting",
14268
14487
  "start-open",
14269
14488
  "mode",
14270
- "hide-branding"
14489
+ "hide-branding",
14490
+ "show-tool-activity"
14271
14491
  ];
14272
14492
  /**
14273
14493
  * Inline SVG icons (static, trusted strings — never interpolated with user data).
@@ -14286,9 +14506,141 @@ ${mode === "fullpage" ? `
14286
14506
  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>`,
14287
14507
  /** OTP interrupt — a padlock. */
14288
14508
  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>`,
14509
+ /** Identity-intake interrupt — a person. */
14510
+ 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>`,
14289
14511
  /** Tool-confirmation interrupt — a shield. */
14290
- 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>`
14512
+ 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>`,
14513
+ /** Tool-activity chip — a wrench. */
14514
+ 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>`
14291
14515
  };
14516
+ /**
14517
+ * The identity_intake card: the fields the agent requested (pre-chat form field
14518
+ * pattern — same classes, same phone formatting), per-field server errors, a
14519
+ * submit and a decline affordance. Server-supplied text (reason, labels, error
14520
+ * messages) is set via `textContent` — never innerHTML.
14521
+ */
14522
+ function buildIdentityIntakeCard(it, ctx) {
14523
+ const form = document.createElement("form");
14524
+ form.className = "int-form";
14525
+ form.noValidate = true;
14526
+ if (it.reason) {
14527
+ const desc = document.createElement("div");
14528
+ desc.className = "int-desc";
14529
+ desc.textContent = it.reason;
14530
+ form.appendChild(desc);
14531
+ }
14532
+ const DEFAULTS = {
14533
+ name: {
14534
+ label: "Name",
14535
+ type: "text",
14536
+ autocomplete: "name"
14537
+ },
14538
+ email: {
14539
+ label: "Email",
14540
+ type: "email",
14541
+ autocomplete: "email"
14542
+ },
14543
+ phone: {
14544
+ label: "Phone",
14545
+ type: "tel",
14546
+ autocomplete: "tel"
14547
+ }
14548
+ };
14549
+ const rawFields = Array.isArray(it.spec.fields) ? it.spec.fields : [];
14550
+ const fields = [];
14551
+ for (const f of rawFields) {
14552
+ if (!f || typeof f !== "object") continue;
14553
+ const o = f;
14554
+ const key = typeof o.key === "string" ? o.key : "";
14555
+ if (key !== "name" && key !== "email" && key !== "phone") continue;
14556
+ fields.push({
14557
+ key,
14558
+ required: o.required === true,
14559
+ label: typeof o.label === "string" ? o.label : void 0
14560
+ });
14561
+ }
14562
+ if (fields.length === 0) fields.push({
14563
+ key: "email",
14564
+ required: true
14565
+ });
14566
+ for (const f of fields) {
14567
+ const d = DEFAULTS[f.key];
14568
+ const label = document.createElement("label");
14569
+ label.className = "pc-field";
14570
+ const caption = document.createElement("span");
14571
+ caption.textContent = f.label ?? d.label;
14572
+ const input = document.createElement("input");
14573
+ input.name = f.key;
14574
+ input.type = d.type;
14575
+ input.setAttribute("autocomplete", d.autocomplete);
14576
+ input.required = f.required;
14577
+ const prefill = ctx.prefill[f.key];
14578
+ if (prefill) input.value = prefill;
14579
+ label.append(caption, input);
14580
+ if (f.key === "phone") {
14581
+ const hint = document.createElement("span");
14582
+ hint.className = "pc-hint";
14583
+ hint.setAttribute("aria-live", "polite");
14584
+ label.appendChild(hint);
14585
+ }
14586
+ const serverError = it.errors?.find((e) => e.field === f.key);
14587
+ if (serverError) {
14588
+ label.classList.add("invalid");
14589
+ const err = document.createElement("span");
14590
+ err.className = "int-error";
14591
+ err.textContent = serverError.message;
14592
+ label.appendChild(err);
14593
+ }
14594
+ form.appendChild(label);
14595
+ }
14596
+ const row = document.createElement("div");
14597
+ row.className = "int-row";
14598
+ const decline = document.createElement("button");
14599
+ decline.className = "int-btn";
14600
+ decline.type = "button";
14601
+ decline.textContent = "No thanks";
14602
+ decline.addEventListener("click", () => ctx.decline());
14603
+ const share = document.createElement("button");
14604
+ share.className = "int-btn primary";
14605
+ share.type = "submit";
14606
+ share.textContent = "Share details";
14607
+ row.append(decline, share);
14608
+ form.appendChild(row);
14609
+ form.addEventListener("submit", (ev) => {
14610
+ ev.preventDefault();
14611
+ if (!form.reportValidity()) return;
14612
+ const data = new FormData(form);
14613
+ const val = (k) => data.get(k)?.trim() || void 0;
14614
+ const rawPhone = val("phone");
14615
+ const phoneInput = form.querySelector("input[name=\"phone\"]");
14616
+ if (rawPhone && phoneInput && !isValidPhoneNumber(rawPhone, PHONE_DEFAULT_REGION)) {
14617
+ const field = phoneInput.closest(".pc-field");
14618
+ field?.classList.add("invalid");
14619
+ const hint = field?.querySelector(".pc-hint");
14620
+ if (hint) hint.textContent = "Enter a valid phone number";
14621
+ if (phoneInput.hasAttribute("required")) {
14622
+ phoneInput.focus();
14623
+ return;
14624
+ }
14625
+ }
14626
+ const phone = rawPhone ? phoneToE164(rawPhone) ?? rawPhone : void 0;
14627
+ ctx.submit({
14628
+ name: val("name"),
14629
+ email: val("email"),
14630
+ phone
14631
+ });
14632
+ });
14633
+ ctx.wirePhoneField(form);
14634
+ queueMicrotask(() => form.querySelector("input")?.focus());
14635
+ return form;
14636
+ }
14637
+ /** Kind → card. See the registry doc above. */
14638
+ const INTERACTION_CARDS = { identity_intake: {
14639
+ capability: "identity_form",
14640
+ title: "Share your details",
14641
+ icon: ICON.user,
14642
+ build: buildIdentityIntakeCard
14643
+ } };
14292
14644
  var SmoothAgentChatElement = class extends HTMLElement {
14293
14645
  static get observedAttributes() {
14294
14646
  return OBSERVED;
@@ -14325,6 +14677,7 @@ ${mode === "fullpage" ? `
14325
14677
  _defineProperty(this, "streamTarget", "");
14326
14678
  _defineProperty(this, "displayedLength", 0);
14327
14679
  _defineProperty(this, "rafId", 0);
14680
+ _defineProperty(this, "prevBlockSig", "");
14328
14681
  this.root = this.attachShadow({ mode: "open" });
14329
14682
  }
14330
14683
  connectedCallback() {
@@ -14396,6 +14749,7 @@ ${mode === "fullpage" ? `
14396
14749
  collectConsent: this.overrides.collectConsent,
14397
14750
  allowChatRestore: this.overrides.allowChatRestore,
14398
14751
  allowAnonymous: this.overrides.allowAnonymous,
14752
+ showToolActivity: this.overrides.showToolActivity ?? this.hasAttribute("show-tool-activity"),
14399
14753
  theme
14400
14754
  };
14401
14755
  }
@@ -14571,19 +14925,31 @@ ${mode === "fullpage" ? `
14571
14925
  head.className = "int-head";
14572
14926
  const ico = document.createElement("span");
14573
14927
  ico.className = "int-ico";
14574
- ico.innerHTML = it.kind === "otp" ? ICON.lock : ICON.shield;
14928
+ const card_meta = it.kind === "interaction" ? INTERACTION_CARDS[it.interactionKind] : void 0;
14929
+ if (it.kind === "interaction" && !card_meta) {
14930
+ this.controller?.declineInteraction();
14931
+ el.classList.add("hidden");
14932
+ return;
14933
+ }
14934
+ ico.innerHTML = it.kind === "otp" ? ICON.lock : it.kind === "interaction" ? card_meta?.icon ?? ICON.user : ICON.shield;
14575
14935
  const title = document.createElement("span");
14576
14936
  title.className = "int-title";
14577
- title.textContent = it.kind === "otp" ? "Verification required" : "Confirm this action";
14937
+ title.textContent = it.kind === "otp" ? "Verification required" : it.kind === "interaction" ? card_meta?.title ?? "One more thing" : "Confirm this action";
14578
14938
  head.append(ico, title);
14579
14939
  card.appendChild(head);
14580
- if (it.actionDescription) {
14940
+ if (it.kind !== "interaction" && it.actionDescription) {
14581
14941
  const desc = document.createElement("div");
14582
14942
  desc.className = "int-desc";
14583
14943
  desc.textContent = it.actionDescription;
14584
14944
  card.appendChild(desc);
14585
14945
  }
14586
- if (it.kind === "otp") {
14946
+ if (it.kind === "interaction") card.appendChild(card_meta.build(it, {
14947
+ submit: (values) => this.controller?.submitInteraction(values),
14948
+ decline: () => this.controller?.declineInteraction(),
14949
+ prefill: this.controller?.getStore().getState().identity ?? {},
14950
+ wirePhoneField: (form) => this.wirePhoneField(form)
14951
+ }));
14952
+ else if (it.kind === "otp") {
14587
14953
  if (it.sent?.maskedDestination) {
14588
14954
  const sent = document.createElement("div");
14589
14955
  sent.className = "int-sent";
@@ -14923,15 +15289,38 @@ ${mode === "fullpage" ? `
14923
15289
  const prev = this.messages;
14924
15290
  const last = messages[messages.length - 1];
14925
15291
  const prevLast = prev[prev.length - 1];
14926
- 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;
15292
+ 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;
14927
15293
  this.messages = messages;
14928
- if (!structural && last && last.streaming && last.id === this.streamMsgId) {
14929
- this.streamTarget = last.text;
15294
+ this.prevBlockSig = this.blockSig(last);
15295
+ if (!structural && last && last.streaming && this.tailKey(last) === this.streamMsgId) {
15296
+ this.streamTarget = this.tailText(last);
14930
15297
  this.ensureRevealLoop();
14931
15298
  return;
14932
15299
  }
14933
15300
  this.renderMessages();
14934
15301
  }
15302
+ /** True when an assistant message's turn invoked at least one tool. */
15303
+ hasToolBlocks(m) {
15304
+ return !!m?.blocks?.some((b) => b.kind === "tool");
15305
+ }
15306
+ /** Signature that changes only when a tool chip is added or resolved (text growth is 'x'). */
15307
+ blockSig(m) {
15308
+ if (!m?.blocks) return "";
15309
+ return m.blocks.map((b) => b.kind === "tool" ? `t:${b.tool.id}:${b.tool.done ? 1 : 0}` : "x").join("|");
15310
+ }
15311
+ /** The live (last) text block for a tool turn, else the whole message text. */
15312
+ tailText(m) {
15313
+ if (this.hasToolBlocks(m) && m.blocks) {
15314
+ const last = m.blocks[m.blocks.length - 1];
15315
+ return last?.kind === "text" ? last.text : "";
15316
+ }
15317
+ return m.text;
15318
+ }
15319
+ /** Reveal-binding key — composite for a tool-turn tail (so a new trailing block rebinds), else msg id. */
15320
+ tailKey(m) {
15321
+ if (this.hasToolBlocks(m) && m.blocks) return `${m.id}#${m.blocks.length - 1}`;
15322
+ return m.id;
15323
+ }
14935
15324
  renderMessages() {
14936
15325
  if (!this.messagesEl) return;
14937
15326
  this.resetReveal();
@@ -14952,6 +15341,11 @@ ${mode === "fullpage" ? `
14952
15341
  this.messagesEl.appendChild(chips);
14953
15342
  }
14954
15343
  for (const msg of this.messages) {
15344
+ if (msg.role === "assistant" && this.hasToolBlocks(msg)) {
15345
+ this.messagesEl.appendChild(this.buildRow("assistant", this.renderAssistantBlocks(msg)));
15346
+ if (!msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
15347
+ continue;
15348
+ }
14955
15349
  const bubble = document.createElement("div");
14956
15350
  bubble.className = `bubble ${msg.role}`;
14957
15351
  if (msg.role === "assistant" && msg.streaming && !msg.text) {
@@ -15009,19 +15403,77 @@ ${mode === "fullpage" ? `
15009
15403
  * doesn't restart the reveal from zero), then resumes the loop.
15010
15404
  */
15011
15405
  bindReveal(msg, bubble) {
15012
- const carryOver = msg.id === this.streamMsgId ? Math.min(this.displayedLength, msg.text.length) : 0;
15406
+ const key = this.tailKey(msg);
15407
+ const target = this.tailText(msg);
15408
+ const carryOver = key === this.streamMsgId ? Math.min(this.displayedLength, target.length) : 0;
15013
15409
  this.streamBubbleEl = bubble;
15014
- this.streamMsgId = msg.id;
15015
- this.streamTarget = msg.text;
15410
+ this.streamMsgId = key;
15411
+ this.streamTarget = target;
15016
15412
  this.displayedLength = carryOver;
15017
15413
  if (this.prefersReducedMotion()) {
15018
- this.displayedLength = msg.text.length;
15019
- bubble.textContent = msg.text;
15414
+ this.displayedLength = target.length;
15415
+ bubble.textContent = target;
15020
15416
  return;
15021
15417
  }
15022
- bubble.textContent = msg.text.slice(0, this.displayedLength);
15418
+ bubble.textContent = target.slice(0, this.displayedLength);
15023
15419
  this.ensureRevealLoop();
15024
15420
  }
15421
+ /**
15422
+ * Render a tool-activity assistant turn as an ordered strip: prose bubbles and
15423
+ * inline tool chips in the order the model produced them (mirrors the daemon
15424
+ * SPA's `blocks`). The live trailing text block (while streaming) binds to the
15425
+ * rAF reveal; earlier/finalized text blocks render as sanitized markdown.
15426
+ */
15427
+ renderAssistantBlocks(msg) {
15428
+ const wrap = document.createElement("div");
15429
+ wrap.className = "blocks";
15430
+ const blocks = msg.blocks ?? [];
15431
+ const lastIdx = blocks.length - 1;
15432
+ blocks.forEach((block, i) => {
15433
+ if (block.kind === "tool") {
15434
+ wrap.appendChild(this.buildToolChip(block.tool));
15435
+ return;
15436
+ }
15437
+ const bubble = document.createElement("div");
15438
+ bubble.className = "bubble assistant";
15439
+ if (msg.streaming && i === lastIdx) {
15440
+ bubble.classList.add("cursor");
15441
+ this.bindReveal(msg, bubble);
15442
+ } else {
15443
+ bubble.classList.add("md");
15444
+ bubble.innerHTML = renderMarkdown(block.text);
15445
+ }
15446
+ wrap.appendChild(bubble);
15447
+ });
15448
+ return wrap;
15449
+ }
15450
+ /**
15451
+ * A single tool-activity chip: icon + tool name + status (running… / done / error),
15452
+ * with a truncated args preview. Tool name/args are set via `textContent` so a
15453
+ * tool payload can never inject markup.
15454
+ */
15455
+ buildToolChip(tool) {
15456
+ const chip = document.createElement("div");
15457
+ chip.className = `toolchip ${tool.done ? tool.isError ? "error" : "done" : "running"}`;
15458
+ chip.setAttribute("part", "tool-chip");
15459
+ const icon = document.createElement("span");
15460
+ icon.className = "ti";
15461
+ icon.innerHTML = ICON.tool;
15462
+ const name = document.createElement("span");
15463
+ name.className = "tn";
15464
+ name.textContent = tool.name || "tool";
15465
+ const status = document.createElement("span");
15466
+ status.className = "ts";
15467
+ status.textContent = tool.done ? tool.isError ? "error" : "done" : "running…";
15468
+ chip.append(icon, name, status);
15469
+ if (tool.args && tool.args !== "{}" && tool.args !== "\"\"") {
15470
+ const args = document.createElement("span");
15471
+ args.className = "ta";
15472
+ args.textContent = tool.args.length > 80 ? `${tool.args.slice(0, 80)}…` : tool.args;
15473
+ chip.appendChild(args);
15474
+ }
15475
+ return chip;
15476
+ }
15025
15477
  /** Start the rAF loop if it isn't already running. */
15026
15478
  ensureRevealLoop() {
15027
15479
  if (this.prefersReducedMotion() || typeof requestAnimationFrame !== "function") {