agentic-ui-kit 0.7.1 → 0.8.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
@@ -1,11 +1,11 @@
1
1
  "use client";
2
- import { A as awaitingToolCalls, C as widgetLoaderMessages, D as toBlocks, F as settleToolCall, L as salvageJson, O as activityTokens, T as readEventStream, _ as replacedId, b as resolveReplaces, d as isStateRef, f as matchesSchema, g as uiActionSchema, i as parseUiMarkup, k as applyEvent } from "./src-D01l3nBc.js";
2
+ import { A as widgetLoaderMessages, C as readAskUserResult, D as resolveReplaces, F as activityTokens, H as settleToolCall, I as applyEvent, L as awaitingToolCalls, M as readEventStream, P as toBlocks, S as readAskUserInput, W as salvageJson, b as uiActionSchema, c as parseUiMarkup, g as matchesSchema, h as isStateRef, r as uiSpecSet, w as replacedId } from "./src-BrPYkHdt.js";
3
3
  import { clsx } from "clsx";
4
4
  import { extendTailwindMerge } from "tailwind-merge";
5
5
  import { Children, Component, Fragment, createContext, createElement, isValidElement, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
6
  import { Autocomplete, Button as Button$1, Dialog, DialogTrigger, DialogTrigger as DialogTrigger$1, Disclosure as Disclosure$1, DisclosurePanel, Focusable, Header, Heading, Input, Keyboard, Link, Menu as Menu$1, MenuItem as MenuItem$1, MenuSection as MenuSection$1, MenuTrigger, MenuTrigger as MenuTrigger$1, Modal as Modal$1, ModalOverlay, Popover as Popover$1, SearchField, Separator, SubmenuTrigger, Text, ToggleButton, ToggleButtonGroup, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
7
7
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
8
- import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, Info, MessageSquare, Mic, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
8
+ import { AlertTriangle, ArrowDown, ArrowRight, ArrowUp, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, GripVertical, Info, MessageSquare, Mic, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
9
9
  import { UNSAFE_PortalProvider } from "react-aria";
10
10
  import { createMathPlugin } from "@streamdown/math";
11
11
  import { Streamdown, defaultComponents, defaultRehypePlugins } from "streamdown";
@@ -204,6 +204,10 @@ const defaultIcons = {
204
204
  info: /* @__PURE__ */ jsx(Info, {}),
205
205
  warning: /* @__PURE__ */ jsx(AlertTriangle, {}),
206
206
  approval: /* @__PURE__ */ jsx(ShieldQuestion, {}),
207
+ askPrevious: /* @__PURE__ */ jsx(ChevronLeft, {}),
208
+ askPagerNext: /* @__PURE__ */ jsx(ChevronRight, {}),
209
+ askNext: /* @__PURE__ */ jsx(ArrowRight, {}),
210
+ dragHandle: /* @__PURE__ */ jsx(GripVertical, {}),
207
211
  web: /* @__PURE__ */ jsx(Globe, {}),
208
212
  pin: /* @__PURE__ */ jsx(Pin, {}),
209
213
  unpin: /* @__PURE__ */ jsx(PinOff, {}),
@@ -301,6 +305,24 @@ const defaultLabels = {
301
305
  tokenCount: (count) => `${count} tokens`,
302
306
  replyPlaceholder: "Reply…",
303
307
  replyDirectly: "Or reply directly…",
308
+ askUserPrompt: "Question for you",
309
+ askUserPager: (current, total) => `${current} of ${total}`,
310
+ askUserPrevious: "Previous question",
311
+ askUserNext: "Next question",
312
+ askUserContinue: "Continue",
313
+ askUserSubmit: "Send answers",
314
+ askUserClose: "Close and decline",
315
+ askUserSomethingElse: "Something else",
316
+ askUserSkip: "Skip",
317
+ askUserSelected: (count) => `${count} selected`,
318
+ askUserRankHint: "Drag to re-order",
319
+ askUserReorder: (option) => `Move ${option}`,
320
+ askUserReorderKeys: "Press Alt with the up or down arrow to move",
321
+ askUserPicked: (options) => options.join(", "),
322
+ askUserRanked: (options) => `Ranked: ${options.map((o, i) => `${i + 1}. ${o}`).join(" ")}`,
323
+ askUserSkipped: "Skipped",
324
+ askUserDeclined: "You declined to answer",
325
+ askUserAnswers: "Your answers",
304
326
  message: "Message",
305
327
  stop: "Stop",
306
328
  send: "Send",
@@ -1070,32 +1092,36 @@ function userMessage(text, parentId, attachments = []) {
1070
1092
  };
1071
1093
  }
1072
1094
  const wantsApproval = (part, tool) => part.approval === void 0 && (part.needsApproval === true || (typeof tool?.approval === "function" ? tool.approval(part.input) : tool?.approval === true));
1095
+ const wantsPrompt = (part, promptTools, tool) => part.approval !== "denied" && (promptTools.includes(part.name) || tool?.view?.Prompt !== void 0);
1073
1096
  const DECLINED = "Declined by the user";
1074
1097
  /**
1075
1098
  * A linear conversation driven by a Transport: sends, streams the reply through `applyEvent`, stops and
1076
1099
  * retries. With an `AgentProvider` it also runs the agent loop: client tools run in the page, approvals
1077
1100
  * wait for the user, then the reply continues; every finished tool call reaches its tool's `onResult`.
1078
1101
  */
1079
- function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12, resume = false }) {
1102
+ function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12, resume = false, promptTools = ["ask_user"] }) {
1080
1103
  const contextAgent = useAgentRegistry();
1081
1104
  const registry = agent === void 0 ? contextAgent : agent;
1082
1105
  const [messages, setMessages] = useState(initialMessages);
1083
1106
  const [busy, setBusy] = useState(false);
1084
1107
  const [approvals, setApprovals] = useState([]);
1108
+ const [prompts, setPrompts] = useState([]);
1085
1109
  const run = useRef(null);
1086
1110
  const latest = useRef({
1087
1111
  messages,
1088
1112
  transport,
1089
1113
  conversationId,
1090
1114
  onTitle,
1091
- registry
1115
+ registry,
1116
+ promptTools
1092
1117
  });
1093
1118
  latest.current = {
1094
1119
  messages,
1095
1120
  transport,
1096
1121
  conversationId,
1097
1122
  onTitle,
1098
- registry
1123
+ registry,
1124
+ promptTools
1099
1125
  };
1100
1126
  const loop = useRef({
1101
1127
  options: {},
@@ -1210,6 +1236,15 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
1210
1236
  setBusy(false);
1211
1237
  return;
1212
1238
  }
1239
+ const prompted = awaiting.filter((p) => wantsPrompt(p, latest.current.promptTools, registry?.tool(p.name)));
1240
+ if (prompted.length > 0) {
1241
+ setPrompts(prompted.map((part) => ({
1242
+ messageId: reply.id,
1243
+ part
1244
+ })));
1245
+ setBusy(false);
1246
+ return;
1247
+ }
1213
1248
  const controller = new AbortController();
1214
1249
  const current = {
1215
1250
  controller,
@@ -1298,6 +1333,29 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
1298
1333
  advance,
1299
1334
  commit
1300
1335
  ]);
1336
+ const settlePrompt = useCallback((partId, fields) => {
1337
+ const pending = prompts.find((p) => p.part.id === partId);
1338
+ if (!pending) return;
1339
+ const message = latest.current.messages.find((m) => m.id === pending.messageId);
1340
+ if (!message) return;
1341
+ const next = settleToolCall(message, partId, fields);
1342
+ commit(next);
1343
+ const rest = prompts.filter((p) => p.part.id !== partId);
1344
+ setPrompts(rest);
1345
+ if (rest.length === 0) advance(next);
1346
+ }, [
1347
+ prompts,
1348
+ advance,
1349
+ commit
1350
+ ]);
1351
+ const answer = useCallback((partId, result) => settlePrompt(partId, {
1352
+ status: "done",
1353
+ result
1354
+ }), [settlePrompt]);
1355
+ const decline = useCallback((partId) => settlePrompt(partId, {
1356
+ status: "error",
1357
+ error: DECLINED
1358
+ }), [settlePrompt]);
1301
1359
  /** Declines whatever is waiting, e.g. because the user moved on. */
1302
1360
  const declineAll = useCallback(() => {
1303
1361
  for (const { messageId, part } of approvals) {
@@ -1313,6 +1371,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
1313
1371
  error: DECLINED
1314
1372
  }));
1315
1373
  setApprovals([]);
1374
+ setPrompts([]);
1316
1375
  }, [approvals, commit]);
1317
1376
  const send = useCallback((text, options = {}) => {
1318
1377
  const attachments = options.attachments ?? [];
@@ -1337,6 +1396,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
1337
1396
  const index = messages.findIndex((m) => m.id === assistantId);
1338
1397
  if (index < 1 || messages[index - 1].role !== "user") return;
1339
1398
  setApprovals([]);
1399
+ setPrompts([]);
1340
1400
  loop.current = {
1341
1401
  options,
1342
1402
  step: 0
@@ -1372,6 +1432,9 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
1372
1432
  busy,
1373
1433
  approvals,
1374
1434
  respond,
1435
+ prompts,
1436
+ answer,
1437
+ decline,
1375
1438
  send,
1376
1439
  stop,
1377
1440
  retry
@@ -2211,17 +2274,18 @@ function Node({ node, registry }) {
2211
2274
  }) });
2212
2275
  }
2213
2276
  /** Draws a `render_ui` tree with the registered components. Unknown components and bad props render nothing. */
2214
- function UiTree({ id, root, state: initial, streaming = false }) {
2277
+ function UiTree({ id, stateKey, root, state: initial, streaming = false }) {
2278
+ const storeKey = stateKey ?? id;
2215
2279
  const registry = useContext(RegistryContext);
2216
2280
  const host = useWidgetHost();
2217
- const [local, setLocal] = useState(() => registry?.persist?.load(id) ?? {});
2281
+ const [local, setLocal] = useState(() => registry?.persist?.load(storeKey) ?? {});
2218
2282
  const changed = useRef(false);
2219
2283
  const latest = useRef(host);
2220
2284
  latest.current = host;
2221
2285
  useEffect(() => {
2222
- if (changed.current) registry?.persist?.save(id, local);
2286
+ if (changed.current) registry?.persist?.save(storeKey, local);
2223
2287
  }, [
2224
- id,
2288
+ storeKey,
2225
2289
  local,
2226
2290
  registry
2227
2291
  ]);
@@ -2272,15 +2336,20 @@ function useUiMarkupLanguage() {
2272
2336
  return registry && registry.markup !== false ? registry.markup : void 0;
2273
2337
  }
2274
2338
  /** A ```ui block from a reply, drawn with the registered components. Safe on a block that is still streaming. */
2275
- function UiMarkupBlock({ id, source, streaming = false }) {
2339
+ function UiMarkupBlock({ id, stateKey, source, streaming = false }) {
2276
2340
  const { nodes, state } = useMemo(() => parseUiMarkup(source), [source]);
2277
2341
  return /* @__PURE__ */ jsx(UiTree, {
2278
2342
  id,
2343
+ ...stateKey && { stateKey },
2279
2344
  root: nodes,
2280
2345
  state,
2281
2346
  streaming
2282
2347
  });
2283
2348
  }
2349
+ function stateKeyOf(options, part) {
2350
+ const stateKey = options.stateKey?.(part);
2351
+ return stateKey ? { stateKey } : {};
2352
+ }
2284
2353
  /**
2285
2354
  * A tool view that draws a call's (streaming) JSX markup with your registered components, for whatever tool
2286
2355
  * your harness uses; the suggested one is core's `showUiTool`. Register it under your tool's name:
@@ -2289,12 +2358,13 @@ function UiMarkupBlock({ id, source, streaming = false }) {
2289
2358
  function uiMarkupToolView(read = (input) => {
2290
2359
  const markup = input?.markup;
2291
2360
  return typeof markup === "string" ? markup : void 0;
2292
- }) {
2361
+ }, options = {}) {
2293
2362
  function UiMarkupToolBlock({ part }) {
2294
2363
  const markup = read(part.input ?? part.partialInput);
2295
2364
  if (!markup) return null;
2296
2365
  return /* @__PURE__ */ jsx(UiMarkupBlock, {
2297
2366
  id: part.id,
2367
+ ...stateKeyOf(options, part),
2298
2368
  source: markup,
2299
2369
  streaming: part.status === "streaming" || part.status === "input"
2300
2370
  });
@@ -2318,13 +2388,14 @@ function readTreeInput(input) {
2318
2388
  * your tool's input to the tree and its starting state; the default reads `{ root, state }`, the shape of
2319
2389
  * the suggested `renderUiTool`.
2320
2390
  */
2321
- function uiTreeToolView(read = readTreeInput) {
2391
+ function uiTreeToolView(read = readTreeInput, options = {}) {
2322
2392
  function UiTreeToolBlock({ part }) {
2323
2393
  const { root, state } = read(part.input ?? part.partialInput);
2324
2394
  if (!root) return null;
2325
2395
  const ok = typeof state === "object" && state !== null;
2326
2396
  return /* @__PURE__ */ jsx(UiTree, {
2327
2397
  id: part.id,
2398
+ ...stateKeyOf(options, part),
2328
2399
  root,
2329
2400
  ...ok && { state },
2330
2401
  streaming: part.status === "streaming" || part.status === "input"
@@ -2417,7 +2488,19 @@ function rehypeWordFade() {
2417
2488
  return (tree) => wrap(tree);
2418
2489
  }
2419
2490
  // src/components/markdown.tsx
2420
- const fadePlugins = [...Object.values(defaultRehypePlugins), rehypeWordFade];
2491
+ const [sanitize, sanitizeSchema] = defaultRehypePlugins.sanitize;
2492
+ const rehypePlugins = Object.values({
2493
+ ...defaultRehypePlugins,
2494
+ sanitize: [sanitize, {
2495
+ ...sanitizeSchema,
2496
+ tagNames: sanitizeSchema.tagNames?.filter((t) => t !== "source"),
2497
+ protocols: {
2498
+ ...sanitizeSchema.protocols,
2499
+ src: [...sanitizeSchema.protocols?.src ?? [], "data"]
2500
+ }
2501
+ }]
2502
+ });
2503
+ const fadePlugins = [...rehypePlugins, rehypeWordFade];
2421
2504
  const math = createMathPlugin({ singleDollarTextMath: true });
2422
2505
  /** Streamdown's own utility classes get this prefix, so neither they nor the host's classes apply. */
2423
2506
  const PREFIX = "llmchat";
@@ -2450,6 +2533,15 @@ function Code({ node, className, children }) {
2450
2533
  });
2451
2534
  }
2452
2535
  const components = {
2536
+ ...Object.fromEntries([
2537
+ "source",
2538
+ "video",
2539
+ "audio",
2540
+ "track",
2541
+ "iframe",
2542
+ "embed",
2543
+ "object"
2544
+ ].map((t) => [t, () => null])),
2453
2545
  pre: ({ children }) => /* @__PURE__ */ jsx(Fragment$1, { children }),
2454
2546
  code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
2455
2547
  node,
@@ -2458,11 +2550,18 @@ const components = {
2458
2550
  })
2459
2551
  };
2460
2552
  const LinksContext = createContext(void 0);
2461
- function MarkdownLinksProvider({ renderLink, renderLinkList, children }) {
2553
+ function MarkdownLinksProvider({ renderLink, renderLinkList, renderImage, imageOrigins, children }) {
2462
2554
  const value = useMemo(() => ({
2463
2555
  renderLink,
2464
- renderLinkList
2465
- }), [renderLink, renderLinkList]);
2556
+ renderLinkList,
2557
+ renderImage,
2558
+ imageOrigins
2559
+ }), [
2560
+ renderLink,
2561
+ renderLinkList,
2562
+ renderImage,
2563
+ imageOrigins
2564
+ ]);
2466
2565
  return /* @__PURE__ */ jsx(LinksContext.Provider, {
2467
2566
  value,
2468
2567
  children
@@ -2479,6 +2578,32 @@ function soleLink(children) {
2479
2578
  };
2480
2579
  return props.node?.tagName === "p" ? soleLink(props.children) : void 0;
2481
2580
  }
2581
+ const originOf = (url, base) => {
2582
+ try {
2583
+ return new URL(url, base).origin;
2584
+ } catch {
2585
+ return;
2586
+ }
2587
+ };
2588
+ /** Whether an image may load on render. Relative and protocol-relative URLs count as the page's origin or the one they name. */
2589
+ function imageAllowed(src, origins) {
2590
+ if (/^data:image\//i.test(src)) return true;
2591
+ if (origins === "all") return /^(https?:)?\/\//i.test(src) || !/^[a-z][a-z\d+.-]*:/i.test(src);
2592
+ if (!origins?.length) return false;
2593
+ const origin = originOf(src, typeof location === "undefined" ? void 0 : location.href);
2594
+ return origin !== void 0 && /^https?:/.test(origin) && origins.some((o) => (originOf(o) ?? o) === origin);
2595
+ }
2596
+ /** What an image the host did not allow becomes: a link to it (alt text or host name), opened only by the reader. */
2597
+ function ImageLink({ src, alt }) {
2598
+ const url = URL.canParse(src) ? new URL(src) : void 0;
2599
+ if (url?.protocol !== "http:" && url?.protocol !== "https:") return /* @__PURE__ */ jsx(Fragment$1, { children: alt });
2600
+ return /* @__PURE__ */ jsx("a", {
2601
+ href: src,
2602
+ target: "_blank",
2603
+ rel: "noopener noreferrer",
2604
+ children: alt || url.host
2605
+ });
2606
+ }
2482
2607
  function listOf(ordered, props, renderLinkList) {
2483
2608
  const Default = ordered ? defaultComponents.ol : defaultComponents.ul;
2484
2609
  const { children } = props;
@@ -2521,9 +2646,27 @@ function BlockScope({ id, streaming, children }) {
2521
2646
  });
2522
2647
  }
2523
2648
  const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
2524
- const { renderLink, renderLinkList } = useContext(LinksContext) ?? {};
2649
+ const { renderLink, renderLinkList, renderImage, imageOrigins } = useContext(LinksContext) ?? {};
2525
2650
  const withLinks = useMemo(() => ({
2526
2651
  ...components,
2652
+ img: ({ src, alt }) => {
2653
+ if (typeof src !== "string" || !src) return /* @__PURE__ */ jsx(Fragment$1, { children: alt });
2654
+ if (renderImage) return /* @__PURE__ */ jsx(Fragment$1, { children: renderImage({
2655
+ src,
2656
+ alt: alt ?? ""
2657
+ }) });
2658
+ if (imageAllowed(src, imageOrigins)) return /* @__PURE__ */ jsx("img", {
2659
+ src,
2660
+ alt: alt ?? "",
2661
+ loading: "lazy",
2662
+ referrerPolicy: "no-referrer",
2663
+ className: "max-w-full rounded-chat-md"
2664
+ });
2665
+ return /* @__PURE__ */ jsx(ImageLink, {
2666
+ src,
2667
+ alt: alt ?? ""
2668
+ });
2669
+ },
2527
2670
  ...renderLink && { a: ({ href, children }) => {
2528
2671
  const custom = href ? renderLink({
2529
2672
  href,
@@ -2540,11 +2683,16 @@ const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
2540
2683
  ul: (props) => listOf(false, props, renderLinkList),
2541
2684
  ol: (props) => listOf(true, props, renderLinkList)
2542
2685
  }
2543
- }), [renderLink, renderLinkList]);
2686
+ }), [
2687
+ renderLink,
2688
+ renderLinkList,
2689
+ renderImage,
2690
+ imageOrigins
2691
+ ]);
2544
2692
  return /* @__PURE__ */ jsx(Streamdown, {
2545
2693
  prefix: PREFIX,
2546
2694
  mode: animating || fade ? "streaming" : "static",
2547
- ...fade && { rehypePlugins: fadePlugins },
2695
+ rehypePlugins: fade ? fadePlugins : rehypePlugins,
2548
2696
  plugins: { math },
2549
2697
  components: withLinks,
2550
2698
  controls: false,
@@ -2553,6 +2701,406 @@ const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
2553
2701
  children: text
2554
2702
  });
2555
2703
  });
2704
+ // src/components/ask-user.tsx
2705
+ const rowClass$1 = "flex w-full cursor-pointer items-center gap-3 rounded-chat-md px-2.5 py-2 text-left text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none hover:bg-chat-hover focus-visible:ring-2 focus-visible:ring-chat-focus";
2706
+ /** Moves focus to the previous or next answer row of a question, without wrapping. */
2707
+ function moveFocus(root, from, delta) {
2708
+ const rows = [...root.querySelectorAll("[data-ask-row]")];
2709
+ const i = rows.findIndex((row) => row === from || row.contains(from));
2710
+ rows[Math.min(rows.length - 1, Math.max(0, i + delta))]?.focus();
2711
+ }
2712
+ const isTextField = (target) => target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
2713
+ /** The 1-based number a plain digit key stands for, or undefined. */
2714
+ function digitOf(e) {
2715
+ if (e.altKey || e.ctrlKey || e.metaKey || isTextField(e.target)) return void 0;
2716
+ return /^[1-9]$/.test(e.key) ? Number(e.key) : void 0;
2717
+ }
2718
+ function Num({ children, active }) {
2719
+ return /* @__PURE__ */ jsx("span", {
2720
+ "aria-hidden": true,
2721
+ className: clsx("flex size-6 shrink-0 items-center justify-center rounded-chat-sm border text-(length:--chat-ui-font-size-sm) tabular-nums", active ? "border-chat-accent bg-chat-accent text-chat-accent-fg" : "border-chat-border-strong text-chat-muted"),
2722
+ children
2723
+ });
2724
+ }
2725
+ function Check$1({ checked }) {
2726
+ const icons = useIcons();
2727
+ return /* @__PURE__ */ jsx("span", {
2728
+ "aria-hidden": true,
2729
+ className: clsx("flex size-5 shrink-0 items-center justify-center rounded-chat-sm border [&_svg]:size-3.5", checked ? "border-chat-accent bg-chat-accent text-chat-accent-fg" : "border-chat-border-strong"),
2730
+ children: checked && icons.check
2731
+ });
2732
+ }
2733
+ /** The "Something else" row: a text input the number key after the last option focuses. */
2734
+ function OtherRow({ number, value, onChange, onEnter, leading }) {
2735
+ const labels = useLabels();
2736
+ return /* @__PURE__ */ jsxs("div", {
2737
+ className: clsx(rowClass$1, "cursor-text"),
2738
+ children: [leading, /* @__PURE__ */ jsx("input", {
2739
+ "data-ask-row": true,
2740
+ "data-ask-other": true,
2741
+ value,
2742
+ onChange: (e) => onChange(e.target.value),
2743
+ onKeyDown: (e) => {
2744
+ if (e.key === "Enter" && !e.nativeEvent.isComposing) {
2745
+ e.preventDefault();
2746
+ onEnter();
2747
+ }
2748
+ },
2749
+ placeholder: labels.askUserSomethingElse,
2750
+ "aria-label": labels.askUserSomethingElse,
2751
+ "aria-keyshortcuts": String(number),
2752
+ className: "min-w-0 flex-1 bg-transparent text-chat-fg outline-none placeholder:text-chat-subtle"
2753
+ })]
2754
+ });
2755
+ }
2756
+ function Footer({ start, onSkip, next }) {
2757
+ const labels = useLabels();
2758
+ const icons = useIcons();
2759
+ return /* @__PURE__ */ jsxs("div", {
2760
+ className: "mt-2 flex items-center gap-2 px-1",
2761
+ children: [
2762
+ /* @__PURE__ */ jsx("span", {
2763
+ className: "min-w-0 flex-1 truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
2764
+ children: start
2765
+ }),
2766
+ /* @__PURE__ */ jsx(Button, {
2767
+ size: "sm",
2768
+ variant: "ghost",
2769
+ className: "text-chat-muted",
2770
+ onPress: onSkip,
2771
+ children: labels.askUserSkip
2772
+ }),
2773
+ next && /* @__PURE__ */ jsx(IconButton, {
2774
+ label: next.label,
2775
+ variant: "primary",
2776
+ size: "sm",
2777
+ isDisabled: next.disabled,
2778
+ onPress: next.onPress,
2779
+ children: icons.askNext
2780
+ })
2781
+ ]
2782
+ });
2783
+ }
2784
+ function SingleQuestion({ question, initial, onAnswer, onSkip }) {
2785
+ const { options } = question;
2786
+ const chosen = typeof initial?.answer === "string" ? initial.answer : void 0;
2787
+ const [other, setOther] = useState(chosen !== void 0 && !options.includes(chosen) ? chosen : "");
2788
+ const root = useRef(null);
2789
+ const submitOther = () => other.trim() && onAnswer(other.trim());
2790
+ return /* @__PURE__ */ jsxs("div", {
2791
+ ref: root,
2792
+ role: "group",
2793
+ "aria-label": question.question,
2794
+ onKeyDown: (e) => {
2795
+ const n = digitOf(e);
2796
+ if (n !== void 0 && n <= options.length) onAnswer(options[n - 1]);
2797
+ else if (n === options.length + 1) root.current?.querySelector("[data-ask-other]")?.focus();
2798
+ else return;
2799
+ e.preventDefault();
2800
+ },
2801
+ children: [
2802
+ options.map((option, i) => /* @__PURE__ */ jsxs("button", {
2803
+ type: "button",
2804
+ "data-ask-row": true,
2805
+ onClick: () => onAnswer(option),
2806
+ className: rowClass$1,
2807
+ children: [/* @__PURE__ */ jsx(Num, {
2808
+ active: option === chosen,
2809
+ children: i + 1
2810
+ }), /* @__PURE__ */ jsx("span", {
2811
+ className: "min-w-0 flex-1",
2812
+ children: option
2813
+ })]
2814
+ }, i)),
2815
+ /* @__PURE__ */ jsx(OtherRow, {
2816
+ number: options.length + 1,
2817
+ value: other,
2818
+ onChange: setOther,
2819
+ onEnter: submitOther,
2820
+ leading: /* @__PURE__ */ jsx(Num, {
2821
+ active: other !== "" && other === chosen,
2822
+ children: options.length + 1
2823
+ })
2824
+ }),
2825
+ /* @__PURE__ */ jsx(Footer, { onSkip })
2826
+ ]
2827
+ });
2828
+ }
2829
+ function MultipleQuestion({ question, initial, last, onAnswer, onSkip }) {
2830
+ const labels = useLabels();
2831
+ const { options } = question;
2832
+ const before = Array.isArray(initial?.answer) ? initial.answer : [];
2833
+ const [picked, setPicked] = useState(() => new Set(options.flatMap((o, i) => before.includes(o) ? [i] : [])));
2834
+ const [other, setOther] = useState(before.find((a) => !options.includes(a)) ?? "");
2835
+ const root = useRef(null);
2836
+ const answer = [...options.filter((_, i) => picked.has(i)), ...other.trim() ? [other.trim()] : []];
2837
+ const toggle = (i) => setPicked((set) => (set.delete(i) || set.add(i), new Set(set)));
2838
+ const next = () => answer.length > 0 && onAnswer(answer);
2839
+ return /* @__PURE__ */ jsxs("div", {
2840
+ ref: root,
2841
+ role: "group",
2842
+ "aria-label": question.question,
2843
+ onKeyDown: (e) => {
2844
+ const n = digitOf(e);
2845
+ if (n !== void 0 && n <= options.length) toggle(n - 1);
2846
+ else if (n === options.length + 1) root.current?.querySelector("[data-ask-other]")?.focus();
2847
+ else return;
2848
+ e.preventDefault();
2849
+ },
2850
+ children: [
2851
+ options.map((option, i) => /* @__PURE__ */ jsxs("button", {
2852
+ type: "button",
2853
+ role: "checkbox",
2854
+ "aria-checked": picked.has(i),
2855
+ "data-ask-row": true,
2856
+ onClick: () => toggle(i),
2857
+ onKeyDown: (e) => {
2858
+ if (e.key !== "Enter" || answer.length === 0) return;
2859
+ e.preventDefault();
2860
+ e.stopPropagation();
2861
+ next();
2862
+ },
2863
+ className: rowClass$1,
2864
+ children: [/* @__PURE__ */ jsx(Check$1, { checked: picked.has(i) }), /* @__PURE__ */ jsx("span", {
2865
+ className: "min-w-0 flex-1",
2866
+ children: option
2867
+ })]
2868
+ }, i)),
2869
+ /* @__PURE__ */ jsx(OtherRow, {
2870
+ number: options.length + 1,
2871
+ value: other,
2872
+ onChange: setOther,
2873
+ onEnter: next,
2874
+ leading: /* @__PURE__ */ jsx(Check$1, { checked: other.trim() !== "" })
2875
+ }),
2876
+ /* @__PURE__ */ jsx(Footer, {
2877
+ start: labels.askUserSelected(answer.length),
2878
+ onSkip,
2879
+ next: {
2880
+ label: last ? labels.askUserSubmit : labels.askUserContinue,
2881
+ disabled: answer.length === 0,
2882
+ onPress: next
2883
+ }
2884
+ })
2885
+ ]
2886
+ });
2887
+ }
2888
+ function RankQuestion({ question, initial, last, onAnswer, onSkip }) {
2889
+ const labels = useLabels();
2890
+ const icons = useIcons();
2891
+ const { options } = question;
2892
+ const [order, setOrder] = useState(() => {
2893
+ const before = Array.isArray(initial?.answer) ? initial.answer : [];
2894
+ const pool = options.map((o, i) => [o, i]);
2895
+ const restored = before.flatMap((text) => {
2896
+ const at = pool.findIndex(([o]) => o === text);
2897
+ return at === -1 ? [] : pool.splice(at, 1).map(([, i]) => i);
2898
+ });
2899
+ return restored.length === options.length ? restored : options.map((_, i) => i);
2900
+ });
2901
+ const rows = useRef(/* @__PURE__ */ new Map());
2902
+ const focusAfter = useRef(null);
2903
+ const dragging = useRef(null);
2904
+ useLayoutEffect(() => {
2905
+ if (focusAfter.current !== null) rows.current.get(focusAfter.current)?.focus();
2906
+ focusAfter.current = null;
2907
+ }, [order]);
2908
+ const move = (from, to) => {
2909
+ if (to < 0 || to >= order.length || from === to) return;
2910
+ setOrder((list) => {
2911
+ const next = [...list];
2912
+ next.splice(to, 0, next.splice(from, 1)[0]);
2913
+ return next;
2914
+ });
2915
+ };
2916
+ const send = () => onAnswer(order.map((i) => options[i]));
2917
+ const onDragOver = (position) => (e) => {
2918
+ e.preventDefault();
2919
+ if (dragging.current === null || dragging.current === position) return;
2920
+ move(dragging.current, position);
2921
+ dragging.current = position;
2922
+ };
2923
+ return /* @__PURE__ */ jsxs("div", {
2924
+ role: "group",
2925
+ "aria-label": question.question,
2926
+ children: [/* @__PURE__ */ jsx("ol", { children: order.map((option, position) => /* @__PURE__ */ jsxs("li", {
2927
+ ref: (el) => void (el ? rows.current.set(option, el) : rows.current.delete(option)),
2928
+ "data-ask-row": true,
2929
+ tabIndex: 0,
2930
+ draggable: true,
2931
+ "aria-label": `${position + 1}. ${options[option]}`,
2932
+ "aria-description": labels.askUserReorderKeys,
2933
+ "aria-keyshortcuts": "Alt+ArrowUp Alt+ArrowDown",
2934
+ onDragStart: () => dragging.current = position,
2935
+ onDragOver: onDragOver(position),
2936
+ onDragEnd: () => dragging.current = null,
2937
+ onDrop: (e) => e.preventDefault(),
2938
+ onKeyDown: (e) => {
2939
+ if (e.key === "Enter") send();
2940
+ else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
2941
+ const delta = e.key === "ArrowUp" ? -1 : 1;
2942
+ if (e.altKey) {
2943
+ focusAfter.current = option;
2944
+ move(position, position + delta);
2945
+ } else moveFocus(e.currentTarget.parentElement, e.currentTarget, delta);
2946
+ } else return;
2947
+ e.preventDefault();
2948
+ },
2949
+ className: clsx(rowClass$1, "cursor-grab"),
2950
+ children: [
2951
+ /* @__PURE__ */ jsx("span", {
2952
+ "aria-hidden": true,
2953
+ className: "text-chat-subtle",
2954
+ children: /* @__PURE__ */ jsx(IconSlot, {
2955
+ className: "[&_svg]:size-4",
2956
+ children: icons.dragHandle
2957
+ })
2958
+ }),
2959
+ /* @__PURE__ */ jsx(Num, { children: position + 1 }),
2960
+ /* @__PURE__ */ jsx("span", {
2961
+ className: "min-w-0 flex-1",
2962
+ children: options[option]
2963
+ })
2964
+ ]
2965
+ }, option)) }), /* @__PURE__ */ jsx(Footer, {
2966
+ start: labels.askUserRankHint,
2967
+ onSkip,
2968
+ next: {
2969
+ label: last ? labels.askUserSubmit : labels.askUserContinue,
2970
+ disabled: false,
2971
+ onPress: send
2972
+ }
2973
+ })]
2974
+ });
2975
+ }
2976
+ const questionViews = {
2977
+ single: SingleQuestion,
2978
+ multiple: MultipleQuestion,
2979
+ rank: RankQuestion
2980
+ };
2981
+ /**
2982
+ * The `ask_user` prompt: the model's questions one at a time with a pager, answered by clicking or the keyboard
2983
+ * (number keys, arrows, Enter; Alt+arrows reorder a rank). Submits `{ answers }` (an `AskUserResult`) after the
2984
+ * last question; Skip leaves a question out, the close button or Escape declines everything.
2985
+ */
2986
+ function AskUserPrompt({ part, onSubmit, onDecline }) {
2987
+ const labels = useLabels();
2988
+ const icons = useIcons();
2989
+ const questions = readAskUserInput(part.input);
2990
+ const [index, setIndex] = useState(0);
2991
+ const [answers, setAnswers] = useState([]);
2992
+ const root = useRef(null);
2993
+ useEffect(() => {
2994
+ root.current?.querySelector("[data-ask-row]:not([data-ask-other])")?.focus();
2995
+ }, [index]);
2996
+ const question = questions[index];
2997
+ if (!question) return null;
2998
+ const Question = questionViews[question.kind];
2999
+ const settle = (answer) => {
3000
+ const next = answers.slice();
3001
+ next[index] = {
3002
+ question: question.question,
3003
+ kind: question.kind,
3004
+ ...answer
3005
+ };
3006
+ setAnswers(next);
3007
+ for (let step = 1; step <= questions.length; step++) {
3008
+ const at = (index + step) % questions.length;
3009
+ if (next[at] === void 0) return setIndex(at);
3010
+ }
3011
+ onSubmit({ answers: questions.map((_, i) => next[i]) });
3012
+ };
3013
+ return /* @__PURE__ */ jsxs("div", {
3014
+ ref: root,
3015
+ role: "dialog",
3016
+ "aria-label": labels.askUserPrompt,
3017
+ onKeyDown: (e) => {
3018
+ if (e.key !== "Escape") return;
3019
+ e.preventDefault();
3020
+ onDecline();
3021
+ },
3022
+ className: "flex flex-col gap-1 rounded-chat-lg border border-chat-border-strong bg-chat-surface p-3 font-chat-sans text-chat-fg shadow-chat-popover",
3023
+ children: [/* @__PURE__ */ jsxs("div", {
3024
+ className: "flex items-start gap-2 px-1 pb-1",
3025
+ children: [
3026
+ /* @__PURE__ */ jsx("p", {
3027
+ className: "min-w-0 flex-1 text-(length:--chat-ui-font-size) leading-5 font-medium",
3028
+ children: question.question
3029
+ }),
3030
+ questions.length > 1 && /* @__PURE__ */ jsxs("div", {
3031
+ className: "flex shrink-0 items-center gap-0.5 text-(length:--chat-ui-font-size-sm) text-chat-muted",
3032
+ children: [
3033
+ /* @__PURE__ */ jsx(IconButton, {
3034
+ label: labels.askUserPrevious,
3035
+ size: "sm",
3036
+ isDisabled: index === 0,
3037
+ onPress: () => setIndex(index - 1),
3038
+ children: icons.askPrevious
3039
+ }),
3040
+ /* @__PURE__ */ jsx("span", {
3041
+ className: "tabular-nums",
3042
+ children: labels.askUserPager(index + 1, questions.length)
3043
+ }),
3044
+ /* @__PURE__ */ jsx(IconButton, {
3045
+ label: labels.askUserNext,
3046
+ size: "sm",
3047
+ isDisabled: index === questions.length - 1,
3048
+ onPress: () => setIndex(index + 1),
3049
+ children: icons.askPagerNext
3050
+ })
3051
+ ]
3052
+ }),
3053
+ /* @__PURE__ */ jsx(IconButton, {
3054
+ label: labels.askUserClose,
3055
+ size: "sm",
3056
+ onPress: onDecline,
3057
+ children: icons.remove
3058
+ })
3059
+ ]
3060
+ }), /* @__PURE__ */ jsx(Question, {
3061
+ question,
3062
+ initial: answers[index],
3063
+ last: answers.filter((a, i) => a !== void 0 && i !== index).length === questions.length - 1,
3064
+ onAnswer: (answer) => settle({ answer }),
3065
+ onSkip: () => settle({ skipped: true })
3066
+ }, index)]
3067
+ });
3068
+ }
3069
+ /** The user's answers as a card in the message: each question with its answer, "Ranked: 1. …", or "Skipped". */
3070
+ function AskUserAnswers({ part }) {
3071
+ const labels = useLabels();
3072
+ if (part.status === "error") return /* @__PURE__ */ jsx("p", {
3073
+ className: "font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
3074
+ children: labels.askUserDeclined
3075
+ });
3076
+ const answers = part.status === "done" ? readAskUserResult(part.result) : void 0;
3077
+ if (!answers) return null;
3078
+ const text = (a) => {
3079
+ if (a.skipped || a.answer === void 0) return labels.askUserSkipped;
3080
+ if (typeof a.answer === "string") return a.answer;
3081
+ return a.kind === "rank" ? labels.askUserRanked(a.answer) : labels.askUserPicked(a.answer);
3082
+ };
3083
+ return /* @__PURE__ */ jsx("ul", {
3084
+ "aria-label": labels.askUserAnswers,
3085
+ className: "flex flex-col gap-3 rounded-chat-lg border border-chat-border bg-chat-surface px-4 py-3 font-chat-sans text-(length:--chat-ui-font-size) leading-5",
3086
+ children: answers.map((a, i) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("p", {
3087
+ className: "text-chat-muted",
3088
+ children: a.question
3089
+ }), /* @__PURE__ */ jsx("p", {
3090
+ className: clsx("text-chat-fg", (a.skipped || a.answer === void 0) && "text-chat-subtle"),
3091
+ children: text(a)
3092
+ })] }, i))
3093
+ });
3094
+ }
3095
+ /**
3096
+ * The view of the `ask_user` tool (`askUserTool` in `agentic-ui-kit/core`): the prompt over the composer while
3097
+ * the call awaits the user, then the answers card in the message. Registered in `defaultToolViews`.
3098
+ */
3099
+ const askUserToolView = {
3100
+ label: (_, labels) => labels.askUserPrompt,
3101
+ Prompt: AskUserPrompt,
3102
+ Block: AskUserAnswers
3103
+ };
2556
3104
  // src/components/tools.tsx
2557
3105
  const ToolViewsContext = createContext(void 0);
2558
3106
  function ToolViewsProvider({ tools, children }) {
@@ -2733,6 +3281,7 @@ const memoryTool = {
2733
3281
  summary: (n, labels) => labels.recalledMemory(n)
2734
3282
  };
2735
3283
  const defaultToolViews = {
3284
+ ask_user: askUserToolView,
2736
3285
  bash: commandTool,
2737
3286
  shell: commandTool,
2738
3287
  web_search: webSearchTool,
@@ -2819,10 +3368,12 @@ function TokenCounter({ tokens, variant = "standalone", className }) {
2819
3368
  * row per step. Each row expands on its own; tool rows render through the registered `ToolView`.
2820
3369
  * A group of one step skips the rows: the heading is the step's own label and the box holds its detail.
2821
3370
  */
2822
- function ActivityGroup({ parts, defaultExpanded = false, className }) {
3371
+ function ActivityGroup({ parts: all, defaultExpanded = false, className }) {
3372
+ const parts = all.filter((p) => !bareThought(p) || p.title || p.durationMs);
2823
3373
  const views = useToolViews();
2824
3374
  const labels = useLabels();
2825
3375
  const [expanded, setExpanded] = useState(defaultExpanded);
3376
+ if (parts.length === 0) return null;
2826
3377
  const live = parts.some(isWorking);
2827
3378
  const tokens = activityTokens(parts);
2828
3379
  const after = live && tokens > 0 && /* @__PURE__ */ jsx(TokenCounter, {
@@ -2888,8 +3439,16 @@ function Shimmer({ children }) {
2888
3439
  children
2889
3440
  });
2890
3441
  }
3442
+ /** A finished thought with nothing to open: hidden reasoning, or no text. It is a plain line, not a disclosure. */
3443
+ function bareThought(part) {
3444
+ return part.type === "thinking" && part.status === "done" && (part.visibility === "hidden" || !part.text);
3445
+ }
2891
3446
  function SingleStep({ part, live, after, expanded, onExpandedChange }) {
2892
3447
  const { summary, detail } = useStepContent(part);
3448
+ if (bareThought(part)) return /* @__PURE__ */ jsxs("div", {
3449
+ className: "flex items-center gap-1.5 text-chat-muted text-(length:--chat-activity-font-size)",
3450
+ children: [/* @__PURE__ */ jsx(Lead, { live }), summary]
3451
+ });
2893
3452
  return /* @__PURE__ */ jsx(Disclosure, {
2894
3453
  isExpanded: expanded,
2895
3454
  onExpandedChange,
@@ -2912,6 +3471,10 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
2912
3471
  function Step({ part }) {
2913
3472
  const { summary, detail } = useStepContent(part);
2914
3473
  const live = isWorking(part);
3474
+ if (bareThought(part)) return /* @__PURE__ */ jsx("div", {
3475
+ className: "px-3 py-2 text-chat-muted text-(length:--chat-activity-font-size)",
3476
+ children: summary
3477
+ });
2915
3478
  return /* @__PURE__ */ jsx(Disclosure, {
2916
3479
  className: "px-3 py-2",
2917
3480
  summaryClassName: "text-(length:--chat-activity-font-size)",
@@ -3380,6 +3943,246 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
3380
3943
  ]
3381
3944
  });
3382
3945
  }
3946
+ // src/components/tool-prompt.tsx
3947
+ /**
3948
+ * Shows the `Prompt` of a pending call's tool view, above the composer (`ChatView` does this for you). Apps
3949
+ * that lay out the chat themselves render it from `useChat().prompts[0]`. Renders nothing if the tool's view
3950
+ * has no `Prompt`.
3951
+ */
3952
+ function ToolPromptCard({ prompt, onAnswer, onDecline }) {
3953
+ const { part } = prompt;
3954
+ const Prompt = useToolView(part.name).Prompt;
3955
+ if (!Prompt) return null;
3956
+ return /* @__PURE__ */ jsx(Prompt, {
3957
+ part,
3958
+ onSubmit: (result) => onAnswer(part.id, result),
3959
+ onDecline: () => onDecline(part.id)
3960
+ }, part.id);
3961
+ }
3962
+ // src/components/ui-components.tsx
3963
+ const toneClass = {
3964
+ default: "text-chat-fg",
3965
+ accent: "text-chat-accent",
3966
+ good: "text-chat-check",
3967
+ warning: "text-chat-warning",
3968
+ bad: "text-chat-danger"
3969
+ };
3970
+ const fillClass = {
3971
+ default: "bg-chat-fg",
3972
+ accent: "bg-chat-accent",
3973
+ good: "bg-chat-check",
3974
+ warning: "bg-chat-warning",
3975
+ bad: "bg-chat-danger"
3976
+ };
3977
+ /** "New" / "Changed": what the model added stands out in the accent. */
3978
+ function Tag({ children, quiet = false }) {
3979
+ return /* @__PURE__ */ jsx("span", {
3980
+ className: `rounded-chat-sm px-1.5 text-(length:--chat-ui-font-size-sm) font-medium ${quiet ? "border border-chat-border-strong text-chat-muted" : "bg-chat-accent text-chat-accent-fg"}`,
3981
+ children
3982
+ });
3983
+ }
3984
+ const Stack = ({ direction = "column", gap = "md", children }) => /* @__PURE__ */ jsx("div", {
3985
+ className: `flex ${direction === "row" ? "flex-row flex-wrap" : "flex-col"} ${{
3986
+ sm: "gap-1.5",
3987
+ md: "gap-3",
3988
+ lg: "gap-5"
3989
+ }[gap]}`,
3990
+ children
3991
+ });
3992
+ const Grid = ({ columns = 2, children }) => /* @__PURE__ */ jsx("div", {
3993
+ className: "grid gap-3",
3994
+ style: { gridTemplateColumns: `repeat(auto-fit, minmax(min(100%, ${Math.floor(36 / columns)}rem), 1fr))` },
3995
+ children
3996
+ });
3997
+ const Card = ({ title, subtitle, tag, tone = "default", children }) => /* @__PURE__ */ jsxs("section", {
3998
+ className: `rounded-chat-lg border p-3 ${tone === "accent" ? "border-chat-accent" : "border-chat-border"}`,
3999
+ children: [
4000
+ (title || tag) && /* @__PURE__ */ jsxs("header", {
4001
+ className: "mb-1 flex items-center gap-2",
4002
+ children: [title && /* @__PURE__ */ jsx("h3", {
4003
+ className: "font-chat-sans text-(length:--chat-ui-font-size) font-semibold",
4004
+ children: title
4005
+ }), tag && /* @__PURE__ */ jsx(Tag, {
4006
+ quiet: tone !== "accent",
4007
+ children: tag
4008
+ })]
4009
+ }),
4010
+ subtitle && /* @__PURE__ */ jsx("p", {
4011
+ className: "mb-2 text-(length:--chat-ui-font-size-sm) text-chat-muted",
4012
+ children: subtitle
4013
+ }),
4014
+ children
4015
+ ]
4016
+ });
4017
+ const Heading$1 = ({ text }) => /* @__PURE__ */ jsx("h2", {
4018
+ className: "font-chat-sans text-(length:--chat-ui-font-size) font-semibold",
4019
+ children: text
4020
+ });
4021
+ const Text$1 = ({ text, tone = "default" }) => /* @__PURE__ */ jsx("p", {
4022
+ className: `font-chat-sans text-(length:--chat-ui-font-size) leading-relaxed ${tone === "muted" ? "text-chat-muted" : ""}`,
4023
+ children: text
4024
+ });
4025
+ const Metric = ({ label, value, note, tone = "default" }) => /* @__PURE__ */ jsxs("div", {
4026
+ className: "min-w-24",
4027
+ children: [
4028
+ /* @__PURE__ */ jsx("div", {
4029
+ className: "text-(length:--chat-ui-font-size-sm) text-chat-muted",
4030
+ children: label
4031
+ }),
4032
+ /* @__PURE__ */ jsx("div", {
4033
+ className: `font-chat-sans text-xl font-semibold tabular-nums ${toneClass[tone]}`,
4034
+ children: value
4035
+ }),
4036
+ note && /* @__PURE__ */ jsx("div", {
4037
+ className: "text-(length:--chat-ui-font-size-sm) text-chat-subtle",
4038
+ children: note
4039
+ })
4040
+ ]
4041
+ });
4042
+ const List = ({ items }) => /* @__PURE__ */ jsx("ul", {
4043
+ className: "divide-y divide-chat-divider",
4044
+ children: items.map((item, i) => /* @__PURE__ */ jsxs("li", {
4045
+ className: "flex items-baseline gap-2 py-1.5 font-chat-sans text-(length:--chat-ui-font-size)",
4046
+ children: [
4047
+ /* @__PURE__ */ jsx("span", {
4048
+ className: item.tag ? "text-chat-accent" : "",
4049
+ children: item.label
4050
+ }),
4051
+ item.tag && /* @__PURE__ */ jsx(Tag, { children: item.tag }),
4052
+ item.detail && /* @__PURE__ */ jsx("span", {
4053
+ className: "ml-auto text-chat-muted tabular-nums",
4054
+ children: item.detail
4055
+ })
4056
+ ]
4057
+ }, i))
4058
+ });
4059
+ const Table = ({ columns, rows }) => /* @__PURE__ */ jsx("div", {
4060
+ className: "max-w-full overflow-x-auto",
4061
+ children: /* @__PURE__ */ jsxs("table", {
4062
+ className: "w-full border-collapse text-left font-chat-sans text-(length:--chat-ui-font-size)",
4063
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
4064
+ className: "border-b border-chat-border-strong",
4065
+ children: columns.map((c, i) => /* @__PURE__ */ jsx("th", {
4066
+ scope: "col",
4067
+ className: "px-2 py-1.5 font-medium text-chat-muted",
4068
+ children: c
4069
+ }, i))
4070
+ }) }), /* @__PURE__ */ jsx("tbody", {
4071
+ className: "divide-y divide-chat-divider",
4072
+ children: rows.map((row, r) => /* @__PURE__ */ jsx("tr", { children: columns.map((_, c) => /* @__PURE__ */ jsx("td", {
4073
+ className: `px-2 py-1.5 ${typeof row[c] === "number" ? "tabular-nums" : ""}`,
4074
+ children: row[c]
4075
+ }, c)) }, r))
4076
+ })]
4077
+ })
4078
+ });
4079
+ const Badge$1 = ({ label, tone = "default" }) => /* @__PURE__ */ jsx("span", {
4080
+ className: `inline-block self-start rounded-chat-sm border border-current px-1.5 font-chat-sans text-(length:--chat-ui-font-size-sm) font-medium ${toneClass[tone]}`,
4081
+ children: label
4082
+ });
4083
+ const Progress = ({ label, value, max = 100, tone = "accent" }) => {
4084
+ const share = Math.min(1, Math.max(0, value / max));
4085
+ return /* @__PURE__ */ jsxs("div", {
4086
+ className: "flex flex-col gap-1",
4087
+ children: [label && /* @__PURE__ */ jsxs("div", {
4088
+ className: "flex justify-between text-(length:--chat-ui-font-size-sm) text-chat-muted",
4089
+ children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsxs("span", {
4090
+ className: "tabular-nums",
4091
+ children: [Math.round(share * 100), "%"]
4092
+ })]
4093
+ }), /* @__PURE__ */ jsx("div", {
4094
+ role: "progressbar",
4095
+ "aria-label": label ?? "Progress",
4096
+ "aria-valuemin": 0,
4097
+ "aria-valuemax": max,
4098
+ "aria-valuenow": Math.min(value, max),
4099
+ className: "h-1.5 overflow-hidden rounded-full bg-chat-border",
4100
+ children: /* @__PURE__ */ jsx("div", {
4101
+ className: `h-full rounded-full ${fillClass[tone]}`,
4102
+ style: { width: `${share * 100}%` }
4103
+ })
4104
+ })]
4105
+ });
4106
+ };
4107
+ const Checklist = function Checklist({ stateKey, items }) {
4108
+ const [state, set] = useUiState();
4109
+ const done = state[stateKey] ?? [];
4110
+ return /* @__PURE__ */ jsx("ul", {
4111
+ className: "flex flex-col gap-1",
4112
+ children: items.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("label", {
4113
+ className: "flex items-center gap-2 font-chat-sans text-(length:--chat-ui-font-size)",
4114
+ children: [/* @__PURE__ */ jsx("input", {
4115
+ type: "checkbox",
4116
+ className: "accent-(--chat-accent)",
4117
+ checked: done.includes(item),
4118
+ onChange: (e) => set(stateKey, e.target.checked ? [...done, item] : done.filter((d) => d !== item))
4119
+ }), /* @__PURE__ */ jsx("span", {
4120
+ className: done.includes(item) ? "text-chat-subtle line-through" : "",
4121
+ children: item
4122
+ })]
4123
+ }) }, item))
4124
+ });
4125
+ };
4126
+ const Tabs = function Tabs({ stateKey, value, options }) {
4127
+ const [, set] = useUiState();
4128
+ const current = value ?? options[0].id;
4129
+ return /* @__PURE__ */ jsx("div", {
4130
+ role: "tablist",
4131
+ className: "inline-flex max-w-full flex-wrap gap-1 self-start rounded-chat-md border border-chat-border p-0.5",
4132
+ children: options.map((o) => /* @__PURE__ */ jsx(Button, {
4133
+ size: "sm",
4134
+ variant: o.id === current ? "subtle" : "ghost",
4135
+ onPress: () => set(stateKey, o.id),
4136
+ "aria-selected": o.id === current,
4137
+ children: o.label
4138
+ }, o.id))
4139
+ });
4140
+ };
4141
+ const Show = ({ value, equals, fallback, children }) => value === equals || value === void 0 && fallback ? /* @__PURE__ */ jsx(Fragment$1, { children }) : null;
4142
+ /**
4143
+ * The drawing of each component, by name and apart from its spec. Take one to reuse in your own
4144
+ * component: `{ ...uiSpecs.Card, render: uiRenders.Card }`.
4145
+ */
4146
+ const uiRenders = {
4147
+ Stack,
4148
+ Grid,
4149
+ Card,
4150
+ Heading: Heading$1,
4151
+ Text: Text$1,
4152
+ Metric,
4153
+ List,
4154
+ Table,
4155
+ Badge: Badge$1,
4156
+ Progress,
4157
+ Checklist,
4158
+ Tabs,
4159
+ Show,
4160
+ Button: function UiButton({ label, variant = "secondary", onPress }) {
4161
+ const run = useUiAction();
4162
+ return /* @__PURE__ */ jsx(Button, {
4163
+ size: "sm",
4164
+ className: "self-start",
4165
+ variant: variant === "primary" ? "primary" : "subtle",
4166
+ onPress: () => run(onPress),
4167
+ children: label
4168
+ });
4169
+ }
4170
+ };
4171
+ /**
4172
+ * The ready components for `UiComponentsProvider`, in the order of `uiComponentSpecs`. Everything by default;
4173
+ * `include` and `exclude` take component names or the groups `"display"` (Stack, Grid, Card, Heading, Text,
4174
+ * Metric, List, Table, Badge, Progress) and `"interactive"` (Checklist, Tabs, Show, Button). `renders` swaps the
4175
+ * drawing of single components. Build the model's tool from the same picks so it only hears of what you
4176
+ * registered: `uiSpecSet({ include })` in `agentic-ui-kit/core`, or strip the renders from this list.
4177
+ */
4178
+ function uiComponentSet({ renders = {}, ...selection } = {}) {
4179
+ return uiSpecSet(selection).map((spec) => ({
4180
+ ...spec,
4181
+ render: renders[spec.name] ?? uiRenders[spec.name]
4182
+ }));
4183
+ }
4184
+ /** The whole set: `uiComponentSet()`. */
4185
+ const uiComponents = uiComponentSet();
3383
4186
  // src/components/approval-card.tsx
3384
4187
  /**
3385
4188
  * Asks the user before a tool runs, above the composer. Shows the tool's label and what it will do: its
@@ -4738,7 +5541,7 @@ const densityVars = {
4738
5541
  }
4739
5542
  };
4740
5543
  /** A whole chat: header, the conversation, the docked composer. An empty chat shows `emptyState`. */
4741
- function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, onReadAloud, messageActions, renderPart, widgets, header, composer, emptyState, disclaimer, renderComposer, density = "full", contentWidth, contentPadding, className }) {
5544
+ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, prompts, onAnswer, onDecline, onRetry, onEdit, onFeedback, onReadAloud, messageActions, renderPart, widgets, header, composer, emptyState, disclaimer, renderComposer, density = "full", contentWidth, contentPadding, className }) {
4742
5545
  const style = {
4743
5546
  ...densityVars[density],
4744
5547
  ...contentWidth && { "--chat-content-width": contentWidth },
@@ -4753,6 +5556,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
4753
5556
  const [body, short] = useShortPane();
4754
5557
  const labels = useLabels();
4755
5558
  const pending = onApprove && approvals?.[0];
5559
+ const asking = !pending && onAnswer ? prompts?.[0] : void 0;
4756
5560
  const box = (variant) => {
4757
5561
  if (composer === false) return null;
4758
5562
  const element = /* @__PURE__ */ jsx(Composer, {
@@ -4761,7 +5565,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
4761
5565
  onSubmit: onSend,
4762
5566
  ...onStop && { onStop },
4763
5567
  ...composer,
4764
- ...pending && { placeholder: labels.replyDirectly }
5568
+ ...(pending || asking) && { placeholder: labels.replyDirectly }
4765
5569
  });
4766
5570
  return renderComposer ? renderComposer(element, variant === "centered" ? "empty" : "docked") : element;
4767
5571
  };
@@ -4798,6 +5602,14 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
4798
5602
  onRespond: onApprove,
4799
5603
  className: "mb-2"
4800
5604
  }, pending.part.id),
5605
+ asking && /* @__PURE__ */ jsx("div", {
5606
+ className: "mb-2",
5607
+ children: /* @__PURE__ */ jsx(ToolPromptCard, {
5608
+ prompt: asking,
5609
+ onAnswer,
5610
+ onDecline: onDecline ?? (() => {})
5611
+ })
5612
+ }),
4801
5613
  box("docked"),
4802
5614
  /* @__PURE__ */ jsx("div", {
4803
5615
  className: clsx("flex items-center justify-center py-1 pb-[max(0.25rem,env(safe-area-inset-bottom))] text-center font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle", short ? "min-h-2" : "min-h-6"),
@@ -5205,4 +6017,4 @@ function SidebarUser({ name, detail, avatarUrl, menu, popoverProps }) {
5205
6017
  children: menu
5206
6018
  })] });
5207
6019
  }
5208
- export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatListRailButton, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextMeter, ContextPanel, CopyButton, CopyField, DRAWER_QUERY, DialogTrigger, DictationButton, DictationControls, Disclosure, Drawer, EffortButton, EffortSlider, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, PopoverTrigger, SegmentedControl, Sidebar, SidebarItem, SidebarList, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, VoiceMeter, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, encodeWav, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useDictation, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };
6020
+ export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AskUserAnswers, AskUserPrompt, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatListRailButton, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextMeter, ContextPanel, CopyButton, CopyField, DRAWER_QUERY, DialogTrigger, DictationButton, DictationControls, Disclosure, Drawer, EffortButton, EffortSlider, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, PopoverTrigger, SegmentedControl, Sidebar, SidebarItem, SidebarList, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolPromptCard, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, VoiceMeter, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, askUserToolView, commandTool, defaultIcons, defaultLabels, defaultToolViews, encodeWav, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiComponentSet, uiComponents, uiMarkupToolView, uiRenders, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useDictation, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };