@smooai/smooth-operator 1.11.4 → 1.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.
@@ -593,9 +593,18 @@ var SmoothAgentChat = (function(exports) {
593
593
  messages = [];
594
594
  status = "idle";
595
595
  seq = 0;
596
- constructor(config, events) {
596
+ /**
597
+ * requestId → resolver for a pending button prompt. The turn loop parks on
598
+ * the promise; {@link answerPrompt} (called by the view on a button click)
599
+ * resolves it and the loop resumes consuming the (now un-paused) stream.
600
+ */
601
+ pendingPrompts = /* @__PURE__ */ new Map();
602
+ /** Extra client options — a test seam for injecting a mock transport. */
603
+ clientOptions;
604
+ constructor(config, events, clientOptions) {
597
605
  this.config = config;
598
606
  this.events = events;
607
+ this.clientOptions = clientOptions;
599
608
  }
600
609
  get connectionStatus() {
601
610
  return this.status;
@@ -616,7 +625,10 @@ var SmoothAgentChat = (function(exports) {
616
625
  if (this.status === "connecting" || this.status === "ready") return;
617
626
  this.setStatus("connecting");
618
627
  try {
619
- this.client = new SmoothAgentClient({ url: this.config.endpoint });
628
+ this.client = new SmoothAgentClient({
629
+ url: this.config.endpoint,
630
+ ...this.clientOptions
631
+ });
620
632
  await this.client.connect();
621
633
  const session = await this.client.createConversationSession({
622
634
  agentId: this.config.agentId,
@@ -665,6 +677,25 @@ var SmoothAgentChat = (function(exports) {
665
677
  assistant.text += token;
666
678
  this.emitMessages();
667
679
  }
680
+ } else if (event.type === "write_confirmation_required") {
681
+ const description = event.data?.data?.actionDescription ?? "Approve this action?";
682
+ const approved = await this.awaitPromptChoice(turn.requestId, {
683
+ requestId: turn.requestId,
684
+ kind: "confirm",
685
+ text: String(description),
686
+ options: [{
687
+ label: "Yes",
688
+ value: true
689
+ }, {
690
+ label: "No",
691
+ value: false
692
+ }]
693
+ });
694
+ this.client?.confirmToolAction({
695
+ sessionId: this.sessionId,
696
+ requestId: turn.requestId,
697
+ approved: approved === true
698
+ });
668
699
  }
669
700
  const inner = (await turn).data?.data;
670
701
  const finalText = extractFinalText(inner?.response);
@@ -682,8 +713,43 @@ var SmoothAgentChat = (function(exports) {
682
713
  this.setStatus("error", err instanceof Error ? err.message : String(err));
683
714
  }
684
715
  }
716
+ /**
717
+ * Attach a pending `prompt` to the current assistant bubble and return a
718
+ * promise that resolves when the user clicks a button (via
719
+ * {@link answerPrompt}). The bubble carrying the prompt is the last
720
+ * assistant message.
721
+ */
722
+ awaitPromptChoice(requestId, prompt) {
723
+ const last = this.messages[this.messages.length - 1];
724
+ if (last && last.role === "assistant") {
725
+ last.prompt = prompt;
726
+ last.streaming = false;
727
+ }
728
+ this.emitMessages();
729
+ return new Promise((resolve) => {
730
+ this.pendingPrompts.set(requestId, resolve);
731
+ });
732
+ }
733
+ /**
734
+ * Resolve a pending button prompt — called by the view when the user clicks
735
+ * an option. Marks the prompt answered (buttons disable, choice sticks) and
736
+ * unblocks the turn loop. No-op if the prompt was already answered.
737
+ */
738
+ answerPrompt(requestId, value) {
739
+ const resolve = this.pendingPrompts.get(requestId);
740
+ if (!resolve) return;
741
+ this.pendingPrompts.delete(requestId);
742
+ const msg = this.messages.find((m) => m.prompt?.requestId === requestId);
743
+ if (msg?.prompt) msg.prompt.answered = msg.prompt.options.find((o) => o.value === value)?.label ?? String(value);
744
+ this.emitMessages();
745
+ resolve(value);
746
+ }
685
747
  /** Tear down the underlying client. */
686
748
  disconnect() {
749
+ for (const [requestId, resolve] of this.pendingPrompts) {
750
+ this.pendingPrompts.delete(requestId);
751
+ resolve(false);
752
+ }
687
753
  this.client?.disconnect("widget closed");
688
754
  this.client = null;
689
755
  this.sessionId = null;
@@ -866,6 +932,35 @@ var SmoothAgentChat = (function(exports) {
866
932
 
867
933
  /* Sources panel — rendered under an assistant bubble whose terminal
868
934
  eventual_response carried citations. */
935
+ .prompt {
936
+ align-self: flex-start;
937
+ max-width: 80%;
938
+ margin-top: -2px;
939
+ display: flex;
940
+ flex-direction: column;
941
+ gap: 8px;
942
+ }
943
+ .panel.fullpage .prompt { max-width: 100%; }
944
+ .prompt-text { font-size: 13.5px; color: var(--sac-text); opacity: 0.9; }
945
+ .prompt-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
946
+ .prompt-button {
947
+ cursor: pointer;
948
+ border: 1px solid var(--sac-border);
949
+ background: var(--sac-bg);
950
+ color: var(--sac-text);
951
+ border-radius: 999px;
952
+ padding: 6px 16px;
953
+ font-size: 13px;
954
+ font-weight: 600;
955
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
956
+ }
957
+ .prompt-button:hover {
958
+ background: var(--sac-primary);
959
+ color: var(--sac-primary-text);
960
+ border-color: var(--sac-primary);
961
+ }
962
+ .prompt-answered { font-size: 12.5px; opacity: 0.7; font-style: italic; }
963
+
869
964
  .sources {
870
965
  align-self: flex-start;
871
966
  max-width: 80%;
@@ -1174,11 +1269,49 @@ var SmoothAgentChat = (function(exports) {
1174
1269
  el.textContent = msg.text;
1175
1270
  } else el.textContent = msg.text;
1176
1271
  this.messagesEl.appendChild(el);
1272
+ if (msg.prompt) this.messagesEl.appendChild(this.renderPrompt(msg.prompt));
1177
1273
  if (msg.role === "assistant" && !msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
1178
1274
  }
1179
1275
  this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
1180
1276
  }
1181
1277
  /**
1278
+ * Render a chat-native button prompt (SEP `ui/confirm` / `ui/select`). Each
1279
+ * option is a button; clicking one resumes the paused turn via
1280
+ * {@link ConversationController.answerPrompt}. Once answered the buttons
1281
+ * disable and the chosen label is shown, so the record stays in the
1282
+ * transcript. Built with DOM APIs (no innerHTML) — prompt text is untrusted.
1283
+ */
1284
+ renderPrompt(prompt) {
1285
+ const wrap = document.createElement("div");
1286
+ wrap.className = "prompt";
1287
+ wrap.setAttribute("part", "prompt");
1288
+ const text = document.createElement("div");
1289
+ text.className = "prompt-text";
1290
+ text.textContent = prompt.text;
1291
+ wrap.appendChild(text);
1292
+ if (prompt.answered) {
1293
+ const chosen = document.createElement("div");
1294
+ chosen.className = "prompt-answered";
1295
+ chosen.textContent = `You chose: ${prompt.answered}`;
1296
+ wrap.appendChild(chosen);
1297
+ return wrap;
1298
+ }
1299
+ const buttons = document.createElement("div");
1300
+ buttons.className = "prompt-buttons";
1301
+ for (const opt of prompt.options) {
1302
+ const btn = document.createElement("button");
1303
+ btn.className = "prompt-button";
1304
+ btn.type = "button";
1305
+ btn.textContent = opt.label;
1306
+ btn.addEventListener("click", () => {
1307
+ this.controller?.answerPrompt(prompt.requestId, opt.value);
1308
+ });
1309
+ buttons.appendChild(btn);
1310
+ }
1311
+ wrap.appendChild(buttons);
1312
+ return wrap;
1313
+ }
1314
+ /**
1182
1315
  * Build the collapsible "Sources (N)" block for an assistant message's
1183
1316
  * citations. Each source renders its `title` (linked to `citation.url` when
1184
1317
  * present — `target=_blank rel=noopener` — plain text otherwise) plus the