@alquimia-ai/ui 2.2.4 → 2.3.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
@@ -15,6 +15,7 @@ It holds no runtime logic. State comes from [`@alquimia-ai/tools`](../tools) —
15
15
  - [Theming](#theming)
16
16
  - [Building a chat](#building-a-chat)
17
17
  - [Chat organisms](#chat-organisms)
18
+ - [Tool approval](#tool-approval)
18
19
  - [GenUI](#genui)
19
20
  - [Speech](#speech)
20
21
  - [Ratings](#ratings)
@@ -190,6 +191,29 @@ A form with autosizing textarea, drag-and-drop and slots.
190
191
 
191
192
  ---
192
193
 
194
+ ## Tool approval
195
+
196
+ When the runtime parks a tool call for human approval, `useAlquimia` exposes the gate in `toolApprovals` and answers it with `approveTool`. `ToolApprovalCard` renders one: the tool name, its arguments, and Approve / Reject — rejecting takes an optional reason, which the runtime keeps in its audit trail.
197
+
198
+ ```tsx
199
+ import { ToolApprovalCard } from '@alquimia-ai/ui/components/organisms';
200
+
201
+ {alquimia.toolApprovals.map((approval) => (
202
+ <ToolApprovalCard
203
+ key={approval.controlId}
204
+ approval={approval}
205
+ onDecide={alquimia.approveTool}
206
+ channelName="WhatsApp"
207
+ />
208
+ ))}
209
+ ```
210
+
211
+ The card follows `approval.status` rather than deciding anything itself, so a decision made by another operator or on the originating channel shows up as settled here too — "Approved by you" or "Approved elsewhere", with the reason for a rejection. When the gate can only be answered on its original channel, or the runtime already refused this client (already decided, no longer pending, not allowed), the buttons are replaced by an explanation; `channelName` names the channel in it, since the runtime's refusal does not.
212
+
213
+ `AssistantChat` does all of this for you when given the whole hook return: approvals render after the message they followed, the typing indicator is suppressed while one waits, and `approvalChannelName` is passed through to the card.
214
+
215
+ ---
216
+
193
217
  ## GenUI
194
218
 
195
219
  The agent answers with a **component tree**, not markup. `@alquimia-ai/tools` validates it against a catalog; this package renders it.
@@ -204,7 +228,7 @@ const alquimia = useAlquimia({ assistantId, adapter, genui: { allow: 'all' } });
204
228
  <AssistantChat alquimia={alquimia} conversationId={conversationId} />
205
229
  ```
206
230
 
207
- `AssistantChat` is a thin composition of `AssistantMessageArea` + `AssistantInput` — it inherits markdown, streaming and error rendering rather than reimplementing them. Props: `alquimia` (required), `registry`, `conversationId`, `placeholder`, `suggestions`, `className`, `thinkIndicator`.
231
+ `AssistantChat` is a thin composition of `AssistantMessageArea` + `AssistantInput` — it inherits markdown, streaming and error rendering rather than reimplementing them. Props: `alquimia` (required), `registry`, `conversationId`, `placeholder`, `suggestions`, `className`, `thinkIndicator`, `approvalChannelName`. Pending tool approvals render in place — see [Tool approval](#tool-approval).
208
232
 
209
233
  Lower level, when you own the shell:
210
234
 
@@ -293,7 +317,7 @@ import { Whisper, SpeechToText } from '@alquimia-ai/ui/components/organisms';
293
317
 
294
318
  **Molecules** — `PageContainer`, `AlertDialog`, `AssistantButton`, `Carousel`, `NavigationMenu`, `Sidebar`, `Sonner`, `CallOut`, rating and document components.
295
319
 
296
- **Organisms** — `Assistant`, `AssistantMessageArea`, `AssistantInput`, `Whisper`, `SpeechToText`, `RatingDialog`.
320
+ **Organisms** — `Assistant`, `AssistantMessageArea`, `AssistantInput`, `ToolApprovalCard`, `Whisper`, `SpeechToText`, `RatingDialog`.
297
321
 
298
322
  **Templates** — `MessagesWindow`, `QueryBox`, `Hero`, `Cards`.
299
323
 
@@ -324,7 +348,7 @@ import { Whisper, SpeechToText } from '@alquimia-ai/ui/components/organisms';
324
348
  | `./components/molecules` | composed components |
325
349
  | `./components/molecules/viewers` | PDF / plain-text viewers |
326
350
  | `./components/molecules/documents` | document selector + viewer |
327
- | `./components/organisms` | chat, speech, ratings |
351
+ | `./components/organisms` | chat, tool approval, speech, ratings |
328
352
  | `./components/templates` | page-level layouts |
329
353
  | `./components/genui` | `A2uiRenderer`, `coreUiRegistry`, `AssistantChat` |
330
354
  | `./components/hooks` | UI hooks |
@@ -1,6 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import { A2uiComponent, SurfaceAction, A2uiSurface, UIAction } from '@alquimia-ai/tools/genui';
4
+ import { b as ToolApprovalView } from '../../tool-approval-K8aLzb1_.mjs';
5
+ import '@alquimia-ai/tools/hooks';
4
6
 
5
7
  /** Props every A2UI component receives from the renderer. */
6
8
  interface A2uiNodeProps {
@@ -48,6 +50,11 @@ interface GenuiChatController {
48
50
  awaitingUser: boolean;
49
51
  dismiss?: (userMessage?: string) => void;
50
52
  };
53
+ /** Human-approval gates (`useAlquimia().toolApprovals`), rendered in place with Approve / Reject. */
54
+ toolApprovals?: Array<ToolApprovalView & {
55
+ afterCount: number;
56
+ }>;
57
+ approveTool?: (controlId: string, approved: boolean, message?: string) => unknown;
51
58
  }
52
59
  interface AssistantChatProps {
53
60
  /** The value returned by `useAlquimia({ genui: {...} })`. */
@@ -61,6 +68,8 @@ interface AssistantChatProps {
61
68
  className?: string;
62
69
  /** Override the typing indicator shown while the agent is working. Defaults to animated dots. */
63
70
  thinkIndicator?: React.ReactNode;
71
+ /** Channel name shown when an approval can only be answered on its originating channel. */
72
+ approvalChannelName?: string;
64
73
  }
65
74
  /**
66
75
  * Batteries-included GenUI chat. A thin composition of the SDK's existing organisms
@@ -74,6 +83,6 @@ interface AssistantChatProps {
74
83
  * the hook). To customize the shell, compose the organisms directly instead of using
75
84
  * this wrapper.
76
85
  */
77
- declare function AssistantChat({ alquimia, registry, conversationId, placeholder, suggestions, className, thinkIndicator, }: AssistantChatProps): react_jsx_runtime.JSX.Element;
86
+ declare function AssistantChat({ alquimia, registry, conversationId, placeholder, suggestions, className, thinkIndicator, approvalChannelName, }: AssistantChatProps): react_jsx_runtime.JSX.Element;
78
87
 
79
88
  export { type A2uiNodeProps, A2uiRenderer, type A2uiRendererProps, AssistantChat, type AssistantChatProps, type GenuiChatController, coreUiRegistry };
@@ -1,6 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import { A2uiComponent, SurfaceAction, A2uiSurface, UIAction } from '@alquimia-ai/tools/genui';
4
+ import { b as ToolApprovalView } from '../../tool-approval-K8aLzb1_.js';
5
+ import '@alquimia-ai/tools/hooks';
4
6
 
5
7
  /** Props every A2UI component receives from the renderer. */
6
8
  interface A2uiNodeProps {
@@ -48,6 +50,11 @@ interface GenuiChatController {
48
50
  awaitingUser: boolean;
49
51
  dismiss?: (userMessage?: string) => void;
50
52
  };
53
+ /** Human-approval gates (`useAlquimia().toolApprovals`), rendered in place with Approve / Reject. */
54
+ toolApprovals?: Array<ToolApprovalView & {
55
+ afterCount: number;
56
+ }>;
57
+ approveTool?: (controlId: string, approved: boolean, message?: string) => unknown;
51
58
  }
52
59
  interface AssistantChatProps {
53
60
  /** The value returned by `useAlquimia({ genui: {...} })`. */
@@ -61,6 +68,8 @@ interface AssistantChatProps {
61
68
  className?: string;
62
69
  /** Override the typing indicator shown while the agent is working. Defaults to animated dots. */
63
70
  thinkIndicator?: React.ReactNode;
71
+ /** Channel name shown when an approval can only be answered on its originating channel. */
72
+ approvalChannelName?: string;
64
73
  }
65
74
  /**
66
75
  * Batteries-included GenUI chat. A thin composition of the SDK's existing organisms
@@ -74,6 +83,6 @@ interface AssistantChatProps {
74
83
  * the hook). To customize the shell, compose the organisms directly instead of using
75
84
  * this wrapper.
76
85
  */
77
- declare function AssistantChat({ alquimia, registry, conversationId, placeholder, suggestions, className, thinkIndicator, }: AssistantChatProps): react_jsx_runtime.JSX.Element;
86
+ declare function AssistantChat({ alquimia, registry, conversationId, placeholder, suggestions, className, thinkIndicator, approvalChannelName, }: AssistantChatProps): react_jsx_runtime.JSX.Element;
78
87
 
79
88
  export { type A2uiNodeProps, A2uiRenderer, type A2uiRendererProps, AssistantChat, type AssistantChatProps, type GenuiChatController, coreUiRegistry };
@@ -2705,8 +2705,8 @@ var coreUiRegistry = {
2705
2705
  Link: LinkNode
2706
2706
  };
2707
2707
  // src/components/genui/assistant-chat.tsx
2708
- var React43 = __toESM(require("react"));
2709
- var import_lucide_react20 = require("lucide-react");
2708
+ var React44 = __toESM(require("react"));
2709
+ var import_lucide_react21 = require("lucide-react");
2710
2710
  // src/components/organisms/assistant.tsx
2711
2711
  var React42 = __toESM(require("react"));
2712
2712
  var import_react8 = require("react");
@@ -4466,17 +4466,208 @@ var AssistantInput = React42.forwardRef(function(_param, ref) {
4466
4466
  }));
4467
4467
  });
4468
4468
  AssistantInput.displayName = "AssistantInput";
4469
- // src/components/genui/assistant-chat.tsx
4469
+ // src/components/organisms/tool-approval.tsx
4470
+ var React43 = __toESM(require("react"));
4471
+ var import_lucide_react20 = require("lucide-react");
4470
4472
  var import_jsx_runtime53 = require("react/jsx-runtime");
4473
+ function formatArguments(args) {
4474
+ if (args === null || args === void 0) return null;
4475
+ if (typeof args === "string") return args;
4476
+ try {
4477
+ return JSON.stringify(args, null, 2);
4478
+ } catch (e) {
4479
+ return String(args);
4480
+ }
4481
+ }
4482
+ function blockedMessage(approval, channelName) {
4483
+ var _approval_lastResult;
4484
+ var outcome = (_approval_lastResult = approval.lastResult) === null || _approval_lastResult === void 0 ? void 0 : _approval_lastResult.outcome;
4485
+ if (approval.channelOwned || outcome === "wrong-channel") {
4486
+ return "This approval has to be answered in ".concat(channelName !== null && channelName !== void 0 ? channelName : "the channel the conversation came from", ". It can't be answered here.");
4487
+ }
4488
+ switch(outcome){
4489
+ case "already-decided":
4490
+ return "Someone else already answered this request. Waiting for the agent to continue.";
4491
+ case "not-pending":
4492
+ return "This request is no longer waiting for a decision.";
4493
+ case "not-found":
4494
+ return "This conversation has ended, so the request can no longer be answered.";
4495
+ case "forbidden":
4496
+ return "You don't have permission to answer this request.";
4497
+ default:
4498
+ return null;
4499
+ }
4500
+ }
4501
+ function ToolApprovalCard(param) {
4502
+ var approval = param.approval, onDecide = param.onDecide, channelName = param.channelName, className = param.className;
4503
+ var _React43_useState = _sliced_to_array(React43.useState(false), 2), rejecting = _React43_useState[0], setRejecting = _React43_useState[1];
4504
+ var _React43_useState1 = _sliced_to_array(React43.useState(""), 2), reason = _React43_useState1[0], setReason = _React43_useState1[1];
4505
+ var args = formatArguments(approval.arguments);
4506
+ var _approval_toolName;
4507
+ var toolLabel = (_approval_toolName = approval.toolName) !== null && _approval_toolName !== void 0 ? _approval_toolName : "a tool";
4508
+ var submitting = approval.status === "submitting";
4509
+ var settled = approval.status === "approved" || approval.status === "rejected";
4510
+ var blocked = !settled && !submitting && !approval.canAnswer ? blockedMessage(approval, channelName) : null;
4511
+ var failed = !settled && !submitting && approval.canAnswer && approval.lastResult && approval.lastResult.outcome !== "accepted" ? approval.lastResult : null;
4512
+ var decide = function(approved) {
4513
+ var said = reason.trim();
4514
+ void onDecide(approval.controlId, approved, approved || !said ? void 0 : said);
4515
+ };
4516
+ return /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("section", {
4517
+ "aria-label": "Approval request for ".concat(toolLabel),
4518
+ "data-status": approval.status,
4519
+ className: cn("alq--tool-approval w-full max-w-[95%] rounded-2xl border bg-card p-4 shadow-sm", settled ? "border-border/60" : "border-amber-500/50", className),
4520
+ children: [
4521
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("header", {
4522
+ className: "flex items-start gap-2",
4523
+ children: [
4524
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.ShieldAlert, {
4525
+ "aria-hidden": true,
4526
+ className: cn("mt-0.5 h-4 w-4 shrink-0", settled ? "text-muted-foreground" : "text-amber-500")
4527
+ }),
4528
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", {
4529
+ className: "min-w-0 flex-1",
4530
+ children: [
4531
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("p", {
4532
+ className: "text-sm font-medium text-foreground",
4533
+ children: settled ? "Approval request" : "The agent needs your approval"
4534
+ }),
4535
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("p", {
4536
+ className: "text-sm text-muted-foreground",
4537
+ children: [
4538
+ "Run ",
4539
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("code", {
4540
+ className: "rounded bg-muted px-1 py-0.5 font-mono text-xs text-foreground",
4541
+ children: toolLabel
4542
+ })
4543
+ ]
4544
+ })
4545
+ ]
4546
+ }),
4547
+ settled && /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("span", {
4548
+ className: cn("flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium", approval.status === "approved" ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" : "bg-destructive/10 text-destructive"),
4549
+ children: [
4550
+ approval.status === "approved" ? /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.Check, {
4551
+ className: "h-3 w-3"
4552
+ }) : /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.X, {
4553
+ className: "h-3 w-3"
4554
+ }),
4555
+ approval.status === "approved" ? "Approved" : "Rejected",
4556
+ approval.decidedHere ? " by you" : " elsewhere"
4557
+ ]
4558
+ })
4559
+ ]
4560
+ }),
4561
+ args !== null && /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("pre", {
4562
+ "aria-label": "Arguments",
4563
+ className: "mt-3 max-h-48 overflow-auto rounded-lg bg-muted/60 p-3 font-mono text-xs text-foreground",
4564
+ children: args
4565
+ }),
4566
+ settled && approval.status === "rejected" && approval.reason && /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("p", {
4567
+ className: "mt-3 text-sm text-muted-foreground",
4568
+ children: [
4569
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("span", {
4570
+ className: "font-medium text-foreground",
4571
+ children: "Reason:"
4572
+ }),
4573
+ " ",
4574
+ approval.reason
4575
+ ]
4576
+ }),
4577
+ blocked && /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("p", {
4578
+ role: "status",
4579
+ className: "mt-3 text-sm text-muted-foreground",
4580
+ children: blocked
4581
+ }),
4582
+ failed && /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("p", {
4583
+ role: "alert",
4584
+ className: "mt-3 text-sm text-destructive",
4585
+ children: [
4586
+ "Couldn't send your decision",
4587
+ failed.detail ? ": ".concat(failed.detail) : ".",
4588
+ " Try again."
4589
+ ]
4590
+ }),
4591
+ !settled && (submitting || approval.canAnswer) && /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", {
4592
+ className: "mt-3 flex flex-col gap-2",
4593
+ children: [
4594
+ rejecting && /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(Textarea, {
4595
+ "aria-label": "Reason for rejecting (optional)",
4596
+ placeholder: "Reason for rejecting (optional)",
4597
+ value: reason,
4598
+ disabled: submitting,
4599
+ onChange: function(e) {
4600
+ return setReason(e.target.value);
4601
+ },
4602
+ className: "min-h-[64px] text-sm"
4603
+ }),
4604
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", {
4605
+ className: "flex flex-wrap items-center justify-end gap-2",
4606
+ children: [
4607
+ submitting && /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.Loader2, {
4608
+ "aria-label": "Sending decision",
4609
+ className: "h-4 w-4 animate-spin text-muted-foreground"
4610
+ }),
4611
+ rejecting ? /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(import_jsx_runtime53.Fragment, {
4612
+ children: [
4613
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(Button, {
4614
+ size: "sm",
4615
+ variant: "ghost",
4616
+ disabled: submitting,
4617
+ onClick: function() {
4618
+ return setRejecting(false);
4619
+ },
4620
+ children: "Cancel"
4621
+ }),
4622
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(Button, {
4623
+ size: "sm",
4624
+ variant: "destructive",
4625
+ disabled: submitting,
4626
+ onClick: function() {
4627
+ return decide(false);
4628
+ },
4629
+ children: "Reject"
4630
+ })
4631
+ ]
4632
+ }) : /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(import_jsx_runtime53.Fragment, {
4633
+ children: [
4634
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(Button, {
4635
+ size: "sm",
4636
+ variant: "outline",
4637
+ disabled: submitting,
4638
+ onClick: function() {
4639
+ return setRejecting(true);
4640
+ },
4641
+ children: "Reject\u2026"
4642
+ }),
4643
+ /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(Button, {
4644
+ size: "sm",
4645
+ disabled: submitting,
4646
+ onClick: function() {
4647
+ return decide(true);
4648
+ },
4649
+ children: "Approve"
4650
+ })
4651
+ ]
4652
+ })
4653
+ ]
4654
+ })
4655
+ ]
4656
+ })
4657
+ ]
4658
+ });
4659
+ }
4660
+ // src/components/genui/assistant-chat.tsx
4661
+ var import_jsx_runtime54 = require("react/jsx-runtime");
4471
4662
  function TypingDots() {
4472
- return /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("div", {
4663
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("div", {
4473
4664
  className: "flex w-fit items-center gap-1.5 rounded-2xl rounded-bl-md border border-border/60 bg-card px-4 py-3 shadow-sm",
4474
4665
  children: [
4475
4666
  0,
4476
4667
  1,
4477
4668
  2
4478
4669
  ].map(function(i) {
4479
- return /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("span", {
4670
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", {
4480
4671
  className: "h-2 w-2 animate-bounce rounded-full bg-primary",
4481
4672
  style: {
4482
4673
  animationDelay: "".concat(i * 0.15, "s"),
@@ -4487,11 +4678,15 @@ function TypingDots() {
4487
4678
  });
4488
4679
  }
4489
4680
  function AssistantChat(param) {
4490
- var alquimia = param.alquimia, _param_registry = param.registry, registry = _param_registry === void 0 ? coreUiRegistry : _param_registry, conversationId = param.conversationId, _param_placeholder = param.placeholder, placeholder = _param_placeholder === void 0 ? "Ask the agent to build something\u2026" : _param_placeholder, _param_suggestions = param.suggestions, suggestions = _param_suggestions === void 0 ? [] : _param_suggestions, className = param.className, thinkIndicator = param.thinkIndicator;
4491
- var endRef = React43.useRef(null);
4492
- var messages = alquimia.messages, input = alquimia.input, handleInputChange = alquimia.handleInputChange, handleSubmit = alquimia.handleSubmit, handleLoadingCancel = alquimia.handleLoadingCancel, isLoading = alquimia.isLoading, isMessageStreaming = alquimia.isMessageStreaming, streamingMessageId = alquimia.streamingMessageId, genui = alquimia.genui;
4681
+ var alquimia = param.alquimia, _param_registry = param.registry, registry = _param_registry === void 0 ? coreUiRegistry : _param_registry, conversationId = param.conversationId, _param_placeholder = param.placeholder, placeholder = _param_placeholder === void 0 ? "Ask the agent to build something\u2026" : _param_placeholder, _param_suggestions = param.suggestions, suggestions = _param_suggestions === void 0 ? [] : _param_suggestions, className = param.className, thinkIndicator = param.thinkIndicator, approvalChannelName = param.approvalChannelName;
4682
+ var endRef = React44.useRef(null);
4683
+ var messages = alquimia.messages, input = alquimia.input, handleInputChange = alquimia.handleInputChange, handleSubmit = alquimia.handleSubmit, handleLoadingCancel = alquimia.handleLoadingCancel, isLoading = alquimia.isLoading, isMessageStreaming = alquimia.isMessageStreaming, streamingMessageId = alquimia.streamingMessageId, genui = alquimia.genui, toolApprovals = alquimia.toolApprovals, approveTool = alquimia.approveTool;
4493
4684
  var _genui_surfaces;
4494
4685
  var surfaces = (_genui_surfaces = genui === null || genui === void 0 ? void 0 : genui.surfaces) !== null && _genui_surfaces !== void 0 ? _genui_surfaces : [];
4686
+ var approvals = toolApprovals !== null && toolApprovals !== void 0 ? toolApprovals : [];
4687
+ var awaitingApproval = approvals.some(function(a) {
4688
+ return a.status === "pending" || a.status === "submitting";
4689
+ });
4495
4690
  var _genui_awaitingUser;
4496
4691
  var awaitingUser = (_genui_awaitingUser = genui === null || genui === void 0 ? void 0 : genui.awaitingUser) !== null && _genui_awaitingUser !== void 0 ? _genui_awaitingUser : false;
4497
4692
  var isBusy = !!(isLoading || isMessageStreaming);
@@ -4499,36 +4694,37 @@ function AssistantChat(param) {
4499
4694
  var _m_content;
4500
4695
  return m.role === "user" || m.role !== "system" && ((_m_content = m.content) !== null && _m_content !== void 0 ? _m_content : "").trim().length > 0;
4501
4696
  });
4502
- var isEmpty = !hasContent && surfaces.length === 0;
4503
- React43.useEffect(function() {
4697
+ var isEmpty = !hasContent && surfaces.length === 0 && approvals.length === 0;
4698
+ React44.useEffect(function() {
4504
4699
  var _endRef_current;
4505
4700
  (_endRef_current = endRef.current) === null || _endRef_current === void 0 ? void 0 : _endRef_current.scrollIntoView({
4506
4701
  behavior: "smooth"
4507
4702
  });
4508
4703
  }, [
4509
4704
  messages,
4510
- surfaces
4705
+ surfaces,
4706
+ approvals.length
4511
4707
  ]);
4512
4708
  var renderSurface = function(item) {
4513
- return /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", {
4709
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", {
4514
4710
  className: "my-3 flex flex-col items-start gap-1.5",
4515
4711
  children: [
4516
- /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", {
4712
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", {
4517
4713
  className: cn("relative w-full max-w-[95%] rounded-2xl border border-border/60 bg-card p-5 shadow-sm duration-500 animate-in fade-in slide-in-from-bottom-2", item.submitted && "opacity-70"),
4518
4714
  children: [
4519
- item.submitted && /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", {
4715
+ item.submitted && /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", {
4520
4716
  className: "absolute right-3 top-3 z-10 flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-xs font-medium text-emerald-600 dark:text-emerald-400",
4521
4717
  children: [
4522
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(import_lucide_react20.Check, {
4718
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(import_lucide_react21.Check, {
4523
4719
  className: "h-3 w-3"
4524
4720
  }),
4525
4721
  item.dismissedWith ? "Skipped" : "Submitted"
4526
4722
  ]
4527
4723
  }),
4528
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("div", {
4724
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("div", {
4529
4725
  className: cn(item.submitted && "pointer-events-none select-none"),
4530
4726
  "aria-disabled": item.submitted,
4531
- children: /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(A2uiRenderer, {
4727
+ children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(A2uiRenderer, {
4532
4728
  surface: item.surface,
4533
4729
  registry: registry,
4534
4730
  onAgentAction: genui ? genui.onAgentAction(item.controlId) : function() {}
@@ -4536,10 +4732,10 @@ function AssistantChat(param) {
4536
4732
  })
4537
4733
  ]
4538
4734
  }),
4539
- item.dismissedWith && /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("p", {
4735
+ item.dismissedWith && /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("p", {
4540
4736
  className: "alq--surface-dismissed max-w-[95%] pl-1 text-sm text-muted-foreground",
4541
4737
  children: [
4542
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("span", {
4738
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", {
4543
4739
  className: "font-medium text-foreground",
4544
4740
  children: "You replied:"
4545
4741
  }),
@@ -4574,21 +4770,56 @@ function AssistantChat(param) {
4574
4770
  }
4575
4771
  }
4576
4772
  }
4773
+ var approvalsByCount = /* @__PURE__ */ new Map();
4774
+ var _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined;
4775
+ try {
4776
+ for(var _iterator1 = approvals[Symbol.iterator](), _step1; !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){
4777
+ var a = _step1.value;
4778
+ var _approvalsByCount_get;
4779
+ var arr1 = (_approvalsByCount_get = approvalsByCount.get(a.afterCount)) !== null && _approvalsByCount_get !== void 0 ? _approvalsByCount_get : [];
4780
+ arr1.push(a);
4781
+ approvalsByCount.set(a.afterCount, arr1);
4782
+ }
4783
+ } catch (err) {
4784
+ _didIteratorError1 = true;
4785
+ _iteratorError1 = err;
4786
+ } finally{
4787
+ try {
4788
+ if (!_iteratorNormalCompletion1 && _iterator1.return != null) {
4789
+ _iterator1.return();
4790
+ }
4791
+ } finally{
4792
+ if (_didIteratorError1) {
4793
+ throw _iteratorError1;
4794
+ }
4795
+ }
4796
+ }
4797
+ var renderApproval = function(approval) {
4798
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("div", {
4799
+ className: "my-3 flex flex-col items-start",
4800
+ children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(ToolApprovalCard, {
4801
+ approval: approval,
4802
+ onDecide: approveTool !== null && approveTool !== void 0 ? approveTool : function() {},
4803
+ channelName: approvalChannelName,
4804
+ className: "duration-500 animate-in fade-in slide-in-from-bottom-2"
4805
+ })
4806
+ }, "a-".concat(approval.controlId));
4807
+ };
4577
4808
  var interleave = function(afterIndex) {
4578
- var _byCount_get;
4579
- var nodes = ((_byCount_get = byCount.get(afterIndex)) !== null && _byCount_get !== void 0 ? _byCount_get : []).map(renderSurface);
4809
+ var _byCount_get, _approvalsByCount_get;
4810
+ var nodes = _to_consumable_array(((_byCount_get = byCount.get(afterIndex)) !== null && _byCount_get !== void 0 ? _byCount_get : []).map(renderSurface)).concat(_to_consumable_array(((_approvalsByCount_get = approvalsByCount.get(afterIndex)) !== null && _approvalsByCount_get !== void 0 ? _approvalsByCount_get : []).map(renderApproval)));
4580
4811
  if (afterIndex === 0 && isEmpty) {
4581
- nodes.unshift(/* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", {
4812
+ nodes.unshift(/* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", {
4582
4813
  className: "flex min-h-full flex-col items-center justify-center gap-4 py-16 text-center",
4583
4814
  children: [
4584
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("p", {
4815
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("p", {
4585
4816
  className: "max-w-sm text-muted-foreground",
4586
4817
  children: "Ask the agent to build an interface."
4587
4818
  }),
4588
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("div", {
4819
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("div", {
4589
4820
  className: "flex flex-col items-center gap-2",
4590
4821
  children: suggestions.map(function(s) {
4591
- return /* @__PURE__ */ (0, import_jsx_runtime53.jsx)("button", {
4822
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("button", {
4592
4823
  type: "button",
4593
4824
  onClick: function() {
4594
4825
  return handleInputChange({
@@ -4662,20 +4893,20 @@ function AssistantChat(param) {
4662
4893
  return _ref.apply(this, arguments);
4663
4894
  };
4664
4895
  }();
4665
- return /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)(Assistant, {
4896
+ return /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(Assistant, {
4666
4897
  className: cn("mx-auto flex h-full w-full max-w-2xl flex-col bg-transparent px-4", className),
4667
4898
  children: [
4668
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(AssistantMessageArea, {
4899
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(AssistantMessageArea, {
4669
4900
  className: "bg-transparent",
4670
4901
  messages: messages,
4671
4902
  messagesEndRef: endRef,
4672
4903
  isLoading: !!isLoading,
4673
4904
  streamingMessageId: streamingMessageId !== null && streamingMessageId !== void 0 ? streamingMessageId : null,
4674
4905
  interleave: interleave,
4675
- awaitingUser: awaitingUser,
4676
- thinkIndicator: thinkIndicator !== null && thinkIndicator !== void 0 ? thinkIndicator : /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(TypingDots, {})
4906
+ awaitingUser: awaitingUser || awaitingApproval,
4907
+ thinkIndicator: thinkIndicator !== null && thinkIndicator !== void 0 ? thinkIndicator : /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(TypingDots, {})
4677
4908
  }),
4678
- /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(AssistantInput, {
4909
+ /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(AssistantInput, {
4679
4910
  className: "mb-5 mt-2 rounded-2xl border-border/60 bg-card/90 shadow-lg backdrop-blur",
4680
4911
  sendMessageFunc: onSubmit,
4681
4912
  isButtonDisabled: isBusy && !awaitingUser,