@slipstream-ui/ai 0.4.0 → 0.5.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.
Files changed (34) hide show
  1. package/dist/index.d.mts +33 -1
  2. package/dist/index.d.ts +33 -1
  3. package/dist/index.js +236 -110
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +238 -112
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/manifests/attachment.manifest.json +2 -0
  8. package/dist/manifests/branch.manifest.json +2 -0
  9. package/dist/manifests/code-block.manifest.json +2 -0
  10. package/dist/manifests/command-palette.manifest.json +37 -3
  11. package/dist/manifests/confirmation.manifest.json +2 -0
  12. package/dist/manifests/inline-citation.manifest.json +2 -0
  13. package/dist/manifests/message-actions.manifest.json +3 -0
  14. package/dist/manifests/reasoning.manifest.json +6 -0
  15. package/dist/manifests/response.manifest.json +2 -0
  16. package/dist/manifests/tool-call.manifest.json +6 -0
  17. package/package.json +3 -3
  18. package/src/components/code-block/CodeBlock.tsx +91 -5
  19. package/src/components/command-palette/CommandPalette.test.tsx +21 -0
  20. package/src/components/command-palette/CommandPalette.tsx +71 -6
  21. package/src/components/confirmation/Confirmation.tsx +1 -1
  22. package/src/components/conversation/Conversation.tsx +6 -1
  23. package/src/components/reasoning/Reasoning.tsx +13 -6
  24. package/src/components/tool-call/ToolCall.tsx +24 -8
  25. package/src/manifests/attachment.manifest.json +2 -0
  26. package/src/manifests/branch.manifest.json +2 -0
  27. package/src/manifests/code-block.manifest.json +2 -0
  28. package/src/manifests/command-palette.manifest.json +37 -3
  29. package/src/manifests/confirmation.manifest.json +2 -0
  30. package/src/manifests/inline-citation.manifest.json +2 -0
  31. package/src/manifests/message-actions.manifest.json +3 -0
  32. package/src/manifests/reasoning.manifest.json +6 -0
  33. package/src/manifests/response.manifest.json +2 -0
  34. package/src/manifests/tool-call.manifest.json +6 -0
package/dist/index.mjs CHANGED
@@ -292,7 +292,7 @@ import { cn as cn6 } from "@slipstream-ui/core";
292
292
 
293
293
  // src/components/code-block/CodeBlock.tsx
294
294
  import * as React5 from "react";
295
- import { Highlight, themes } from "prism-react-renderer";
295
+ import { Highlight } from "prism-react-renderer";
296
296
  import { cn as cn5 } from "@slipstream-ui/core";
297
297
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
298
298
  function useIsDark() {
@@ -306,11 +306,80 @@ function useIsDark() {
306
306
  }, []);
307
307
  return dark;
308
308
  }
309
+ var LIGHT_CODE_THEME = {
310
+ plain: { color: "#1f2328", backgroundColor: "transparent" },
311
+ styles: [
312
+ { types: ["comment", "prolog", "doctype", "cdata"], style: { color: "#57606a" } },
313
+ { types: ["punctuation", "operator"], style: { color: "#24292f" } },
314
+ { types: ["namespace"], style: { color: "#6f42c1" } },
315
+ { types: ["tag", "deleted", "keyword", "atrule", "important"], style: { color: "#b31d28" } },
316
+ { types: ["attr-name", "variable"], style: { color: "#953800" } },
317
+ { types: ["string", "char", "attr-value", "inserted", "regex"], style: { color: "#0a5c2f" } },
318
+ {
319
+ types: [
320
+ "function",
321
+ "class-name",
322
+ "number",
323
+ "boolean",
324
+ "constant",
325
+ "symbol",
326
+ "property",
327
+ "selector",
328
+ "builtin",
329
+ "entity",
330
+ "url"
331
+ ],
332
+ style: { color: "#0550ae" }
333
+ }
334
+ ]
335
+ };
336
+ var DARK_CODE_THEME = {
337
+ plain: { color: "#e6edf3", backgroundColor: "transparent" },
338
+ styles: [
339
+ { types: ["comment", "prolog", "doctype", "cdata"], style: { color: "#8b949e" } },
340
+ { types: ["punctuation", "operator"], style: { color: "#c9d1d9" } },
341
+ { types: ["namespace"], style: { color: "#d2a8ff" } },
342
+ { types: ["tag", "deleted", "keyword", "atrule", "important"], style: { color: "#ff7b72" } },
343
+ { types: ["attr-name", "variable"], style: { color: "#ffa657" } },
344
+ { types: ["string", "char", "attr-value", "inserted", "regex"], style: { color: "#7ee787" } },
345
+ {
346
+ types: [
347
+ "function",
348
+ "class-name",
349
+ "number",
350
+ "boolean",
351
+ "constant",
352
+ "symbol",
353
+ "property",
354
+ "selector",
355
+ "builtin",
356
+ "entity",
357
+ "url"
358
+ ],
359
+ style: { color: "#79c0ff" }
360
+ }
361
+ ]
362
+ };
309
363
  var CodeBlock = React5.forwardRef(
310
364
  ({ code, language = "tsx", showCopyButton = true, className, ...props }, ref) => {
311
365
  const [copied, setCopied] = React5.useState(false);
312
366
  const isDark = useIsDark();
313
367
  const trimmedCode = code.replace(/\n$/, "");
368
+ const preRef = React5.useRef(null);
369
+ const [scrollable, setScrollable] = React5.useState(false);
370
+ React5.useEffect(() => {
371
+ const element = preRef.current;
372
+ if (!element) return;
373
+ const update = () => setScrollable(element.scrollWidth > element.clientWidth + 1);
374
+ update();
375
+ if (typeof ResizeObserver === "undefined") {
376
+ window.addEventListener("resize", update);
377
+ return () => window.removeEventListener("resize", update);
378
+ }
379
+ const observer = new ResizeObserver(update);
380
+ observer.observe(element);
381
+ return () => observer.disconnect();
382
+ }, [trimmedCode, language]);
314
383
  const handleCopy = async () => {
315
384
  try {
316
385
  await navigator.clipboard.writeText(trimmedCode);
@@ -324,8 +393,8 @@ var CodeBlock = React5.forwardRef(
324
393
  {
325
394
  ref,
326
395
  className: cn5(
327
- // bg-background (not muted): Prism light themes assume a white
328
- // surface — on gray the comment/keyword tokens fall below AA.
396
+ // bg-background (not muted): the light syntax palette is tuned for
397
+ // a white surface; on gray the token contrast drops below AA.
329
398
  "relative group rounded-md border border-border bg-background overflow-hidden",
330
399
  className
331
400
  ),
@@ -344,10 +413,13 @@ var CodeBlock = React5.forwardRef(
344
413
  }
345
414
  )
346
415
  ] }),
347
- /* @__PURE__ */ jsx5(Highlight, { theme: isDark ? themes.vsDark : themes.github, code: trimmedCode, language, children: ({ style, tokens, getLineProps, getTokenProps }) => /* @__PURE__ */ jsx5(
416
+ /* @__PURE__ */ jsx5(Highlight, { theme: isDark ? DARK_CODE_THEME : LIGHT_CODE_THEME, code: trimmedCode, language, children: ({ style, tokens, getLineProps, getTokenProps }) => /* @__PURE__ */ jsx5(
348
417
  "pre",
349
418
  {
350
- className: "overflow-x-auto p-4 text-xs leading-relaxed m-0",
419
+ ref: preRef,
420
+ tabIndex: scrollable ? 0 : void 0,
421
+ "aria-label": scrollable ? `${language} code, scrollable` : void 0,
422
+ className: "overflow-x-auto p-4 text-xs leading-relaxed m-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
351
423
  style: { ...style, background: "transparent" },
352
424
  children: tokens.map((line, i) => /* @__PURE__ */ jsx5("div", { ...getLineProps({ line }), children: line.map((token, key) => /* @__PURE__ */ jsx5("span", { ...getTokenProps({ token }) }, key)) }, i))
353
425
  }
@@ -444,7 +516,7 @@ import { Button as Button2 } from "@slipstream-ui/core";
444
516
  import { cn as cn7 } from "@slipstream-ui/core";
445
517
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
446
518
  var statusChipClasses = {
447
- pending: "bg-warning/10 text-warning border-warning/20",
519
+ pending: "bg-warning/10 text-warning-ink border-warning/20",
448
520
  approved: "bg-success/10 text-success border-success/20",
449
521
  rejected: "bg-destructive/10 text-destructive border-destructive/20"
450
522
  };
@@ -744,62 +816,71 @@ var Conversation = React10.forwardRef(
744
816
  };
745
817
  const isEmpty = messages.length === 0 && !isLoading;
746
818
  return /* @__PURE__ */ jsxs8("div", { ref, className: cn10("relative h-full w-full", className), ...props, children: [
747
- /* @__PURE__ */ jsxs8("div", { ref: scrollRef, className: "flex flex-col gap-4 p-4 overflow-y-auto h-full", children: [
748
- (hasMoreHistory || isLoadingMore) && /* @__PURE__ */ jsx10(
749
- "button",
750
- {
751
- type: "button",
752
- onClick: handleLoadMore,
753
- disabled: isLoadingMore,
754
- className: "mx-auto rounded-full border border-border bg-background px-3 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50",
755
- children: isLoadingMore ? "Loading\u2026" : "Load earlier messages"
756
- }
757
- ),
758
- isEmpty ? /* @__PURE__ */ jsxs8("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-center py-16", children: [
759
- /* @__PURE__ */ jsx10("h3", { className: "text-lg font-semibold", children: welcomeTitle }),
760
- /* @__PURE__ */ jsx10("p", { className: "text-sm text-muted-foreground max-w-sm", children: welcomeDescription }),
761
- welcomeSuggestions.length > 0 && /* @__PURE__ */ jsx10("div", { className: "mt-2 flex flex-col gap-2 w-full max-w-sm", children: welcomeSuggestions.map((suggestion) => /* @__PURE__ */ jsx10(
762
- "button",
763
- {
764
- type: "button",
765
- onClick: () => onWelcomeSelect?.(suggestion),
766
- className: "rounded-md border border-border bg-background px-4 py-2.5 text-sm text-left text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
767
- children: suggestion
768
- },
769
- suggestion
770
- )) })
771
- ] }) : messages.map((message) => /* @__PURE__ */ jsx10(
772
- MemoizedMessage,
773
- {
774
- message,
775
- showAvatar: showAvatars,
776
- avatarSrc: message.role === "user" ? userAvatarSrc : assistantAvatarSrc,
777
- avatarFallback: message.role === "user" ? userAvatarFallback : assistantAvatarFallback,
778
- renderContent,
779
- isStreaming: message.id === streamingMessageId,
780
- error: messageErrors?.[message.id],
781
- onRetry: onRetryMessage && messageErrors?.[message.id] ? () => onRetryMessage(message) : void 0
782
- },
783
- message.id
784
- )),
785
- isLoading && !isEmpty && /* @__PURE__ */ jsx10(
786
- MemoizedMessage,
787
- {
788
- message: {
789
- id: "__loading__",
790
- role: "assistant",
791
- content: loadingMessage
792
- },
793
- showAvatar: showAvatars,
794
- avatarSrc: assistantAvatarSrc,
795
- avatarFallback: assistantAvatarFallback,
796
- className: "bg-muted/50",
797
- renderContent: (content) => /* @__PURE__ */ jsx10("span", { className: "text-muted-foreground animate-pulse motion-reduce:animate-none", children: content })
798
- }
799
- ),
800
- /* @__PURE__ */ jsx10("div", { "aria-live": "polite", className: "sr-only", children: announcement }),
801
- /* @__PURE__ */ jsx10("div", { ref: messagesEndRef })
802
- ] }),
819
+ /* @__PURE__ */ jsxs8(
820
+ "div",
821
+ {
822
+ ref: scrollRef,
823
+ tabIndex: 0,
824
+ "aria-label": "Conversation transcript",
825
+ className: "flex flex-col gap-4 p-4 overflow-y-auto h-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
826
+ children: [
827
+ (hasMoreHistory || isLoadingMore) && /* @__PURE__ */ jsx10(
828
+ "button",
829
+ {
830
+ type: "button",
831
+ onClick: handleLoadMore,
832
+ disabled: isLoadingMore,
833
+ className: "mx-auto rounded-full border border-border bg-background px-3 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50",
834
+ children: isLoadingMore ? "Loading\u2026" : "Load earlier messages"
835
+ }
836
+ ),
837
+ isEmpty ? /* @__PURE__ */ jsxs8("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-center py-16", children: [
838
+ /* @__PURE__ */ jsx10("h3", { className: "text-lg font-semibold", children: welcomeTitle }),
839
+ /* @__PURE__ */ jsx10("p", { className: "text-sm text-muted-foreground max-w-sm", children: welcomeDescription }),
840
+ welcomeSuggestions.length > 0 && /* @__PURE__ */ jsx10("div", { className: "mt-2 flex flex-col gap-2 w-full max-w-sm", children: welcomeSuggestions.map((suggestion) => /* @__PURE__ */ jsx10(
841
+ "button",
842
+ {
843
+ type: "button",
844
+ onClick: () => onWelcomeSelect?.(suggestion),
845
+ className: "rounded-md border border-border bg-background px-4 py-2.5 text-sm text-left text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
846
+ children: suggestion
847
+ },
848
+ suggestion
849
+ )) })
850
+ ] }) : messages.map((message) => /* @__PURE__ */ jsx10(
851
+ MemoizedMessage,
852
+ {
853
+ message,
854
+ showAvatar: showAvatars,
855
+ avatarSrc: message.role === "user" ? userAvatarSrc : assistantAvatarSrc,
856
+ avatarFallback: message.role === "user" ? userAvatarFallback : assistantAvatarFallback,
857
+ renderContent,
858
+ isStreaming: message.id === streamingMessageId,
859
+ error: messageErrors?.[message.id],
860
+ onRetry: onRetryMessage && messageErrors?.[message.id] ? () => onRetryMessage(message) : void 0
861
+ },
862
+ message.id
863
+ )),
864
+ isLoading && !isEmpty && /* @__PURE__ */ jsx10(
865
+ MemoizedMessage,
866
+ {
867
+ message: {
868
+ id: "__loading__",
869
+ role: "assistant",
870
+ content: loadingMessage
871
+ },
872
+ showAvatar: showAvatars,
873
+ avatarSrc: assistantAvatarSrc,
874
+ avatarFallback: assistantAvatarFallback,
875
+ className: "bg-muted/50",
876
+ renderContent: (content) => /* @__PURE__ */ jsx10("span", { className: "text-muted-foreground animate-pulse motion-reduce:animate-none", children: content })
877
+ }
878
+ ),
879
+ /* @__PURE__ */ jsx10("div", { "aria-live": "polite", className: "sr-only", children: announcement }),
880
+ /* @__PURE__ */ jsx10("div", { ref: messagesEndRef })
881
+ ]
882
+ }
883
+ ),
803
884
  showDownload && messages.length > 0 && /* @__PURE__ */ jsx10(
804
885
  "button",
805
886
  {
@@ -1210,17 +1291,17 @@ import * as React12 from "react";
1210
1291
  import { cn as cn12 } from "@slipstream-ui/core";
1211
1292
  import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1212
1293
  var Reasoning = React12.forwardRef(
1213
- ({ steps, className, collapsible = true, defaultExpanded = false, duration, isStreaming = false, ...props }, ref) => {
1294
+ ({ steps, className, collapsible = true, defaultExpanded = false, duration, isStreaming = false, labels, ...props }, ref) => {
1214
1295
  const [isExpanded, setIsExpanded] = React12.useState(defaultExpanded);
1215
1296
  const contentId = React12.useId();
1216
1297
  if (steps.length === 0) {
1217
1298
  return null;
1218
1299
  }
1219
1300
  const headerText = [
1220
- "Reasoning",
1221
- `(${steps.length} ${steps.length === 1 ? "step" : "steps"})`,
1301
+ labels?.title ?? "Reasoning",
1302
+ labels?.count ? labels.count(steps.length) : `(${steps.length} ${steps.length === 1 ? "step" : "steps"})`,
1222
1303
  typeof duration === "number" ? `\xB7 ${duration}s` : duration ? `\xB7 ${duration}` : "",
1223
- isStreaming ? "\xB7 thinking\u2026" : ""
1304
+ isStreaming ? `\xB7 ${labels?.thinking ?? "thinking\u2026"}` : ""
1224
1305
  ].filter(Boolean).join(" ");
1225
1306
  return /* @__PURE__ */ jsxs10("div", { ref, className: cn12("border-l-2 border-muted-foreground/30 pl-4", className), ...props, children: [
1226
1307
  collapsible ? /* @__PURE__ */ jsxs10(
@@ -1231,7 +1312,7 @@ var Reasoning = React12.forwardRef(
1231
1312
  "aria-expanded": isExpanded,
1232
1313
  "aria-controls": contentId,
1233
1314
  children: [
1234
- /* @__PURE__ */ jsx12("span", { className: cn12("transition-transform", isExpanded ? "rotate-90" : ""), children: "\u25B6" }),
1315
+ /* @__PURE__ */ jsx12("span", { className: cn12("transition-transform", isExpanded ? "rotate-90" : ""), children: "?" }),
1235
1316
  /* @__PURE__ */ jsx12("span", { children: headerText }),
1236
1317
  isStreaming && /* @__PURE__ */ jsx12("span", { className: "flex gap-0.5", "aria-hidden": "true", children: [0, 150, 300].map((delay) => /* @__PURE__ */ jsx12(
1237
1318
  "span",
@@ -1247,10 +1328,7 @@ var Reasoning = React12.forwardRef(
1247
1328
  (isExpanded || !collapsible) && /* @__PURE__ */ jsx12("div", { id: contentId, className: "mt-3 space-y-3", children: steps.map((step, index) => /* @__PURE__ */ jsxs10("div", { className: "flex gap-3", children: [
1248
1329
  /* @__PURE__ */ jsx12("div", { className: "w-3 h-3 rounded-full bg-primary/20 border-2 border-primary flex-shrink-0 mt-1" }),
1249
1330
  /* @__PURE__ */ jsxs10("div", { className: "text-sm flex-1", children: [
1250
- /* @__PURE__ */ jsxs10("div", { className: "font-medium text-foreground mb-1", children: [
1251
- "Step ",
1252
- index + 1
1253
- ] }),
1331
+ /* @__PURE__ */ jsx12("div", { className: "font-medium text-foreground mb-1", children: labels?.step?.(index + 1) ?? `Step ${index + 1}` }),
1254
1332
  /* @__PURE__ */ jsx12("div", { className: "text-muted-foreground", children: step.content }),
1255
1333
  step.timestamp && /* @__PURE__ */ jsx12("div", { className: "text-xs text-muted-foreground mt-1", children: new Date(step.timestamp).toLocaleTimeString() })
1256
1334
  ] })
@@ -1265,19 +1343,19 @@ import * as React13 from "react";
1265
1343
  import { cn as cn13 } from "@slipstream-ui/core";
1266
1344
  import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1267
1345
  var statusChipClasses2 = {
1268
- pending: "bg-warning/10 text-warning border-warning/20",
1269
- running: "bg-info/10 text-info border-info/20",
1346
+ pending: "bg-warning/10 text-warning-ink border-warning/20",
1347
+ running: "bg-info/10 text-info-ink border-info/20",
1270
1348
  completed: "bg-success/10 text-success border-success/20",
1271
1349
  failed: "bg-destructive/10 text-destructive border-destructive/20"
1272
1350
  };
1273
- var statusLabels = {
1351
+ var defaultStatusLabels = {
1274
1352
  pending: "Pending",
1275
1353
  running: "Running",
1276
1354
  completed: "Completed",
1277
1355
  failed: "Failed"
1278
1356
  };
1279
1357
  var ToolCall = React13.forwardRef(
1280
- ({ toolCall, className, showResult = true, ...props }, ref) => {
1358
+ ({ toolCall, className, showResult = true, labels, ...props }, ref) => {
1281
1359
  const serializedArguments = React13.useMemo(
1282
1360
  () => JSON.stringify(toolCall.arguments, null, 2),
1283
1361
  [toolCall.arguments]
@@ -1286,6 +1364,12 @@ var ToolCall = React13.forwardRef(
1286
1364
  if (toolCall.result === void 0) return void 0;
1287
1365
  return typeof toolCall.result === "string" ? toolCall.result : JSON.stringify(toolCall.result, null, 2);
1288
1366
  }, [toolCall.result]);
1367
+ const statusLabels = {
1368
+ pending: labels?.pending ?? defaultStatusLabels.pending,
1369
+ running: labels?.running ?? defaultStatusLabels.running,
1370
+ completed: labels?.completed ?? defaultStatusLabels.completed,
1371
+ failed: labels?.failed ?? defaultStatusLabels.failed
1372
+ };
1289
1373
  return /* @__PURE__ */ jsxs11(
1290
1374
  "div",
1291
1375
  {
@@ -1314,15 +1398,15 @@ var ToolCall = React13.forwardRef(
1314
1398
  ] }),
1315
1399
  /* @__PURE__ */ jsxs11("div", { className: "space-y-2", children: [
1316
1400
  /* @__PURE__ */ jsxs11("div", { children: [
1317
- /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-muted-foreground mb-1", children: "Arguments" }),
1401
+ /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-muted-foreground mb-1", children: labels?.arguments ?? "Arguments" }),
1318
1402
  /* @__PURE__ */ jsx13("pre", { className: "text-xs bg-muted p-2 rounded overflow-x-auto", children: /* @__PURE__ */ jsx13("code", { children: serializedArguments }) })
1319
1403
  ] }),
1320
1404
  showResult && serializedResult !== void 0 && /* @__PURE__ */ jsxs11("div", { children: [
1321
- /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-muted-foreground mb-1", children: "Result" }),
1405
+ /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-muted-foreground mb-1", children: labels?.result ?? "Result" }),
1322
1406
  /* @__PURE__ */ jsx13("pre", { className: "text-xs bg-muted p-2 rounded overflow-x-auto", children: /* @__PURE__ */ jsx13("code", { children: serializedResult }) })
1323
1407
  ] }),
1324
1408
  toolCall.status === "failed" && toolCall.error && /* @__PURE__ */ jsxs11("div", { role: "alert", children: [
1325
- /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-destructive mb-1", children: "Error" }),
1409
+ /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-destructive mb-1", children: labels?.error ?? "Error" }),
1326
1410
  /* @__PURE__ */ jsx13("pre", { className: "text-xs bg-destructive/10 text-destructive p-2 rounded overflow-x-auto whitespace-pre-wrap", children: /* @__PURE__ */ jsx13("code", { children: toolCall.error }) })
1327
1411
  ] })
1328
1412
  ] })
@@ -1482,27 +1566,52 @@ StreamingIndicator.displayName = "StreamingIndicator";
1482
1566
  // src/components/command-palette/CommandPalette.tsx
1483
1567
  import * as React19 from "react";
1484
1568
  import { cn as cn19 } from "@slipstream-ui/core";
1485
- import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
1569
+ import { Fragment as Fragment5, jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
1486
1570
  function CommandPalette({
1487
1571
  open,
1488
1572
  onOpenChange,
1489
1573
  commands,
1490
1574
  onSelect,
1491
- placeholder = "Type a command\u2026"
1575
+ loadCommands,
1576
+ debounceMs = 200,
1577
+ renderCommand,
1578
+ loadingText = "Searching\u2026",
1579
+ placeholder = "Type a command."
1492
1580
  }) {
1493
1581
  const [query, setQuery] = React19.useState("");
1494
1582
  const [activeIndex, setActiveIndex] = React19.useState(0);
1583
+ const [asyncCommands, setAsyncCommands] = React19.useState([]);
1584
+ const [loading, setLoading] = React19.useState(false);
1585
+ const requestId = React19.useRef(0);
1586
+ const asyncSignature = loadCommands ? String(loadCommands.length) : "";
1495
1587
  const inputRef = React19.useRef(null);
1496
1588
  const listboxId = React19.useId();
1497
1589
  const optionId = (index) => `${listboxId}-option-${index}`;
1590
+ React19.useEffect(() => {
1591
+ if (!open || !loadCommands) return;
1592
+ const id = ++requestId.current;
1593
+ setLoading(true);
1594
+ const timer = setTimeout(async () => {
1595
+ try {
1596
+ const result = await loadCommands(query);
1597
+ if (requestId.current === id) setAsyncCommands(result);
1598
+ } catch {
1599
+ if (requestId.current === id) setAsyncCommands([]);
1600
+ } finally {
1601
+ if (requestId.current === id) setLoading(false);
1602
+ }
1603
+ }, debounceMs);
1604
+ return () => clearTimeout(timer);
1605
+ }, [open, query, debounceMs, asyncSignature]);
1498
1606
  const filtered = React19.useMemo(() => {
1607
+ if (loadCommands) return asyncCommands;
1499
1608
  const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
1500
1609
  if (terms.length === 0) return commands;
1501
1610
  return commands.filter((command) => {
1502
1611
  const haystack = `${command.label} ${command.section ?? ""} ${(command.keywords ?? []).join(" ")}`.toLowerCase();
1503
1612
  return terms.every((term) => haystack.includes(term));
1504
1613
  });
1505
- }, [commands, query]);
1614
+ }, [commands, query, asyncCommands, loadCommands]);
1506
1615
  React19.useEffect(() => {
1507
1616
  if (open) {
1508
1617
  setQuery("");
@@ -1565,37 +1674,54 @@ function CommandPalette({
1565
1674
  className: "w-full border-b border-border bg-transparent px-4 py-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring placeholder:text-muted-foreground"
1566
1675
  }
1567
1676
  ),
1568
- /* @__PURE__ */ jsxs16("ul", { id: listboxId, role: "listbox", "aria-label": "Commands", className: "max-h-72 overflow-y-auto py-1", children: [
1569
- filtered.length === 0 && /* @__PURE__ */ jsx19("li", { role: "presentation", className: "px-4 py-3 text-sm text-muted-foreground", children: "No matching commands." }),
1570
- filtered.map((command, index) => {
1571
- const showSection = command.section && command.section !== lastSection;
1572
- lastSection = command.section ?? lastSection;
1573
- return /* @__PURE__ */ jsxs16(React19.Fragment, { children: [
1574
- showSection && /* @__PURE__ */ jsx19("li", { role: "presentation", className: "px-4 pb-1 pt-3 text-xs font-medium uppercase text-muted-foreground", children: command.section }),
1575
- /* @__PURE__ */ jsxs16(
1576
- "li",
1577
- {
1578
- id: optionId(index),
1579
- role: "option",
1580
- "aria-selected": index === activeIndex,
1581
- onMouseEnter: () => setActiveIndex(index),
1582
- onClick: () => {
1583
- onSelect(command);
1584
- onOpenChange(false);
1585
- },
1586
- className: cn19(
1587
- "flex cursor-pointer items-center justify-between px-4 py-2 text-sm",
1588
- index === activeIndex ? "bg-primary/10 text-foreground" : "text-muted-foreground"
1589
- ),
1590
- children: [
1591
- /* @__PURE__ */ jsx19("span", { children: command.label }),
1592
- command.shortcut && /* @__PURE__ */ jsx19("span", { className: "font-mono text-xs", children: command.shortcut })
1593
- ]
1594
- }
1595
- )
1596
- ] }, command.id);
1597
- })
1598
- ] })
1677
+ /* @__PURE__ */ jsxs16(
1678
+ "ul",
1679
+ {
1680
+ id: listboxId,
1681
+ role: "listbox",
1682
+ "aria-label": "Commands",
1683
+ "aria-busy": loading || void 0,
1684
+ className: "max-h-72 overflow-y-auto py-1",
1685
+ children: [
1686
+ loading && filtered.length === 0 && /* @__PURE__ */ jsx19("li", { role: "presentation", className: "px-4 py-3 text-sm text-muted-foreground", children: loadingText }),
1687
+ !loading && filtered.length === 0 && /* @__PURE__ */ jsx19("li", { role: "presentation", className: "px-4 py-3 text-sm text-muted-foreground", children: "No matching commands." }),
1688
+ filtered.map((command, index) => {
1689
+ const showSection = command.section && command.section !== lastSection;
1690
+ lastSection = command.section ?? lastSection;
1691
+ return /* @__PURE__ */ jsxs16(React19.Fragment, { children: [
1692
+ showSection && /* @__PURE__ */ jsx19("li", { role: "presentation", className: "px-4 pb-1 pt-3 text-xs font-medium uppercase text-muted-foreground", children: command.section }),
1693
+ /* @__PURE__ */ jsx19(
1694
+ "li",
1695
+ {
1696
+ id: optionId(index),
1697
+ role: "option",
1698
+ "aria-selected": index === activeIndex,
1699
+ onMouseEnter: () => setActiveIndex(index),
1700
+ onClick: () => {
1701
+ onSelect(command);
1702
+ onOpenChange(false);
1703
+ },
1704
+ className: cn19(
1705
+ "flex cursor-pointer items-center justify-between px-4 py-2 text-sm",
1706
+ index === activeIndex ? "bg-primary/10 text-foreground" : "text-muted-foreground"
1707
+ ),
1708
+ children: renderCommand ? renderCommand(command, { active: index === activeIndex }) : /* @__PURE__ */ jsxs16(Fragment5, { children: [
1709
+ /* @__PURE__ */ jsxs16("span", { className: "flex min-w-0 items-center gap-2", children: [
1710
+ command.icon && /* @__PURE__ */ jsx19("span", { className: "shrink-0 [&_svg]:size-4", children: command.icon }),
1711
+ /* @__PURE__ */ jsxs16("span", { className: "min-w-0", children: [
1712
+ /* @__PURE__ */ jsx19("span", { className: "block truncate", children: command.label }),
1713
+ command.description && /* @__PURE__ */ jsx19("span", { className: "block text-xs text-muted-foreground", children: command.description })
1714
+ ] })
1715
+ ] }),
1716
+ command.shortcut && /* @__PURE__ */ jsx19("span", { className: "font-mono text-xs", children: command.shortcut })
1717
+ ] })
1718
+ }
1719
+ )
1720
+ ] }, command.id);
1721
+ })
1722
+ ]
1723
+ }
1724
+ )
1599
1725
  ] })
1600
1726
  }
1601
1727
  );