@slipstream-ui/ai 0.0.0-stage → 0.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.
Files changed (111) hide show
  1. package/LICENSE +25 -0
  2. package/dist/index.d.mts +579 -0
  3. package/dist/index.d.ts +579 -0
  4. package/dist/index.js +1778 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +1706 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/dist/manifests/attachment.manifest.json +33 -0
  9. package/dist/manifests/branch.manifest.json +65 -0
  10. package/dist/manifests/chain-thought.manifest.json +39 -0
  11. package/dist/manifests/code-block.manifest.json +50 -0
  12. package/dist/manifests/command-palette.manifest.json +80 -0
  13. package/dist/manifests/confirmation.manifest.json +143 -0
  14. package/dist/manifests/conversation.manifest.json +290 -0
  15. package/dist/manifests/inline-citation.manifest.json +46 -0
  16. package/dist/manifests/message-actions.manifest.json +35 -0
  17. package/dist/manifests/message.manifest.json +235 -0
  18. package/dist/manifests/prompt-input.manifest.json +334 -0
  19. package/dist/manifests/reasoning.manifest.json +177 -0
  20. package/dist/manifests/response.manifest.json +51 -0
  21. package/dist/manifests/source.manifest.json +78 -0
  22. package/dist/manifests/streaming-indicator.manifest.json +39 -0
  23. package/dist/manifests/suggestions.manifest.json +45 -0
  24. package/dist/manifests/tool-call.manifest.json +158 -0
  25. package/package.json +61 -3
  26. package/src/adapter/adapter.test.tsx +83 -0
  27. package/src/adapter/hooks.ts +82 -0
  28. package/src/adapter/index.ts +3 -0
  29. package/src/adapter/messages.ts +49 -0
  30. package/src/adapter/types.ts +24 -0
  31. package/src/components/attachment/Attachment.test.tsx +72 -0
  32. package/src/components/attachment/Attachment.tsx +84 -0
  33. package/src/components/attachment/AttachmentList.tsx +30 -0
  34. package/src/components/attachment/index.ts +5 -0
  35. package/src/components/branch/Branch.test.tsx +52 -0
  36. package/src/components/branch/Branch.tsx +77 -0
  37. package/src/components/branch/index.ts +2 -0
  38. package/src/components/chainthought/ChainThought.test.tsx +34 -0
  39. package/src/components/chainthought/ChainThought.tsx +45 -0
  40. package/src/components/chainthought/index.ts +2 -0
  41. package/src/components/code-block/CodeBlock.test.tsx +46 -0
  42. package/src/components/code-block/CodeBlock.tsx +95 -0
  43. package/src/components/code-block/index.ts +2 -0
  44. package/src/components/command-palette/CommandPalette.test.tsx +74 -0
  45. package/src/components/command-palette/CommandPalette.tsx +156 -0
  46. package/src/components/command-palette/index.ts +2 -0
  47. package/src/components/confirmation/Confirmation.test.tsx +97 -0
  48. package/src/components/confirmation/Confirmation.tsx +146 -0
  49. package/src/components/confirmation/index.ts +2 -0
  50. package/src/components/conversation/Conversation.test.tsx +49 -0
  51. package/src/components/conversation/Conversation.tsx +301 -0
  52. package/src/components/conversation/Conversation.welcome.test.tsx +73 -0
  53. package/src/components/conversation/index.ts +2 -0
  54. package/src/components/inline-citation/InlineCitation.test.tsx +42 -0
  55. package/src/components/inline-citation/InlineCitation.tsx +56 -0
  56. package/src/components/inline-citation/index.ts +2 -0
  57. package/src/components/message/Message.error-edit.test.tsx +60 -0
  58. package/src/components/message/Message.test.tsx +80 -0
  59. package/src/components/message/Message.tsx +205 -0
  60. package/src/components/message/index.ts +2 -0
  61. package/src/components/message-actions/MessageActions.test.tsx +77 -0
  62. package/src/components/message-actions/MessageActions.tsx +134 -0
  63. package/src/components/message-actions/index.ts +2 -0
  64. package/src/components/prompt-input/PromptInput.compound.test.tsx +150 -0
  65. package/src/components/prompt-input/PromptInput.test.tsx +74 -0
  66. package/src/components/prompt-input/PromptInput.tsx +584 -0
  67. package/src/components/prompt-input/index.ts +21 -0
  68. package/src/components/reasoning/Reasoning.duration.test.tsx +35 -0
  69. package/src/components/reasoning/Reasoning.test.tsx +58 -0
  70. package/src/components/reasoning/Reasoning.tsx +85 -0
  71. package/src/components/reasoning/index.ts +2 -0
  72. package/src/components/response/Response.test.tsx +69 -0
  73. package/src/components/response/Response.tsx +75 -0
  74. package/src/components/response/close-incomplete-markdown.ts +47 -0
  75. package/src/components/response/index.ts +3 -0
  76. package/src/components/source/Source.tsx +51 -0
  77. package/src/components/source/Sources.tsx +42 -0
  78. package/src/components/source/index.ts +5 -0
  79. package/src/components/source/source.test.tsx +72 -0
  80. package/src/components/streaming/StreamingIndicator.test.tsx +26 -0
  81. package/src/components/streaming/StreamingIndicator.tsx +46 -0
  82. package/src/components/streaming/index.ts +2 -0
  83. package/src/components/suggestion/Suggestion.test.tsx +43 -0
  84. package/src/components/suggestion/Suggestion.tsx +47 -0
  85. package/src/components/suggestion/index.ts +2 -0
  86. package/src/components/tool-call/ToolCall.test.tsx +45 -0
  87. package/src/components/tool-call/ToolCall.tsx +101 -0
  88. package/src/components/tool-call/index.ts +2 -0
  89. package/src/index.ts +29 -0
  90. package/src/manifests/attachment.manifest.json +33 -0
  91. package/src/manifests/branch.manifest.json +65 -0
  92. package/src/manifests/chain-thought.manifest.json +39 -0
  93. package/src/manifests/code-block.manifest.json +50 -0
  94. package/src/manifests/command-palette.manifest.json +80 -0
  95. package/src/manifests/confirmation.manifest.json +143 -0
  96. package/src/manifests/conversation.manifest.json +290 -0
  97. package/src/manifests/inline-citation.manifest.json +46 -0
  98. package/src/manifests/message-actions.manifest.json +35 -0
  99. package/src/manifests/message.manifest.json +235 -0
  100. package/src/manifests/prompt-input.manifest.json +334 -0
  101. package/src/manifests/reasoning.manifest.json +177 -0
  102. package/src/manifests/response.manifest.json +51 -0
  103. package/src/manifests/source.manifest.json +78 -0
  104. package/src/manifests/streaming-indicator.manifest.json +39 -0
  105. package/src/manifests/suggestions.manifest.json +45 -0
  106. package/src/manifests/tool-call.manifest.json +158 -0
  107. package/src/test/jest-axe.d.ts +16 -0
  108. package/src/test/setup.ts +18 -0
  109. package/src/test/vitest-matchers.d.ts +10 -0
  110. package/src/types/index.ts +40 -0
  111. package/README.md +0 -3
package/dist/index.mjs ADDED
@@ -0,0 +1,1706 @@
1
+ 'use client';
2
+
3
+ // src/components/message/Message.tsx
4
+ import * as React from "react";
5
+ import { Avatar, AvatarImage, AvatarFallback, Button } from "@slipstream-ui/core";
6
+ import { cn } from "@slipstream-ui/core";
7
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
8
+ var Message = React.forwardRef(
9
+ ({
10
+ message,
11
+ className,
12
+ showAvatar = true,
13
+ avatarSrc,
14
+ avatarFallback,
15
+ userLabel = "You",
16
+ assistantLabel = "Assistant",
17
+ renderContent,
18
+ isStreaming = false,
19
+ error,
20
+ onRetry,
21
+ onEditContent,
22
+ labels,
23
+ ...props
24
+ }, ref) => {
25
+ const [isEditing, setIsEditing] = React.useState(false);
26
+ const [draft, setDraft] = React.useState(message.content);
27
+ const t = {
28
+ retry: labels?.retry ?? "Retry",
29
+ edit: labels?.edit ?? "Edit",
30
+ save: labels?.save ?? "Save & resend",
31
+ cancel: labels?.cancel ?? "Cancel"
32
+ };
33
+ const isUser = message.role === "user";
34
+ const isSystem = message.role === "system";
35
+ const displayLabel = isUser ? userLabel : message.role === "assistant" ? assistantLabel : message.role;
36
+ const startEdit = () => {
37
+ setDraft(message.content);
38
+ setIsEditing(true);
39
+ };
40
+ const commitEdit = () => {
41
+ const trimmed = draft.trim();
42
+ if (!trimmed) return;
43
+ setIsEditing(false);
44
+ onEditContent?.(trimmed);
45
+ };
46
+ return /* @__PURE__ */ jsxs(
47
+ "div",
48
+ {
49
+ ref,
50
+ "aria-busy": isStreaming || void 0,
51
+ className: cn(
52
+ "flex gap-3 p-4 rounded-lg",
53
+ isUser ? "bg-primary/10" : "bg-muted",
54
+ isSystem && "border-l-4 border-muted-foreground",
55
+ error && "border border-destructive/40",
56
+ className
57
+ ),
58
+ ...props,
59
+ children: [
60
+ showAvatar && !isSystem && /* @__PURE__ */ jsxs(Avatar, { className: "h-8 w-8 flex-shrink-0", children: [
61
+ avatarSrc && /* @__PURE__ */ jsx(AvatarImage, { src: avatarSrc, alt: displayLabel }),
62
+ /* @__PURE__ */ jsx(AvatarFallback, { className: cn(
63
+ "text-xs font-medium",
64
+ isUser ? "bg-primary text-primary-foreground" : "bg-secondary text-secondary-foreground"
65
+ ), children: avatarFallback || (isUser ? "U" : "A") })
66
+ ] }),
67
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
68
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-1", children: [
69
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: displayLabel }),
70
+ message.timestamp && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: new Date(message.timestamp).toLocaleTimeString() })
71
+ ] }),
72
+ error ? /* @__PURE__ */ jsxs("div", { role: "alert", className: "text-sm", children: [
73
+ /* @__PURE__ */ jsx("div", { className: "text-destructive mb-2", children: error }),
74
+ onRetry && /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: onRetry, children: [
75
+ "\u21BB ",
76
+ t.retry
77
+ ] })
78
+ ] }) : isEditing ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
79
+ /* @__PURE__ */ jsx(
80
+ "textarea",
81
+ {
82
+ value: draft,
83
+ onChange: (event) => setDraft(event.target.value),
84
+ onKeyDown: (event) => {
85
+ if (event.key === "Enter" && !event.shiftKey) {
86
+ event.preventDefault();
87
+ commitEdit();
88
+ }
89
+ if (event.key === "Escape") setIsEditing(false);
90
+ },
91
+ "aria-label": "Edit message",
92
+ autoFocus: true,
93
+ rows: Math.min(6, draft.split("\n").length + 1),
94
+ className: "w-full resize-none rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
95
+ }
96
+ ),
97
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
98
+ /* @__PURE__ */ jsx(Button, { size: "sm", onClick: commitEdit, children: t.save }),
99
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: () => setIsEditing(false), children: t.cancel })
100
+ ] })
101
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "text-sm whitespace-pre-wrap break-words", children: [
102
+ renderContent ? renderContent(message.content) : /* @__PURE__ */ jsxs(Fragment, { children: [
103
+ message.content,
104
+ isStreaming && /* @__PURE__ */ jsx(
105
+ "span",
106
+ {
107
+ className: "inline-block w-[2px] h-[1em] ml-0.5 align-text-bottom bg-current animate-pulse motion-reduce:animate-none",
108
+ "data-testid": "streaming-cursor"
109
+ }
110
+ )
111
+ ] }),
112
+ onEditContent && !isStreaming && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
113
+ "button",
114
+ {
115
+ type: "button",
116
+ onClick: startEdit,
117
+ "aria-label": t.edit,
118
+ className: "mt-1 rounded px-1 py-0.5 text-xs text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
119
+ children: [
120
+ "\u270E ",
121
+ t.edit
122
+ ]
123
+ }
124
+ ) })
125
+ ] })
126
+ ] })
127
+ ]
128
+ }
129
+ );
130
+ }
131
+ );
132
+ Message.displayName = "Message";
133
+ var MemoizedMessage = React.memo(Message);
134
+ MemoizedMessage.displayName = "Message";
135
+
136
+ // src/components/message-actions/MessageActions.tsx
137
+ import * as React2 from "react";
138
+ import { cn as cn2 } from "@slipstream-ui/core";
139
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
140
+ var MessageActions = React2.forwardRef(
141
+ ({ content, onCopy, onFeedback, onRegenerate, autohide = "always", labels, className, ...props }, ref) => {
142
+ const [copied, setCopied] = React2.useState(false);
143
+ const [rating, setRating] = React2.useState();
144
+ const t = {
145
+ copy: labels?.copy ?? "Copy",
146
+ copied: labels?.copied ?? "Copied",
147
+ thumbUp: labels?.thumbUp ?? "Good response",
148
+ thumbDown: labels?.thumbDown ?? "Bad response",
149
+ regenerate: labels?.regenerate ?? "Regenerate"
150
+ };
151
+ const handleCopy = async () => {
152
+ if (!content) return;
153
+ try {
154
+ await navigator.clipboard.writeText(content);
155
+ } catch {
156
+ }
157
+ setCopied(true);
158
+ window.setTimeout(() => setCopied(false), 2e3);
159
+ onCopy?.(content);
160
+ };
161
+ const handleRating = (next) => {
162
+ const updated = rating === next ? void 0 : next;
163
+ setRating(updated);
164
+ onFeedback?.(updated);
165
+ };
166
+ const actionButton = "rounded px-1.5 py-0.5 text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
167
+ const actionButtonIdle = "text-muted-foreground hover:bg-accent hover:text-accent-foreground";
168
+ return /* @__PURE__ */ jsxs2(
169
+ "div",
170
+ {
171
+ ref,
172
+ role: "toolbar",
173
+ "aria-label": "Message actions",
174
+ className: cn2(
175
+ "flex items-center gap-1 mt-2",
176
+ autohide !== "always" && "opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100 motion-reduce:opacity-100",
177
+ className
178
+ ),
179
+ ...props,
180
+ children: [
181
+ content !== void 0 && /* @__PURE__ */ jsx2("button", { type: "button", onClick: handleCopy, className: cn2(actionButton, actionButtonIdle), children: copied ? t.copied : t.copy }),
182
+ /* @__PURE__ */ jsx2(
183
+ "button",
184
+ {
185
+ type: "button",
186
+ onClick: () => handleRating("up"),
187
+ "aria-label": t.thumbUp,
188
+ "aria-pressed": rating === "up",
189
+ className: cn2(
190
+ actionButton,
191
+ rating === "up" ? "text-primary" : actionButtonIdle
192
+ ),
193
+ children: "\u{1F44D}"
194
+ }
195
+ ),
196
+ /* @__PURE__ */ jsx2(
197
+ "button",
198
+ {
199
+ type: "button",
200
+ onClick: () => handleRating("down"),
201
+ "aria-label": t.thumbDown,
202
+ "aria-pressed": rating === "down",
203
+ className: cn2(
204
+ actionButton,
205
+ rating === "down" ? "text-destructive" : actionButtonIdle
206
+ ),
207
+ children: "\u{1F44E}"
208
+ }
209
+ ),
210
+ onRegenerate && /* @__PURE__ */ jsx2("button", { type: "button", onClick: onRegenerate, className: cn2(actionButton, actionButtonIdle), children: t.regenerate })
211
+ ]
212
+ }
213
+ );
214
+ }
215
+ );
216
+ MessageActions.displayName = "MessageActions";
217
+
218
+ // src/components/attachment/Attachment.tsx
219
+ import * as React3 from "react";
220
+ import { Spinner } from "@slipstream-ui/core";
221
+ import { cn as cn3 } from "@slipstream-ui/core";
222
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
223
+ var Attachment = React3.forwardRef(
224
+ ({ attachment, onRemove, className, ...props }, ref) => {
225
+ return /* @__PURE__ */ jsxs3(
226
+ "div",
227
+ {
228
+ ref,
229
+ className: cn3(
230
+ "inline-flex items-center gap-2 rounded-md border border-border bg-muted/50 px-2 py-1.5 text-sm max-w-xs",
231
+ className
232
+ ),
233
+ ...props,
234
+ children: [
235
+ attachment.previewUrl ? /* @__PURE__ */ jsx3(
236
+ "img",
237
+ {
238
+ src: attachment.previewUrl,
239
+ alt: "",
240
+ "aria-hidden": "true",
241
+ className: "h-8 w-8 rounded object-cover flex-shrink-0"
242
+ }
243
+ ) : /* @__PURE__ */ jsx3(
244
+ "span",
245
+ {
246
+ "aria-hidden": "true",
247
+ className: "flex h-8 w-8 items-center justify-center rounded bg-secondary text-[10px] font-semibold uppercase text-secondary-foreground flex-shrink-0",
248
+ children: (attachment.type ?? attachment.name.split(".").pop() ?? "?").slice(0, 4)
249
+ }
250
+ ),
251
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
252
+ /* @__PURE__ */ jsx3("div", { className: "truncate font-medium text-foreground", title: attachment.name, children: attachment.name }),
253
+ /* @__PURE__ */ jsx3("div", { className: "text-xs text-muted-foreground", children: attachment.status === "uploading" ? "Uploading..." : [attachment.size, attachment.type].filter(Boolean).join(" \xB7 ") || null })
254
+ ] }),
255
+ attachment.status === "uploading" && /* @__PURE__ */ jsx3(Spinner, { size: "sm", label: "Uploading", "aria-label": "Uploading", className: "flex-shrink-0" }),
256
+ onRemove && /* @__PURE__ */ jsx3(
257
+ "button",
258
+ {
259
+ type: "button",
260
+ onClick: () => onRemove(attachment),
261
+ "aria-label": `Remove ${attachment.name}`,
262
+ className: "flex-shrink-0 rounded p-0.5 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
263
+ children: "\u2715"
264
+ }
265
+ )
266
+ ]
267
+ }
268
+ );
269
+ }
270
+ );
271
+ Attachment.displayName = "Attachment";
272
+
273
+ // src/components/attachment/AttachmentList.tsx
274
+ import * as React4 from "react";
275
+ import { cn as cn4 } from "@slipstream-ui/core";
276
+ import { jsx as jsx4 } from "react/jsx-runtime";
277
+ var AttachmentList = React4.forwardRef(
278
+ ({ attachments, onRemove, className, ...props }, ref) => {
279
+ if (attachments.length === 0) {
280
+ return null;
281
+ }
282
+ return /* @__PURE__ */ jsx4("div", { ref, className: cn4("flex flex-wrap gap-2", className), ...props, children: attachments.map((attachment) => /* @__PURE__ */ jsx4(Attachment, { attachment, onRemove }, attachment.id)) });
283
+ }
284
+ );
285
+ AttachmentList.displayName = "AttachmentList";
286
+
287
+ // src/components/response/Response.tsx
288
+ import * as React6 from "react";
289
+ import ReactMarkdown from "react-markdown";
290
+ import remarkGfm from "remark-gfm";
291
+ import { cn as cn6 } from "@slipstream-ui/core";
292
+
293
+ // src/components/code-block/CodeBlock.tsx
294
+ import * as React5 from "react";
295
+ import { Highlight, themes } from "prism-react-renderer";
296
+ import { cn as cn5 } from "@slipstream-ui/core";
297
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
298
+ function useIsDark() {
299
+ const [dark, setDark] = React5.useState(false);
300
+ React5.useEffect(() => {
301
+ const el = document.documentElement;
302
+ setDark(el.classList.contains("dark"));
303
+ const observer = new MutationObserver(() => setDark(el.classList.contains("dark")));
304
+ observer.observe(el, { attributes: true, attributeFilter: ["class"] });
305
+ return () => observer.disconnect();
306
+ }, []);
307
+ return dark;
308
+ }
309
+ var CodeBlock = React5.forwardRef(
310
+ ({ code, language = "tsx", showCopyButton = true, className, ...props }, ref) => {
311
+ const [copied, setCopied] = React5.useState(false);
312
+ const isDark = useIsDark();
313
+ const trimmedCode = code.replace(/\n$/, "");
314
+ const handleCopy = async () => {
315
+ try {
316
+ await navigator.clipboard.writeText(trimmedCode);
317
+ } catch {
318
+ }
319
+ setCopied(true);
320
+ window.setTimeout(() => setCopied(false), 2e3);
321
+ };
322
+ return /* @__PURE__ */ jsxs4(
323
+ "div",
324
+ {
325
+ ref,
326
+ className: cn5(
327
+ // bg-background (not muted): Prism light themes assume a white
328
+ // surface — on gray the comment/keyword tokens fall below AA.
329
+ "relative group rounded-md border border-border bg-background overflow-hidden",
330
+ className
331
+ ),
332
+ ...props,
333
+ children: [
334
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between px-3 py-1.5 border-b border-border bg-muted/60", children: [
335
+ /* @__PURE__ */ jsx5("span", { className: "text-xs font-mono text-muted-foreground uppercase", children: language }),
336
+ showCopyButton && /* @__PURE__ */ jsx5(
337
+ "button",
338
+ {
339
+ type: "button",
340
+ onClick: handleCopy,
341
+ "aria-label": copied ? "Copied" : "Copy code",
342
+ className: "text-xs text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded px-1",
343
+ children: copied ? "Copied!" : "Copy"
344
+ }
345
+ )
346
+ ] }),
347
+ /* @__PURE__ */ jsx5(Highlight, { theme: isDark ? themes.vsDark : themes.github, code: trimmedCode, language, children: ({ style, tokens, getLineProps, getTokenProps }) => /* @__PURE__ */ jsx5(
348
+ "pre",
349
+ {
350
+ className: "overflow-x-auto p-4 text-xs leading-relaxed m-0",
351
+ style: { ...style, background: "transparent" },
352
+ children: tokens.map((line, i) => /* @__PURE__ */ jsx5("div", { ...getLineProps({ line }), children: line.map((token, key) => /* @__PURE__ */ jsx5("span", { ...getTokenProps({ token }) }, key)) }, i))
353
+ }
354
+ ) })
355
+ ]
356
+ }
357
+ );
358
+ }
359
+ );
360
+ CodeBlock.displayName = "CodeBlock";
361
+
362
+ // src/components/response/close-incomplete-markdown.ts
363
+ function closeIncompleteMarkdown(markdown) {
364
+ let text = markdown;
365
+ const fenceMatches = text.match(/^`{3,}/gm);
366
+ if (fenceMatches && fenceMatches.length % 2 === 1) {
367
+ text = `${text}
368
+ \`\`\``;
369
+ }
370
+ const firstFence = text.indexOf("```");
371
+ const hasUnclosedFence = fenceMatches !== null && fenceMatches.length % 2 === 1;
372
+ const prose = hasUnclosedFence && firstFence !== -1 ? text.slice(0, firstFence) : text;
373
+ const rest = hasUnclosedFence && firstFence !== -1 ? text.slice(firstFence) : "";
374
+ let repaired = prose;
375
+ for (const marker of ["**", "__", "~~"]) {
376
+ const occurrences = prose.split(marker).length - 1;
377
+ if (occurrences % 2 === 1) {
378
+ repaired += marker;
379
+ }
380
+ }
381
+ for (const marker of ["*", "_"]) {
382
+ const singles = repaired.replace(/\*\*/g, "").replace(/__/g, "").split(marker).length - 1;
383
+ if (singles % 2 === 1) {
384
+ repaired += marker;
385
+ }
386
+ }
387
+ const backtickCount = repaired.replace(/```/g, "").split("`").length - 1;
388
+ if (backtickCount % 2 === 1) {
389
+ repaired += "`";
390
+ }
391
+ return repaired + rest;
392
+ }
393
+
394
+ // src/components/response/Response.tsx
395
+ import { Fragment as Fragment2, jsx as jsx6 } from "react/jsx-runtime";
396
+ var Response = React6.forwardRef(
397
+ ({ children, parseIncompleteMarkdown = true, className, ...props }, ref) => {
398
+ const content = React6.useMemo(
399
+ () => parseIncompleteMarkdown ? closeIncompleteMarkdown(children) : children,
400
+ [children, parseIncompleteMarkdown]
401
+ );
402
+ return /* @__PURE__ */ jsx6(
403
+ "div",
404
+ {
405
+ ref,
406
+ className: cn6(
407
+ "text-sm space-y-3 [&_a]:text-primary [&_a]:underline underline-offset-2",
408
+ "[&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5",
409
+ "[&_h1]:text-lg [&_h1]:font-semibold [&_h2]:text-base [&_h2]:font-semibold",
410
+ "[&_h3]:text-sm [&_h3]:font-semibold",
411
+ "[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground",
412
+ "[&_table]:w-full [&_table]:text-xs [&_th]:border [&_th]:border-border [&_th]:p-1.5 [&_td]:border [&_td]:border-border [&_td]:p-1.5",
413
+ className
414
+ ),
415
+ ...props,
416
+ children: /* @__PURE__ */ jsx6(
417
+ ReactMarkdown,
418
+ {
419
+ remarkPlugins: [remarkGfm],
420
+ components: {
421
+ pre: ({ children: children2 }) => /* @__PURE__ */ jsx6(Fragment2, { children: children2 }),
422
+ code: ({ className: codeClassName, children: codeChildren }) => {
423
+ const raw = String(codeChildren).replace(/\n$/, "");
424
+ const match = /language-(\w+)/.exec(codeClassName ?? "");
425
+ const isInline = !match && !raw.includes("\n");
426
+ if (isInline) {
427
+ return /* @__PURE__ */ jsx6("code", { className: "bg-muted px-1 py-0.5 rounded text-xs font-mono before:content-none after:content-none", children: raw });
428
+ }
429
+ return /* @__PURE__ */ jsx6(CodeBlock, { code: raw, language: match?.[1] ?? "text" });
430
+ }
431
+ },
432
+ children: content
433
+ }
434
+ )
435
+ }
436
+ );
437
+ }
438
+ );
439
+ Response.displayName = "Response";
440
+
441
+ // src/components/confirmation/Confirmation.tsx
442
+ import * as React7 from "react";
443
+ import { Button as Button2 } from "@slipstream-ui/core";
444
+ import { cn as cn7 } from "@slipstream-ui/core";
445
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
446
+ var statusChipClasses = {
447
+ pending: "bg-warning/10 text-warning border-warning/20",
448
+ approved: "bg-success/10 text-success border-success/20",
449
+ rejected: "bg-destructive/10 text-destructive border-destructive/20"
450
+ };
451
+ var pendingVariantClasses = {
452
+ default: "border-warning/40",
453
+ warning: "border-warning/40",
454
+ destructive: "border-destructive/40 bg-destructive/5",
455
+ info: "border-info/40",
456
+ success: "border-success/40"
457
+ };
458
+ var Confirmation = React7.forwardRef(
459
+ ({
460
+ title,
461
+ description,
462
+ toolName,
463
+ status: controlledStatus,
464
+ variant = "default",
465
+ onApprove,
466
+ onReject,
467
+ approveVariant = "default",
468
+ rejectVariant = "outline",
469
+ showActions = true,
470
+ labels,
471
+ className,
472
+ ...props
473
+ }, ref) => {
474
+ const [internalStatus, setInternalStatus] = React7.useState("pending");
475
+ const status = controlledStatus ?? internalStatus;
476
+ const t = {
477
+ approve: labels?.approve ?? "Approve",
478
+ reject: labels?.reject ?? "Reject",
479
+ approved: labels?.approved ?? "Approved",
480
+ rejected: labels?.rejected ?? "Rejected"
481
+ };
482
+ const decide = (next) => {
483
+ setInternalStatus(next);
484
+ (next === "approved" ? onApprove : onReject)?.();
485
+ };
486
+ return /* @__PURE__ */ jsxs5(
487
+ "div",
488
+ {
489
+ ref,
490
+ role: status === "pending" ? "alertdialog" : void 0,
491
+ "aria-label": status === "pending" ? title : void 0,
492
+ className: cn7(
493
+ "rounded-lg border p-4 bg-muted/30 max-w-md",
494
+ status === "pending" && pendingVariantClasses[variant],
495
+ className
496
+ ),
497
+ ...props,
498
+ children: [
499
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-2 mb-2", children: [
500
+ /* @__PURE__ */ jsx7("div", { className: "text-sm font-medium text-foreground", children: title }),
501
+ /* @__PURE__ */ jsx7(
502
+ "span",
503
+ {
504
+ className: cn7(
505
+ "text-xs px-2 py-0.5 rounded-full border whitespace-nowrap",
506
+ statusChipClasses[status]
507
+ ),
508
+ children: status === "approved" ? t.approved : status === "rejected" ? t.rejected : "Pending"
509
+ }
510
+ )
511
+ ] }),
512
+ description && /* @__PURE__ */ jsx7("div", { className: "text-sm text-muted-foreground mb-1", children: description }),
513
+ toolName && /* @__PURE__ */ jsx7("div", { className: "text-xs font-mono text-muted-foreground mb-3", children: toolName }),
514
+ status === "pending" && showActions && /* @__PURE__ */ jsxs5("div", { className: "flex gap-2 mt-2", children: [
515
+ /* @__PURE__ */ jsx7(Button2, { size: "sm", variant: approveVariant, onClick: () => decide("approved"), children: t.approve }),
516
+ /* @__PURE__ */ jsx7(Button2, { size: "sm", variant: rejectVariant, onClick: () => decide("rejected"), children: t.reject })
517
+ ] })
518
+ ]
519
+ }
520
+ );
521
+ }
522
+ );
523
+ Confirmation.displayName = "Confirmation";
524
+
525
+ // src/components/branch/Branch.tsx
526
+ import * as React8 from "react";
527
+ import { cn as cn8 } from "@slipstream-ui/core";
528
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
529
+ var Branch = React8.forwardRef(
530
+ ({ count, selected, defaultSelected = 0, onSelect, labels, className, ...props }, ref) => {
531
+ const [internalIndex, setInternalIndex] = React8.useState(defaultSelected);
532
+ const isControlled = selected !== void 0;
533
+ const index = Math.min(Math.max(isControlled ? selected : internalIndex, 0), count - 1);
534
+ const t = {
535
+ previous: labels?.previous ?? "Previous response",
536
+ next: labels?.next ?? "Next response"
537
+ };
538
+ if (count < 2) return null;
539
+ const goTo = (next) => {
540
+ const clamped = Math.min(Math.max(next, 0), count - 1);
541
+ if (!isControlled) setInternalIndex(clamped);
542
+ onSelect?.(clamped);
543
+ };
544
+ return /* @__PURE__ */ jsxs6(
545
+ "div",
546
+ {
547
+ ref,
548
+ className: cn8("flex items-center gap-1 text-xs text-muted-foreground", className),
549
+ ...props,
550
+ children: [
551
+ /* @__PURE__ */ jsx8(
552
+ "button",
553
+ {
554
+ type: "button",
555
+ onClick: () => goTo(index - 1),
556
+ disabled: index === 0,
557
+ "aria-label": t.previous,
558
+ className: "rounded px-1 hover:bg-accent hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40",
559
+ children: "\u2039"
560
+ }
561
+ ),
562
+ /* @__PURE__ */ jsxs6("span", { "aria-live": "polite", className: "tabular-nums", children: [
563
+ index + 1,
564
+ " / ",
565
+ count
566
+ ] }),
567
+ /* @__PURE__ */ jsx8(
568
+ "button",
569
+ {
570
+ type: "button",
571
+ onClick: () => goTo(index + 1),
572
+ disabled: index === count - 1,
573
+ "aria-label": t.next,
574
+ className: "rounded px-1 hover:bg-accent hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-40",
575
+ children: "\u203A"
576
+ }
577
+ )
578
+ ]
579
+ }
580
+ );
581
+ }
582
+ );
583
+ Branch.displayName = "Branch";
584
+
585
+ // src/components/inline-citation/InlineCitation.tsx
586
+ import * as React9 from "react";
587
+ import { cn as cn9 } from "@slipstream-ui/core";
588
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
589
+ var InlineCitation = React9.forwardRef(
590
+ ({ source, children, className, ...props }, ref) => {
591
+ return /* @__PURE__ */ jsxs7(
592
+ "span",
593
+ {
594
+ ref,
595
+ className: cn9("group relative inline focus-within:outline-none", className),
596
+ ...props,
597
+ children: [
598
+ /* @__PURE__ */ jsxs7(
599
+ "button",
600
+ {
601
+ type: "button",
602
+ "aria-describedby": `citation-${source.id}-card`,
603
+ className: "inline-flex items-baseline rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
604
+ children: [
605
+ children ?? /* @__PURE__ */ jsx9("span", { children: source.title }),
606
+ /* @__PURE__ */ jsx9(
607
+ "span",
608
+ {
609
+ "aria-hidden": "true",
610
+ className: "inline-flex items-center justify-center align-super ml-0.5 h-4 min-w-4 px-1 rounded-full border border-border bg-muted text-[10px] font-medium text-muted-foreground group-hover:text-foreground transition-colors",
611
+ children: source.title.charAt(0).toUpperCase()
612
+ }
613
+ )
614
+ ]
615
+ }
616
+ ),
617
+ /* @__PURE__ */ jsxs7(
618
+ "span",
619
+ {
620
+ id: `citation-${source.id}-card`,
621
+ role: "tooltip",
622
+ className: "pointer-events-none absolute bottom-full left-1/2 z-10 mb-1 -translate-x-1/2 w-64 rounded-md border border-border bg-popover p-2.5 text-xs text-popover-foreground shadow-md opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity",
623
+ children: [
624
+ /* @__PURE__ */ jsx9("span", { className: "block font-medium text-foreground truncate", children: source.title }),
625
+ source.snippet && /* @__PURE__ */ jsx9("span", { className: "mt-0.5 block text-muted-foreground line-clamp-3", children: source.snippet })
626
+ ]
627
+ }
628
+ )
629
+ ]
630
+ }
631
+ );
632
+ }
633
+ );
634
+ InlineCitation.displayName = "InlineCitation";
635
+
636
+ // src/components/conversation/Conversation.tsx
637
+ import * as React10 from "react";
638
+ import { cn as cn10 } from "@slipstream-ui/core";
639
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
640
+ var PINNED_THRESHOLD = 80;
641
+ var DEFAULT_WELCOME_TITLE = "Start the conversation";
642
+ var DEFAULT_WELCOME_DESCRIPTION = "Ask anything \u2014 responses stream in real time.";
643
+ var DEFAULT_WELCOME_SUGGESTIONS = [
644
+ "Explain what you can do",
645
+ "Summarize a topic for me"
646
+ ];
647
+ function transcriptToMarkdown(messages) {
648
+ return messages.map((message) => {
649
+ const speaker = message.role === "user" ? "You" : message.role === "assistant" ? "Assistant" : message.role;
650
+ const time = message.timestamp ? ` (${new Date(message.timestamp).toISOString()})` : "";
651
+ return `**${speaker}${time}:**
652
+
653
+ ${message.content}`;
654
+ }).join("\n\n---\n\n");
655
+ }
656
+ var Conversation = React10.forwardRef(
657
+ ({
658
+ messages,
659
+ className,
660
+ showAvatars = true,
661
+ userAvatarSrc,
662
+ userAvatarFallback,
663
+ assistantAvatarSrc,
664
+ assistantAvatarFallback,
665
+ isLoading = false,
666
+ loadingMessage = "Thinking...",
667
+ renderContent,
668
+ streamingMessageId,
669
+ showScrollButton = true,
670
+ showDownload = false,
671
+ messageErrors,
672
+ onRetryMessage,
673
+ welcomeTitle = DEFAULT_WELCOME_TITLE,
674
+ welcomeDescription = DEFAULT_WELCOME_DESCRIPTION,
675
+ welcomeSuggestions = DEFAULT_WELCOME_SUGGESTIONS,
676
+ onWelcomeSelect,
677
+ hasMoreHistory = false,
678
+ isLoadingMore = false,
679
+ onLoadMore,
680
+ ...props
681
+ }, ref) => {
682
+ const scrollRef = React10.useRef(null);
683
+ const messagesEndRef = React10.useRef(null);
684
+ const [isUnpinned, setIsUnpinned] = React10.useState(false);
685
+ const prevScrollMetrics = React10.useRef(null);
686
+ const [announcement, setAnnouncement] = React10.useState("");
687
+ const prevStreamingRef = React10.useRef(void 0);
688
+ React10.useEffect(() => {
689
+ if (prevStreamingRef.current && !streamingMessageId) {
690
+ setAnnouncement("Response complete.");
691
+ }
692
+ prevStreamingRef.current = streamingMessageId;
693
+ }, [streamingMessageId]);
694
+ React10.useEffect(() => {
695
+ const container = scrollRef.current;
696
+ if (!container) return;
697
+ const updatePinned = () => {
698
+ const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
699
+ setIsUnpinned(distanceFromBottom > PINNED_THRESHOLD);
700
+ };
701
+ updatePinned();
702
+ container.addEventListener("scroll", updatePinned, { passive: true });
703
+ return () => container.removeEventListener("scroll", updatePinned);
704
+ }, []);
705
+ React10.useLayoutEffect(() => {
706
+ const container = scrollRef.current;
707
+ if (!container || prevScrollMetrics.current === null) return;
708
+ const { height, top } = prevScrollMetrics.current;
709
+ container.scrollTop = container.scrollHeight - height + top;
710
+ prevScrollMetrics.current = null;
711
+ }, [messages]);
712
+ const captureAnchor = () => {
713
+ const container = scrollRef.current;
714
+ if (!container) return;
715
+ prevScrollMetrics.current = { height: container.scrollHeight, top: container.scrollTop };
716
+ };
717
+ const handleLoadMore = () => {
718
+ captureAnchor();
719
+ onLoadMore?.();
720
+ };
721
+ React10.useEffect(() => {
722
+ const container = scrollRef.current;
723
+ if (!container) return;
724
+ const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
725
+ if (distanceFromBottom > PINNED_THRESHOLD) return;
726
+ container.scrollTo?.({
727
+ top: container.scrollHeight,
728
+ behavior: streamingMessageId ? "auto" : "smooth"
729
+ });
730
+ }, [messages, streamingMessageId]);
731
+ const scrollToLatest = () => {
732
+ const container = scrollRef.current;
733
+ if (!container) return;
734
+ container.scrollTo?.({ top: container.scrollHeight, behavior: "smooth" });
735
+ };
736
+ const handleDownload = () => {
737
+ const blob = new Blob([transcriptToMarkdown(messages)], { type: "text/markdown" });
738
+ const url = URL.createObjectURL(blob);
739
+ const anchor = document.createElement("a");
740
+ anchor.href = url;
741
+ anchor.download = "conversation.md";
742
+ anchor.click();
743
+ URL.revokeObjectURL(url);
744
+ };
745
+ const isEmpty = messages.length === 0 && !isLoading;
746
+ 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
+ ] }),
803
+ showDownload && messages.length > 0 && /* @__PURE__ */ jsx10(
804
+ "button",
805
+ {
806
+ type: "button",
807
+ onClick: handleDownload,
808
+ "aria-label": "Download transcript",
809
+ className: "absolute top-2 right-2 z-10 rounded-md border border-border bg-background/90 px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
810
+ children: "\u2193 .md"
811
+ }
812
+ ),
813
+ showScrollButton && isUnpinned && /* @__PURE__ */ jsx10(
814
+ "button",
815
+ {
816
+ type: "button",
817
+ onClick: scrollToLatest,
818
+ "aria-label": "Scroll to latest message",
819
+ className: "absolute bottom-3 left-1/2 z-10 -translate-x-1/2 rounded-full border border-border bg-background/95 px-3 py-1.5 text-xs font-medium shadow-sm hover:bg-accent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
820
+ children: "\u2193 Latest"
821
+ }
822
+ )
823
+ ] });
824
+ }
825
+ );
826
+ Conversation.displayName = "Conversation";
827
+
828
+ // src/components/prompt-input/PromptInput.tsx
829
+ import * as React11 from "react";
830
+ import { Button as Button3 } from "@slipstream-ui/core";
831
+ import { cn as cn11 } from "@slipstream-ui/core";
832
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
833
+ function usePromptInputAttachments(options = {}) {
834
+ const { accept, maxFiles, maxFileSize, multiple = true, onError } = options;
835
+ const [files, setFiles] = React11.useState([]);
836
+ const inputRef = React11.useRef(null);
837
+ const add = React11.useCallback(
838
+ (fileList) => {
839
+ const incoming = Array.from(fileList);
840
+ setFiles((prev) => {
841
+ const next = [...prev];
842
+ for (const file of incoming) {
843
+ if (accept && !file.type.match(accept.replace(/\*/g, ".*"))) {
844
+ onError?.({ code: "accept", message: `"${file.name}" is not an accepted file type.` });
845
+ continue;
846
+ }
847
+ if (maxFileSize && file.size > maxFileSize) {
848
+ onError?.({
849
+ code: "max_file_size",
850
+ message: `"${file.name}" exceeds the ${Math.round(maxFileSize / 1024)} KB limit.`
851
+ });
852
+ continue;
853
+ }
854
+ if (maxFiles !== void 0 && next.length >= maxFiles) {
855
+ onError?.({ code: "max_files", message: `At most ${maxFiles} files can be attached.` });
856
+ break;
857
+ }
858
+ if (!multiple && next.length >= 1) {
859
+ onError?.({ code: "max_files", message: "Only one file can be attached." });
860
+ break;
861
+ }
862
+ next.push({
863
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
864
+ name: file.name,
865
+ size: `${Math.max(1, Math.round(file.size / 1024))} KB`,
866
+ type: file.type.split("/")[1]?.toUpperCase() ?? file.name.split(".").pop()?.toUpperCase()
867
+ });
868
+ }
869
+ return next;
870
+ });
871
+ },
872
+ [accept, maxFileSize, maxFiles, multiple, onError]
873
+ );
874
+ const remove = React11.useCallback((id) => {
875
+ setFiles((prev) => prev.filter((file) => file.id !== id));
876
+ }, []);
877
+ const clear = React11.useCallback(() => setFiles([]), []);
878
+ const openFileDialog = React11.useCallback(() => inputRef.current?.click(), []);
879
+ const input = /* @__PURE__ */ jsx11(
880
+ "input",
881
+ {
882
+ ref: inputRef,
883
+ type: "file",
884
+ hidden: true,
885
+ accept,
886
+ multiple,
887
+ onChange: (event) => {
888
+ if (event.target.files) add(event.target.files);
889
+ event.target.value = "";
890
+ }
891
+ }
892
+ );
893
+ return { files, add, remove, clear, openFileDialog, input };
894
+ }
895
+ function useSpeechRecognition(onTranscript) {
896
+ const [isListening, setIsListening] = React11.useState(false);
897
+ const Ctor = typeof window !== "undefined" ? window.SpeechRecognition ?? window.webkitSpeechRecognition : void 0;
898
+ const recognitionRef = React11.useRef(null);
899
+ const toggle = React11.useCallback(() => {
900
+ if (!Ctor) return;
901
+ if (isListening) {
902
+ recognitionRef.current?.stop();
903
+ setIsListening(false);
904
+ return;
905
+ }
906
+ const recognition = new Ctor();
907
+ recognition.lang = navigator.language || "en-US";
908
+ recognition.continuous = false;
909
+ recognition.interimResults = false;
910
+ recognition.onresult = (event) => {
911
+ const transcript = Array.from({ length: event.results.length }, (_, i) => event.results[i][0].transcript).join(" ");
912
+ onTranscript(transcript.trim());
913
+ };
914
+ recognition.onend = () => setIsListening(false);
915
+ recognitionRef.current = recognition;
916
+ recognition.start();
917
+ setIsListening(true);
918
+ }, [Ctor, isListening, onTranscript]);
919
+ return { supported: Boolean(Ctor), isListening, toggle };
920
+ }
921
+ var PromptInputContext = React11.createContext(null);
922
+ function usePromptInputContext() {
923
+ const context = React11.useContext(PromptInputContext);
924
+ if (!context) throw new Error("PromptInput primitives must be rendered inside <PromptInput>.");
925
+ return context;
926
+ }
927
+ var PromptInputRoot = React11.forwardRef(
928
+ ({
929
+ onSubmit,
930
+ onStop,
931
+ attachments: controlledAttachments,
932
+ onRemoveAttachment,
933
+ onAttachmentsChange,
934
+ hasError = false,
935
+ accept,
936
+ multiple = true,
937
+ maxFiles,
938
+ maxFileSize,
939
+ onAttachmentError,
940
+ globalDrop = false,
941
+ placeholder = "Type your message...",
942
+ disabled = false,
943
+ isLoading = false,
944
+ className,
945
+ autoFocus = false,
946
+ maxLength,
947
+ showSendButton = true,
948
+ sendButtonText = "Send",
949
+ stopButtonText = "Stop generating",
950
+ children,
951
+ ...props
952
+ }, ref) => {
953
+ const [value, setValue] = React11.useState("");
954
+ const textareaRef = React11.useRef(null);
955
+ const managed = usePromptInputAttachments({ accept, maxFiles, maxFileSize, multiple, onError: onAttachmentError });
956
+ const prevManagedCount = React11.useRef(0);
957
+ React11.useEffect(() => {
958
+ if (!controlledAttachments && managed.files.length !== prevManagedCount.current) {
959
+ prevManagedCount.current = managed.files.length;
960
+ onAttachmentsChange?.(managed.files);
961
+ }
962
+ }, [managed.files, controlledAttachments, onAttachmentsChange]);
963
+ const activeAttachments = controlledAttachments ?? managed.files;
964
+ React11.useEffect(() => {
965
+ if (autoFocus && textareaRef.current) textareaRef.current.focus();
966
+ }, [autoFocus]);
967
+ React11.useImperativeHandle(ref, () => textareaRef.current);
968
+ React11.useEffect(() => {
969
+ if (textareaRef.current) {
970
+ textareaRef.current.style.height = "auto";
971
+ textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
972
+ }
973
+ }, [value]);
974
+ React11.useEffect(() => {
975
+ if (!globalDrop || controlledAttachments) return;
976
+ const prevent = (event) => event.preventDefault();
977
+ const drop = (event) => {
978
+ event.preventDefault();
979
+ if (event.dataTransfer?.files.length) managed.add(event.dataTransfer.files);
980
+ };
981
+ document.addEventListener("dragover", prevent);
982
+ document.addEventListener("drop", drop);
983
+ return () => {
984
+ document.removeEventListener("dragover", prevent);
985
+ document.removeEventListener("drop", drop);
986
+ };
987
+ }, [globalDrop, controlledAttachments, managed]);
988
+ const canSubmit = Boolean(value.trim()) && !disabled && !isLoading;
989
+ const handleSubmit = () => {
990
+ const trimmedValue = value.trim();
991
+ if (!trimmedValue || disabled || isLoading) return;
992
+ onSubmit(trimmedValue);
993
+ setValue("");
994
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
995
+ if (!controlledAttachments) managed.clear();
996
+ };
997
+ const handleStop = () => onStop?.();
998
+ const handleKeyDown = (event) => {
999
+ if (event.key === "Enter" && !event.shiftKey) {
1000
+ event.preventDefault();
1001
+ handleSubmit();
1002
+ }
1003
+ };
1004
+ const dictate = useSpeechRecognition(
1005
+ (transcript) => setValue((current) => current ? `${current} ${transcript}` : transcript)
1006
+ );
1007
+ const contextValue = {
1008
+ value,
1009
+ setValue,
1010
+ disabled,
1011
+ isLoading,
1012
+ canSubmit,
1013
+ submit: handleSubmit,
1014
+ stop: handleStop,
1015
+ hasStop: Boolean(onStop),
1016
+ attachments: activeAttachments,
1017
+ removeAttachment: (id) => controlledAttachments ? onRemoveAttachment?.({ id }) : managed.remove(id),
1018
+ addFiles: (files) => !controlledAttachments && managed.add(files),
1019
+ openFilePicker: managed.openFileDialog,
1020
+ hiddenInput: managed.input,
1021
+ dictate
1022
+ };
1023
+ const showStop = isLoading && Boolean(onStop);
1024
+ const submitIcon = showStop ? "\u25A0" : hasError ? "\u26A0" : "\u2192";
1025
+ const submitLabel = showStop ? stopButtonText : hasError ? "Failed \u2014 check message" : sendButtonText;
1026
+ const textarea = /* @__PURE__ */ jsx11(
1027
+ "textarea",
1028
+ {
1029
+ ref: textareaRef,
1030
+ value,
1031
+ onChange: (event) => setValue(event.target.value),
1032
+ onKeyDown: handleKeyDown,
1033
+ placeholder,
1034
+ disabled: disabled || isLoading,
1035
+ maxLength,
1036
+ rows: 1,
1037
+ className: cn11(
1038
+ "flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm",
1039
+ "placeholder:text-muted-foreground",
1040
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
1041
+ "disabled:cursor-not-allowed disabled:opacity-50",
1042
+ "min-h-[40px] max-h-[200px] overflow-y-auto"
1043
+ )
1044
+ }
1045
+ );
1046
+ const sendButton = showSendButton ? /* @__PURE__ */ jsx11(
1047
+ Button3,
1048
+ {
1049
+ onClick: showStop ? handleStop : handleSubmit,
1050
+ disabled: showStop ? false : !canSubmit || disabled,
1051
+ variant: showStop ? "outline" : "default",
1052
+ size: "icon",
1053
+ type: "button",
1054
+ "aria-label": submitLabel,
1055
+ children: /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: submitIcon })
1056
+ }
1057
+ ) : null;
1058
+ const dropHandlers = controlledAttachments ? {} : {
1059
+ onDrop: (event) => {
1060
+ event.preventDefault();
1061
+ if (event.dataTransfer.files.length) managed.add(event.dataTransfer.files);
1062
+ },
1063
+ onPaste: (event) => {
1064
+ const files = event.clipboardData?.files;
1065
+ if (files && files.length > 0) {
1066
+ event.preventDefault();
1067
+ managed.add(files);
1068
+ }
1069
+ }
1070
+ };
1071
+ return /* @__PURE__ */ jsx11(PromptInputContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs9("div", { className: cn11("flex flex-col gap-2", className), ...dropHandlers, ...props, children: [
1072
+ activeAttachments.length > 0 && /* @__PURE__ */ jsx11(
1073
+ AttachmentList,
1074
+ {
1075
+ attachments: activeAttachments,
1076
+ onRemove: controlledAttachments ? onRemoveAttachment : (a) => managed.remove(a.id)
1077
+ }
1078
+ ),
1079
+ children ? /* @__PURE__ */ jsxs9(Fragment3, { children: [
1080
+ children,
1081
+ managed.input
1082
+ ] }) : /* @__PURE__ */ jsxs9(Fragment3, { children: [
1083
+ /* @__PURE__ */ jsxs9("div", { className: "flex gap-2 items-end", children: [
1084
+ textarea,
1085
+ sendButton
1086
+ ] }),
1087
+ managed.input
1088
+ ] })
1089
+ ] }) });
1090
+ }
1091
+ );
1092
+ PromptInputRoot.displayName = "PromptInput";
1093
+ var PromptInputTextarea = React11.forwardRef(({ className, onKeyDown, ...props }, ref) => {
1094
+ const { value, setValue, disabled, isLoading, submit } = usePromptInputContext();
1095
+ return /* @__PURE__ */ jsx11(
1096
+ "textarea",
1097
+ {
1098
+ ref,
1099
+ value,
1100
+ onChange: (event) => setValue(event.target.value),
1101
+ onKeyDown: (event) => {
1102
+ if (event.key === "Enter" && !event.shiftKey) {
1103
+ event.preventDefault();
1104
+ submit();
1105
+ }
1106
+ onKeyDown?.(event);
1107
+ },
1108
+ disabled: disabled || isLoading,
1109
+ rows: 1,
1110
+ className: cn11(
1111
+ "flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm",
1112
+ "placeholder:text-muted-foreground",
1113
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
1114
+ "min-h-[40px]",
1115
+ className
1116
+ ),
1117
+ ...props
1118
+ }
1119
+ );
1120
+ });
1121
+ PromptInputTextarea.displayName = "PromptInputTextarea";
1122
+ var PromptInputSubmit = React11.forwardRef(({ className }, ref) => {
1123
+ const { canSubmit, isLoading, hasStop, submit, stop, disabled } = usePromptInputContext();
1124
+ return /* @__PURE__ */ jsx11(
1125
+ Button3,
1126
+ {
1127
+ ref,
1128
+ onClick: isLoading && hasStop ? stop : submit,
1129
+ disabled: isLoading && hasStop ? false : !canSubmit || disabled,
1130
+ variant: isLoading && hasStop ? "outline" : "default",
1131
+ size: "icon",
1132
+ type: "button",
1133
+ "aria-label": isLoading && hasStop ? "Stop generating" : "Send",
1134
+ className,
1135
+ children: /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: isLoading && hasStop ? "\u25A0" : "\u2192" })
1136
+ }
1137
+ );
1138
+ });
1139
+ PromptInputSubmit.displayName = "PromptInputSubmit";
1140
+ function PromptInputFooter({ className, ...props }) {
1141
+ return /* @__PURE__ */ jsx11("div", { className: cn11("flex items-center justify-between gap-2 pt-1", className), ...props });
1142
+ }
1143
+ function PromptInputTools({ className, ...props }) {
1144
+ return /* @__PURE__ */ jsx11("div", { className: cn11("flex items-center gap-1", className), ...props });
1145
+ }
1146
+ function PromptInputHeader({ className, ...props }) {
1147
+ return /* @__PURE__ */ jsx11("div", { className: cn11("mb-1", className), ...props });
1148
+ }
1149
+ function PromptInputBody({ className, ...props }) {
1150
+ return /* @__PURE__ */ jsx11("div", { className: cn11("flex gap-2 items-end", className), ...props });
1151
+ }
1152
+ var PromptInputButton = React11.forwardRef(
1153
+ ({ tooltip, active = false, className, ...props }, ref) => {
1154
+ const tip = typeof tooltip === "string" ? { content: tooltip } : tooltip;
1155
+ return /* @__PURE__ */ jsxs9("span", { className: "relative inline-flex group/tip", children: [
1156
+ /* @__PURE__ */ jsx11(
1157
+ "button",
1158
+ {
1159
+ ref,
1160
+ type: "button",
1161
+ "aria-pressed": active || void 0,
1162
+ className: cn11(
1163
+ "inline-flex h-7 items-center gap-1 rounded-md border border-transparent px-2 text-xs text-muted-foreground transition-colors",
1164
+ "hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1165
+ active && "border-border bg-muted text-foreground",
1166
+ className
1167
+ ),
1168
+ ...props
1169
+ }
1170
+ ),
1171
+ tip && /* @__PURE__ */ jsxs9(
1172
+ "span",
1173
+ {
1174
+ role: "tooltip",
1175
+ className: "pointer-events-none absolute bottom-full left-0 mb-1 z-10 flex items-center gap-1 whitespace-nowrap rounded border border-border bg-popover px-2 py-0.5 text-[11px] text-popover-foreground opacity-0 shadow-sm transition-opacity group-hover/tip:opacity-100",
1176
+ children: [
1177
+ tip.content,
1178
+ tip.shortcut && /* @__PURE__ */ jsx11("kbd", { className: "rounded bg-muted px-1 font-mono", children: tip.shortcut })
1179
+ ]
1180
+ }
1181
+ )
1182
+ ] });
1183
+ }
1184
+ );
1185
+ PromptInputButton.displayName = "PromptInputButton";
1186
+ function PromptInputActionDictate({ labels }) {
1187
+ const { dictate } = usePromptInputContext();
1188
+ if (!dictate.supported) return null;
1189
+ return /* @__PURE__ */ jsx11(
1190
+ PromptInputButton,
1191
+ {
1192
+ onClick: dictate.toggle,
1193
+ active: dictate.isListening,
1194
+ tooltip: { content: dictate.isListening ? labels?.stop ?? "Stop dictating" : labels?.start ?? "Dictate" },
1195
+ "aria-label": dictate.isListening ? labels?.stop ?? "Stop dictation" : labels?.start ?? "Start dictation",
1196
+ children: /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: "\u{1F399}" })
1197
+ }
1198
+ );
1199
+ }
1200
+ function PromptInputActionAddAttachments({ label = "Add photos or files" }) {
1201
+ const { openFilePicker } = usePromptInputContext();
1202
+ return /* @__PURE__ */ jsxs9(PromptInputButton, { onClick: openFilePicker, children: [
1203
+ /* @__PURE__ */ jsx11("span", { "aria-hidden": "true", children: "\uFF0B" }),
1204
+ /* @__PURE__ */ jsx11("span", { children: label })
1205
+ ] });
1206
+ }
1207
+
1208
+ // src/components/reasoning/Reasoning.tsx
1209
+ import * as React12 from "react";
1210
+ import { cn as cn12 } from "@slipstream-ui/core";
1211
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1212
+ var Reasoning = React12.forwardRef(
1213
+ ({ steps, className, collapsible = true, defaultExpanded = false, duration, isStreaming = false, ...props }, ref) => {
1214
+ const [isExpanded, setIsExpanded] = React12.useState(defaultExpanded);
1215
+ const contentId = React12.useId();
1216
+ if (steps.length === 0) {
1217
+ return null;
1218
+ }
1219
+ const headerText = [
1220
+ "Reasoning",
1221
+ `(${steps.length} ${steps.length === 1 ? "step" : "steps"})`,
1222
+ typeof duration === "number" ? `\xB7 ${duration}s` : duration ? `\xB7 ${duration}` : "",
1223
+ isStreaming ? "\xB7 thinking\u2026" : ""
1224
+ ].filter(Boolean).join(" ");
1225
+ return /* @__PURE__ */ jsxs10("div", { ref, className: cn12("border-l-2 border-muted-foreground/30 pl-4", className), ...props, children: [
1226
+ collapsible ? /* @__PURE__ */ jsxs10(
1227
+ "button",
1228
+ {
1229
+ onClick: () => setIsExpanded(!isExpanded),
1230
+ className: "flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors",
1231
+ "aria-expanded": isExpanded,
1232
+ "aria-controls": contentId,
1233
+ children: [
1234
+ /* @__PURE__ */ jsx12("span", { className: cn12("transition-transform", isExpanded ? "rotate-90" : ""), children: "\u25B6" }),
1235
+ /* @__PURE__ */ jsx12("span", { children: headerText }),
1236
+ isStreaming && /* @__PURE__ */ jsx12("span", { className: "flex gap-0.5", "aria-hidden": "true", children: [0, 150, 300].map((delay) => /* @__PURE__ */ jsx12(
1237
+ "span",
1238
+ {
1239
+ className: "h-1 w-1 rounded-full bg-muted-foreground animate-pulse motion-reduce:animate-none",
1240
+ style: { animationDelay: `${delay}ms` }
1241
+ },
1242
+ delay
1243
+ )) })
1244
+ ]
1245
+ }
1246
+ ) : null,
1247
+ (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
+ /* @__PURE__ */ jsx12("div", { className: "w-3 h-3 rounded-full bg-primary/20 border-2 border-primary flex-shrink-0 mt-1" }),
1249
+ /* @__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
+ ] }),
1254
+ /* @__PURE__ */ jsx12("div", { className: "text-muted-foreground", children: step.content }),
1255
+ step.timestamp && /* @__PURE__ */ jsx12("div", { className: "text-xs text-muted-foreground mt-1", children: new Date(step.timestamp).toLocaleTimeString() })
1256
+ ] })
1257
+ ] }, step.id)) })
1258
+ ] });
1259
+ }
1260
+ );
1261
+ Reasoning.displayName = "Reasoning";
1262
+
1263
+ // src/components/tool-call/ToolCall.tsx
1264
+ import * as React13 from "react";
1265
+ import { cn as cn13 } from "@slipstream-ui/core";
1266
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
1267
+ var statusChipClasses2 = {
1268
+ pending: "bg-warning/10 text-warning border-warning/20",
1269
+ running: "bg-info/10 text-info border-info/20",
1270
+ completed: "bg-success/10 text-success border-success/20",
1271
+ failed: "bg-destructive/10 text-destructive border-destructive/20"
1272
+ };
1273
+ var statusLabels = {
1274
+ pending: "Pending",
1275
+ running: "Running",
1276
+ completed: "Completed",
1277
+ failed: "Failed"
1278
+ };
1279
+ var ToolCall = React13.forwardRef(
1280
+ ({ toolCall, className, showResult = true, ...props }, ref) => {
1281
+ const serializedArguments = React13.useMemo(
1282
+ () => JSON.stringify(toolCall.arguments, null, 2),
1283
+ [toolCall.arguments]
1284
+ );
1285
+ const serializedResult = React13.useMemo(() => {
1286
+ if (toolCall.result === void 0) return void 0;
1287
+ return typeof toolCall.result === "string" ? toolCall.result : JSON.stringify(toolCall.result, null, 2);
1288
+ }, [toolCall.result]);
1289
+ return /* @__PURE__ */ jsxs11(
1290
+ "div",
1291
+ {
1292
+ ref,
1293
+ className: cn13(
1294
+ "border rounded-lg p-4 bg-muted/30",
1295
+ className
1296
+ ),
1297
+ ...props,
1298
+ children: [
1299
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center justify-between mb-3", children: [
1300
+ /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-2", children: [
1301
+ /* @__PURE__ */ jsx13("span", { className: "font-medium text-sm", children: toolCall.name }),
1302
+ /* @__PURE__ */ jsx13(
1303
+ "span",
1304
+ {
1305
+ className: cn13(
1306
+ "text-xs px-2 py-0.5 rounded-full border",
1307
+ statusChipClasses2[toolCall.status]
1308
+ ),
1309
+ children: statusLabels[toolCall.status]
1310
+ }
1311
+ )
1312
+ ] }),
1313
+ /* @__PURE__ */ jsx13("span", { className: "text-xs text-muted-foreground", children: toolCall.id })
1314
+ ] }),
1315
+ /* @__PURE__ */ jsxs11("div", { className: "space-y-2", children: [
1316
+ /* @__PURE__ */ jsxs11("div", { children: [
1317
+ /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-muted-foreground mb-1", children: "Arguments" }),
1318
+ /* @__PURE__ */ jsx13("pre", { className: "text-xs bg-muted p-2 rounded overflow-x-auto", children: /* @__PURE__ */ jsx13("code", { children: serializedArguments }) })
1319
+ ] }),
1320
+ showResult && serializedResult !== void 0 && /* @__PURE__ */ jsxs11("div", { children: [
1321
+ /* @__PURE__ */ jsx13("div", { className: "text-xs font-medium text-muted-foreground mb-1", children: "Result" }),
1322
+ /* @__PURE__ */ jsx13("pre", { className: "text-xs bg-muted p-2 rounded overflow-x-auto", children: /* @__PURE__ */ jsx13("code", { children: serializedResult }) })
1323
+ ] }),
1324
+ 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" }),
1326
+ /* @__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
+ ] })
1328
+ ] })
1329
+ ]
1330
+ }
1331
+ );
1332
+ }
1333
+ );
1334
+ ToolCall.displayName = "ToolCall";
1335
+
1336
+ // src/components/source/Source.tsx
1337
+ import * as React14 from "react";
1338
+ import { cn as cn14 } from "@slipstream-ui/core";
1339
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
1340
+ var Source = React14.forwardRef(
1341
+ ({ source, index, className, ...props }, ref) => {
1342
+ return /* @__PURE__ */ jsxs12("div", { ref, className: cn14("flex gap-2 items-start text-sm", className), ...props, children: [
1343
+ index !== void 0 && /* @__PURE__ */ jsxs12("span", { className: "text-muted-foreground select-none", "aria-hidden": "true", children: [
1344
+ "[",
1345
+ index,
1346
+ "]"
1347
+ ] }),
1348
+ /* @__PURE__ */ jsxs12("div", { className: "flex-1 min-w-0", children: [
1349
+ /* @__PURE__ */ jsx14("div", { className: "font-medium text-foreground", children: source.url ? /* @__PURE__ */ jsx14(
1350
+ "a",
1351
+ {
1352
+ href: source.url,
1353
+ target: "_blank",
1354
+ rel: "noopener noreferrer",
1355
+ className: "hover:text-primary transition-colors",
1356
+ children: source.title
1357
+ }
1358
+ ) : /* @__PURE__ */ jsx14("span", { children: source.title }) }),
1359
+ source.snippet && /* @__PURE__ */ jsx14("div", { className: "text-xs text-muted-foreground mt-1 line-clamp-2", children: source.snippet })
1360
+ ] })
1361
+ ] });
1362
+ }
1363
+ );
1364
+ Source.displayName = "Source";
1365
+
1366
+ // src/components/source/Sources.tsx
1367
+ import * as React15 from "react";
1368
+ import { cn as cn15 } from "@slipstream-ui/core";
1369
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
1370
+ var Sources = React15.forwardRef(
1371
+ ({ sources, className, maxDisplay = 3, label = "Sources", ...props }, ref) => {
1372
+ const displaySources = maxDisplay ? sources.slice(0, maxDisplay) : sources;
1373
+ const hasMore = maxDisplay && sources.length > maxDisplay;
1374
+ if (sources.length === 0) {
1375
+ return null;
1376
+ }
1377
+ return /* @__PURE__ */ jsxs13("div", { ref, className: cn15("mt-4 pt-4 border-t border-border", className), ...props, children: [
1378
+ /* @__PURE__ */ jsx15("div", { className: "text-xs font-medium text-muted-foreground mb-2", children: label }),
1379
+ /* @__PURE__ */ jsxs13("div", { className: "space-y-2", children: [
1380
+ displaySources.map((source, index) => /* @__PURE__ */ jsx15(Source, { source, index: index + 1 }, source.id)),
1381
+ hasMore && /* @__PURE__ */ jsxs13("div", { className: "text-xs text-muted-foreground", children: [
1382
+ "+",
1383
+ sources.length - (maxDisplay ?? 0),
1384
+ " more sources"
1385
+ ] })
1386
+ ] })
1387
+ ] });
1388
+ }
1389
+ );
1390
+ Sources.displayName = "Sources";
1391
+
1392
+ // src/components/suggestion/Suggestion.tsx
1393
+ import * as React16 from "react";
1394
+ import { cn as cn16 } from "@slipstream-ui/core";
1395
+ import { jsx as jsx16 } from "react/jsx-runtime";
1396
+ var Suggestions = React16.forwardRef(
1397
+ ({ suggestions, onSelect, className, maxDisplay = 4, ...props }, ref) => {
1398
+ const displaySuggestions = maxDisplay ? suggestions.slice(0, maxDisplay) : suggestions;
1399
+ if (suggestions.length === 0) {
1400
+ return null;
1401
+ }
1402
+ return /* @__PURE__ */ jsx16("div", { ref, className: cn16("mt-4 space-y-2", className), ...props, children: displaySuggestions.map((suggestion) => /* @__PURE__ */ jsx16(
1403
+ "button",
1404
+ {
1405
+ className: cn16(
1406
+ "w-full text-left h-auto py-2 px-3 text-sm rounded-md",
1407
+ "hover:bg-accent hover:text-accent-foreground",
1408
+ "transition-colors",
1409
+ "text-muted-foreground",
1410
+ "focus:outline-none focus:ring-2 focus:ring-ring"
1411
+ ),
1412
+ onClick: () => onSelect?.(suggestion),
1413
+ children: suggestion.text
1414
+ },
1415
+ suggestion.id
1416
+ )) });
1417
+ }
1418
+ );
1419
+ Suggestions.displayName = "Suggestions";
1420
+
1421
+ // src/components/chainthought/ChainThought.tsx
1422
+ import * as React17 from "react";
1423
+ import { cn as cn17 } from "@slipstream-ui/core";
1424
+ import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
1425
+ var ChainThought = React17.forwardRef(
1426
+ ({ steps, className, compact = false, ...props }, ref) => {
1427
+ if (steps.length === 0) {
1428
+ return null;
1429
+ }
1430
+ return /* @__PURE__ */ jsx17("div", { ref, className: cn17("space-y-2", className), ...props, children: steps.map((step, index) => /* @__PURE__ */ jsxs14("div", { className: "flex gap-3 items-start", children: [
1431
+ /* @__PURE__ */ jsx17("div", { className: cn17(
1432
+ "w-2 h-2 rounded-full mt-1.5 flex-shrink-0",
1433
+ step.status === "completed" ? "bg-success" : step.status === "failed" ? "bg-destructive" : step.status === "pending" ? "bg-warning" : "bg-primary"
1434
+ ) }),
1435
+ /* @__PURE__ */ jsxs14("div", { className: cn17("flex-1", compact && "text-sm"), children: [
1436
+ /* @__PURE__ */ jsxs14("div", { className: "text-sm text-muted-foreground", children: [
1437
+ "Step ",
1438
+ index + 1
1439
+ ] }),
1440
+ /* @__PURE__ */ jsx17("div", { className: "text-foreground", children: step.content })
1441
+ ] })
1442
+ ] }, step.id)) });
1443
+ }
1444
+ );
1445
+ ChainThought.displayName = "ChainThought";
1446
+
1447
+ // src/components/streaming/StreamingIndicator.tsx
1448
+ import * as React18 from "react";
1449
+ import { cn as cn18 } from "@slipstream-ui/core";
1450
+ import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
1451
+ var StreamingIndicator = React18.forwardRef(
1452
+ ({ isStreaming, className, text = "Generating...", ...props }, ref) => {
1453
+ if (!isStreaming) {
1454
+ return null;
1455
+ }
1456
+ return /* @__PURE__ */ jsxs15(
1457
+ "div",
1458
+ {
1459
+ ref,
1460
+ role: "status",
1461
+ "aria-live": "polite",
1462
+ "aria-label": text,
1463
+ className: cn18("flex items-center gap-2 text-sm text-muted-foreground", className),
1464
+ ...props,
1465
+ children: [
1466
+ /* @__PURE__ */ jsx18("div", { className: "flex gap-1", "aria-hidden": "true", children: [0, 150, 300].map((delay) => /* @__PURE__ */ jsx18(
1467
+ "span",
1468
+ {
1469
+ className: "w-2 h-2 bg-primary rounded-full animate-pulse motion-reduce:animate-none",
1470
+ style: { animationDelay: `${delay}ms` }
1471
+ },
1472
+ delay
1473
+ )) }),
1474
+ /* @__PURE__ */ jsx18("span", { children: text })
1475
+ ]
1476
+ }
1477
+ );
1478
+ }
1479
+ );
1480
+ StreamingIndicator.displayName = "StreamingIndicator";
1481
+
1482
+ // src/components/command-palette/CommandPalette.tsx
1483
+ import * as React19 from "react";
1484
+ import { cn as cn19 } from "@slipstream-ui/core";
1485
+ import { jsx as jsx19, jsxs as jsxs16 } from "react/jsx-runtime";
1486
+ function CommandPalette({
1487
+ open,
1488
+ onOpenChange,
1489
+ commands,
1490
+ onSelect,
1491
+ placeholder = "Type a command\u2026"
1492
+ }) {
1493
+ const [query, setQuery] = React19.useState("");
1494
+ const [activeIndex, setActiveIndex] = React19.useState(0);
1495
+ const inputRef = React19.useRef(null);
1496
+ const listboxId = React19.useId();
1497
+ const optionId = (index) => `${listboxId}-option-${index}`;
1498
+ const filtered = React19.useMemo(() => {
1499
+ const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
1500
+ if (terms.length === 0) return commands;
1501
+ return commands.filter((command) => {
1502
+ const haystack = `${command.label} ${command.section ?? ""} ${(command.keywords ?? []).join(" ")}`.toLowerCase();
1503
+ return terms.every((term) => haystack.includes(term));
1504
+ });
1505
+ }, [commands, query]);
1506
+ React19.useEffect(() => {
1507
+ if (open) {
1508
+ setQuery("");
1509
+ setActiveIndex(0);
1510
+ const timer = setTimeout(() => inputRef.current?.focus(), 0);
1511
+ return () => clearTimeout(timer);
1512
+ }
1513
+ return void 0;
1514
+ }, [open]);
1515
+ React19.useEffect(() => {
1516
+ setActiveIndex((index) => Math.min(index, Math.max(0, filtered.length - 1)));
1517
+ }, [filtered.length]);
1518
+ if (!open) return null;
1519
+ const onKeyDown = (event) => {
1520
+ if (event.key === "Escape") {
1521
+ event.preventDefault();
1522
+ onOpenChange(false);
1523
+ } else if (event.key === "ArrowDown") {
1524
+ event.preventDefault();
1525
+ setActiveIndex((index) => filtered.length === 0 ? 0 : (index + 1) % filtered.length);
1526
+ } else if (event.key === "ArrowUp") {
1527
+ event.preventDefault();
1528
+ setActiveIndex((index) => filtered.length === 0 ? 0 : (index - 1 + filtered.length) % filtered.length);
1529
+ } else if (event.key === "Enter") {
1530
+ event.preventDefault();
1531
+ const command = filtered[activeIndex];
1532
+ if (command) {
1533
+ onSelect(command);
1534
+ onOpenChange(false);
1535
+ }
1536
+ }
1537
+ };
1538
+ let lastSection;
1539
+ return /* @__PURE__ */ jsx19(
1540
+ "div",
1541
+ {
1542
+ className: "fixed inset-0 z-50 flex items-start justify-center bg-background/80 p-4 pt-[15vh]",
1543
+ onMouseDown: (event) => {
1544
+ if (event.target === event.currentTarget) onOpenChange(false);
1545
+ },
1546
+ children: /* @__PURE__ */ jsxs16("div", { className: "w-full max-w-lg overflow-hidden rounded-xl border border-border bg-card shadow-2xl", children: [
1547
+ /* @__PURE__ */ jsx19(
1548
+ "input",
1549
+ {
1550
+ ref: inputRef,
1551
+ type: "text",
1552
+ role: "combobox",
1553
+ "aria-expanded": "true",
1554
+ "aria-controls": listboxId,
1555
+ "aria-activedescendant": filtered.length > 0 ? optionId(activeIndex) : void 0,
1556
+ "aria-autocomplete": "list",
1557
+ "aria-label": "Command palette",
1558
+ placeholder,
1559
+ value: query,
1560
+ onChange: (event) => {
1561
+ setQuery(event.target.value);
1562
+ setActiveIndex(0);
1563
+ },
1564
+ onKeyDown,
1565
+ 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
+ }
1567
+ ),
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
+ ] })
1599
+ ] })
1600
+ }
1601
+ );
1602
+ }
1603
+
1604
+ // src/adapter/messages.ts
1605
+ function toSlipstreamToolCall(invocation) {
1606
+ const status = invocation.state === "result" ? "completed" : invocation.state === "error" ? "failed" : invocation.state === "call" ? "pending" : "pending";
1607
+ return {
1608
+ id: invocation.toolCallId,
1609
+ name: invocation.toolName,
1610
+ arguments: invocation.args ?? {},
1611
+ status,
1612
+ ...status === "completed" && invocation.result !== void 0 ? { result: invocation.result } : {},
1613
+ ...status === "failed" ? { error: invocation.error instanceof Error ? invocation.error.message : String(invocation.error ?? "Tool call failed") } : {}
1614
+ };
1615
+ }
1616
+ function toSlipstreamToolCalls(message) {
1617
+ return (message.toolInvocations ?? []).map(toSlipstreamToolCall);
1618
+ }
1619
+ function toSlipstreamMessage(message) {
1620
+ return {
1621
+ id: message.id,
1622
+ role: message.role,
1623
+ content: message.content,
1624
+ ...message.toolInvocations?.length ? { metadata: { toolCalls: toSlipstreamToolCalls(message) } } : {}
1625
+ };
1626
+ }
1627
+ function toSlipstreamMessages(messages) {
1628
+ return messages.map(toSlipstreamMessage);
1629
+ }
1630
+
1631
+ // src/adapter/hooks.ts
1632
+ import { useMemo as useMemo4 } from "react";
1633
+ function useSlipstreamMessages({ messages }) {
1634
+ const mapped = useMemo4(() => toSlipstreamMessages(messages), [messages]);
1635
+ return { messages: mapped };
1636
+ }
1637
+ function useSlipstreamStream({
1638
+ status,
1639
+ error,
1640
+ onRetry,
1641
+ messages = []
1642
+ }) {
1643
+ const isLoading = status === "submitted" || status === "streaming";
1644
+ const streamingMessageId = useMemo4(() => {
1645
+ if (status !== "streaming") return void 0;
1646
+ for (let index = messages.length - 1; index >= 0; index--) {
1647
+ const message = messages[index];
1648
+ if (message?.role === "assistant") return message.id;
1649
+ }
1650
+ return messages.at(-1)?.id;
1651
+ }, [status, messages]);
1652
+ const messageErrors = useMemo4(() => {
1653
+ if (status !== "error" || !error) return void 0;
1654
+ const last = messages.at(-1);
1655
+ if (!last) return void 0;
1656
+ return {
1657
+ [last.id]: error instanceof Error ? error.message : String(error)
1658
+ };
1659
+ }, [status, error, messages]);
1660
+ return {
1661
+ isLoading,
1662
+ ...streamingMessageId ? { streamingMessageId } : {},
1663
+ ...messageErrors ? { messageErrors } : {},
1664
+ ...onRetry ? { onRetryMessage: onRetry } : {}
1665
+ };
1666
+ }
1667
+ export {
1668
+ Attachment,
1669
+ AttachmentList,
1670
+ Branch,
1671
+ ChainThought,
1672
+ CodeBlock,
1673
+ CommandPalette,
1674
+ Confirmation,
1675
+ Conversation,
1676
+ InlineCitation,
1677
+ MemoizedMessage as Message,
1678
+ MessageActions,
1679
+ PromptInputRoot as PromptInput,
1680
+ PromptInputActionAddAttachments,
1681
+ PromptInputActionDictate,
1682
+ PromptInputBody,
1683
+ PromptInputButton,
1684
+ PromptInputFooter,
1685
+ PromptInputHeader,
1686
+ PromptInputSubmit,
1687
+ PromptInputTextarea,
1688
+ PromptInputTools,
1689
+ Reasoning,
1690
+ Response,
1691
+ Source,
1692
+ Sources,
1693
+ StreamingIndicator,
1694
+ Suggestions,
1695
+ ToolCall,
1696
+ closeIncompleteMarkdown,
1697
+ toSlipstreamMessage,
1698
+ toSlipstreamMessages,
1699
+ toSlipstreamToolCall,
1700
+ toSlipstreamToolCalls,
1701
+ usePromptInputAttachments,
1702
+ useSlipstreamMessages,
1703
+ useSlipstreamStream,
1704
+ useSpeechRecognition
1705
+ };
1706
+ //# sourceMappingURL=index.mjs.map