@smooai/chat-widget 0.12.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/README.md CHANGED
@@ -232,6 +232,7 @@ Declarative attributes mirror the programmatic [`ChatWidgetConfig`](./src/config
232
232
  | `mode` | `mode` | `popover` (default) or `fullpage`. |
233
233
  | `start-open` | `startOpen` | Open the panel immediately (no launcher click). |
234
234
  | `hide-branding` | `hideBranding` | Hide the "powered by smooth-operator" tag + footer link. Default shown. |
235
+ | `show-tool-activity` | `showToolActivity` | Show the agent's tool activity as inline chips interleaved with its prose. Default **off** (end-users normally shouldn't see raw tool calls). |
235
236
  | — | `theme` | Brand colors (see [Theming](#theming)). |
236
237
  | — | `userName` / `userEmail` | Optional participant identity. |
237
238
 
@@ -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",
@@ -12943,6 +12944,59 @@ var SmoothAgentChat = (function(exports) {
12943
12944
  streaming: false
12944
12945
  };
12945
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
+ }
12946
13000
  var ConversationController = class {
12947
13001
  constructor(config, events, store) {
12948
13002
  _defineProperty(this, "config", void 0);
@@ -13289,11 +13343,13 @@ var SmoothAgentChat = (function(exports) {
13289
13343
  text: trimmed,
13290
13344
  streaming: false
13291
13345
  });
13346
+ const showTools = this.config.showToolActivity === true;
13292
13347
  const assistant = {
13293
13348
  id: this.nextId("a"),
13294
13349
  role: "assistant",
13295
13350
  text: "",
13296
- streaming: true
13351
+ streaming: true,
13352
+ blocks: showTools ? [] : void 0
13297
13353
  };
13298
13354
  this.messages.push(assistant);
13299
13355
  this.emitMessages();
@@ -13308,8 +13364,11 @@ var SmoothAgentChat = (function(exports) {
13308
13364
  const token = event.token ?? event.data?.token ?? "";
13309
13365
  if (token) {
13310
13366
  assistant.text += token;
13367
+ if (showTools && assistant.blocks) growTextBlock(assistant.blocks, token);
13311
13368
  this.emitMessages();
13312
13369
  }
13370
+ } else if (showTools && event.type === "stream_chunk") {
13371
+ if (assistant.blocks && applyToolChunk(assistant.blocks, event.data?.state)) this.emitMessages();
13313
13372
  } else this.handleTurnEvent(event);
13314
13373
  const inner = (await turn).data?.data;
13315
13374
  const finalText = extractFinalText(inner?.response);
@@ -13319,6 +13378,7 @@ var SmoothAgentChat = (function(exports) {
13319
13378
  if (citations.length > 0) assistant.citations = citations;
13320
13379
  const suggestions = extractSuggestions(inner?.response);
13321
13380
  if (suggestions.length > 0) assistant.suggestions = suggestions;
13381
+ if (assistant.blocks && !assistant.blocks.some((b) => b.kind === "tool")) assistant.blocks = void 0;
13322
13382
  assistant.streaming = false;
13323
13383
  this.emitMessages();
13324
13384
  } catch (err) {
@@ -13630,6 +13690,7 @@ var SmoothAgentChat = (function(exports) {
13630
13690
  --sac-bg: ${theme.background};
13631
13691
  --sac-primary: ${theme.primary};
13632
13692
  --sac-primary-text: ${theme.primaryText};
13693
+ --sac-secondary: ${theme.secondary};
13633
13694
  --sac-assistant-bubble: ${theme.assistantBubble};
13634
13695
  --sac-assistant-bubble-text: ${theme.assistantBubbleText};
13635
13696
  --sac-user-bubble: ${theme.userBubble};
@@ -13942,6 +14003,47 @@ ${mode === "fullpage" ? `
13942
14003
  }
13943
14004
  @keyframes sac-blink { to { opacity: 0 } }
13944
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
+
13945
14047
  /* ─────────────── Rendered markdown (assistant bubbles / snippets) ─────────── */
13946
14048
  /* The renderer (markdown.ts) emits a small allowlisted set of tags; these rules
13947
14049
  keep them legible inside the tight Aurora-Glass bubble + citation card. */
@@ -14384,7 +14486,8 @@ ${mode === "fullpage" ? `
14384
14486
  "greeting",
14385
14487
  "start-open",
14386
14488
  "mode",
14387
- "hide-branding"
14489
+ "hide-branding",
14490
+ "show-tool-activity"
14388
14491
  ];
14389
14492
  /**
14390
14493
  * Inline SVG icons (static, trusted strings — never interpolated with user data).
@@ -14406,7 +14509,9 @@ ${mode === "fullpage" ? `
14406
14509
  /** Identity-intake interrupt — a person. */
14407
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>`,
14408
14511
  /** Tool-confirmation interrupt — a shield. */
14409
- 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>`
14410
14515
  };
14411
14516
  /**
14412
14517
  * The identity_intake card: the fields the agent requested (pre-chat form field
@@ -14572,6 +14677,7 @@ ${mode === "fullpage" ? `
14572
14677
  _defineProperty(this, "streamTarget", "");
14573
14678
  _defineProperty(this, "displayedLength", 0);
14574
14679
  _defineProperty(this, "rafId", 0);
14680
+ _defineProperty(this, "prevBlockSig", "");
14575
14681
  this.root = this.attachShadow({ mode: "open" });
14576
14682
  }
14577
14683
  connectedCallback() {
@@ -14643,6 +14749,7 @@ ${mode === "fullpage" ? `
14643
14749
  collectConsent: this.overrides.collectConsent,
14644
14750
  allowChatRestore: this.overrides.allowChatRestore,
14645
14751
  allowAnonymous: this.overrides.allowAnonymous,
14752
+ showToolActivity: this.overrides.showToolActivity ?? this.hasAttribute("show-tool-activity"),
14646
14753
  theme
14647
14754
  };
14648
14755
  }
@@ -15182,15 +15289,38 @@ ${mode === "fullpage" ? `
15182
15289
  const prev = this.messages;
15183
15290
  const last = messages[messages.length - 1];
15184
15291
  const prevLast = prev[prev.length - 1];
15185
- 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;
15186
15293
  this.messages = messages;
15187
- if (!structural && last && last.streaming && last.id === this.streamMsgId) {
15188
- 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);
15189
15297
  this.ensureRevealLoop();
15190
15298
  return;
15191
15299
  }
15192
15300
  this.renderMessages();
15193
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
+ }
15194
15324
  renderMessages() {
15195
15325
  if (!this.messagesEl) return;
15196
15326
  this.resetReveal();
@@ -15211,6 +15341,11 @@ ${mode === "fullpage" ? `
15211
15341
  this.messagesEl.appendChild(chips);
15212
15342
  }
15213
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
+ }
15214
15349
  const bubble = document.createElement("div");
15215
15350
  bubble.className = `bubble ${msg.role}`;
15216
15351
  if (msg.role === "assistant" && msg.streaming && !msg.text) {
@@ -15268,19 +15403,77 @@ ${mode === "fullpage" ? `
15268
15403
  * doesn't restart the reveal from zero), then resumes the loop.
15269
15404
  */
15270
15405
  bindReveal(msg, bubble) {
15271
- 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;
15272
15409
  this.streamBubbleEl = bubble;
15273
- this.streamMsgId = msg.id;
15274
- this.streamTarget = msg.text;
15410
+ this.streamMsgId = key;
15411
+ this.streamTarget = target;
15275
15412
  this.displayedLength = carryOver;
15276
15413
  if (this.prefersReducedMotion()) {
15277
- this.displayedLength = msg.text.length;
15278
- bubble.textContent = msg.text;
15414
+ this.displayedLength = target.length;
15415
+ bubble.textContent = target;
15279
15416
  return;
15280
15417
  }
15281
- bubble.textContent = msg.text.slice(0, this.displayedLength);
15418
+ bubble.textContent = target.slice(0, this.displayedLength);
15282
15419
  this.ensureRevealLoop();
15283
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
+ }
15284
15477
  /** Start the rAF loop if it isn't already running. */
15285
15478
  ensureRevealLoop() {
15286
15479
  if (this.prefersReducedMotion() || typeof requestAnimationFrame !== "function") {