@tulipfarm/sprout 0.1.0 → 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 (98) hide show
  1. package/README.md +111 -0
  2. package/dist/components/agent-fanout.d.ts +46 -0
  3. package/dist/components/agent-fanout.js +228 -0
  4. package/dist/components/attachment.js +2 -2
  5. package/dist/components/brand-icon.js +4 -0
  6. package/dist/components/canvas.js +2 -2
  7. package/dist/components/chat-input.d.ts +94 -0
  8. package/dist/components/chat-input.js +279 -0
  9. package/dist/components/chat-sources.js +11 -2
  10. package/dist/components/code-block.js +2 -33
  11. package/dist/components/file-explorer.d.ts +27 -0
  12. package/dist/components/file-explorer.js +300 -0
  13. package/dist/components/file-viewer.d.ts +18 -1
  14. package/dist/components/file-viewer.js +35 -8
  15. package/dist/components/presence-cursor.d.ts +19 -0
  16. package/dist/components/presence-cursor.js +14 -0
  17. package/dist/components/questionnaire.js +2 -1
  18. package/dist/components/schema-diagram.js +5 -78
  19. package/dist/components/search-dialog.js +5 -4
  20. package/dist/components/sql-viewer.d.ts +76 -0
  21. package/dist/components/sql-viewer.js +409 -0
  22. package/dist/components/state-panel.js +2 -2
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +5 -0
  25. package/dist/internal/brand-glyphs.d.ts +28 -0
  26. package/dist/internal/brand-glyphs.js +24 -0
  27. package/dist/internal/code-tokens.d.ts +4 -0
  28. package/dist/internal/code-tokens.js +34 -0
  29. package/dist/internal/flow.d.ts +22 -0
  30. package/dist/internal/flow.js +82 -0
  31. package/dist/styles/accordion.css +2 -2
  32. package/dist/styles/agent-fanout.css +529 -0
  33. package/dist/styles/alert-dialog.css +2 -2
  34. package/dist/styles/alert.css +2 -2
  35. package/dist/styles/attachment.css +4 -24
  36. package/dist/styles/badge.css +2 -2
  37. package/dist/styles/brand-icon.css +2 -2
  38. package/dist/styles/breadcrumb.css +2 -2
  39. package/dist/styles/button.css +2 -2
  40. package/dist/styles/calendar.css +3 -3
  41. package/dist/styles/canvas.css +4 -18
  42. package/dist/styles/carousel.css +2 -2
  43. package/dist/styles/chart.css +2 -2
  44. package/dist/styles/chat-confirmation.css +2 -2
  45. package/dist/styles/chat-input.css +240 -0
  46. package/dist/styles/chat-sources.css +2 -2
  47. package/dist/styles/checkbox.css +2 -3
  48. package/dist/styles/choice-list.css +2 -2
  49. package/dist/styles/code-block.css +2 -2
  50. package/dist/styles/collapsible.css +2 -2
  51. package/dist/styles/combobox.css +6 -6
  52. package/dist/styles/command.css +2 -2
  53. package/dist/styles/context-menu.css +3 -3
  54. package/dist/styles/dialog.css +2 -2
  55. package/dist/styles/dropdown-menu.css +1 -1
  56. package/dist/styles/field.css +2 -3
  57. package/dist/styles/file-explorer.css +517 -0
  58. package/dist/styles/file-viewer.css +113 -16
  59. package/dist/styles/hover-card.css +2 -2
  60. package/dist/styles/index.css +9 -0
  61. package/dist/styles/input-group.css +6 -11
  62. package/dist/styles/input-otp.css +2 -2
  63. package/dist/styles/input.css +3 -4
  64. package/dist/styles/mention-list.css +1 -1
  65. package/dist/styles/menubar.css +3 -3
  66. package/dist/styles/message-response.css +4 -4
  67. package/dist/styles/message-scroller.css +2 -2
  68. package/dist/styles/message.css +5 -5
  69. package/dist/styles/navigation-menu.css +2 -2
  70. package/dist/styles/page-header.css +4 -4
  71. package/dist/styles/plan-outline.css +6 -6
  72. package/dist/styles/presence-cursor.css +68 -0
  73. package/dist/styles/prompt-list.css +2 -2
  74. package/dist/styles/questionnaire.css +2 -17
  75. package/dist/styles/radio-group.css +2 -3
  76. package/dist/styles/reasoning-effort.css +2 -2
  77. package/dist/styles/review-card.css +2 -2
  78. package/dist/styles/rich-text-editor.css +2 -2
  79. package/dist/styles/schema-diagram.css +4 -4
  80. package/dist/styles/scroll-area.css +2 -2
  81. package/dist/styles/search-dialog.css +0 -83
  82. package/dist/styles/select.css +3 -3
  83. package/dist/styles/separator.css +2 -2
  84. package/dist/styles/sheet.css +2 -2
  85. package/dist/styles/sidebar.css +2 -2
  86. package/dist/styles/slider.css +1 -1
  87. package/dist/styles/sql-viewer.css +406 -0
  88. package/dist/styles/state-panel.css +1 -10
  89. package/dist/styles/switch.css +2 -2
  90. package/dist/styles/table.css +10 -10
  91. package/dist/styles/tabs.css +4 -4
  92. package/dist/styles/textarea.css +3 -4
  93. package/dist/styles/themes/tulipfarm.css +89 -99
  94. package/dist/styles/toast.css +2 -2
  95. package/dist/styles/toggle-group.css +2 -2
  96. package/dist/styles/toggle.css +2 -2
  97. package/dist/styles/tool-activity.css +5 -5
  98. package/package.json +27 -6
@@ -0,0 +1,279 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useId, useLayoutEffect, useMemo, useRef, useState, } from "react";
4
+ import { Extension, Node, mergeAttributes, } from "@tiptap/core";
5
+ import { PluginKey } from "@tiptap/pm/state";
6
+ import Suggestion, { exitSuggestion, } from "@tiptap/suggestion";
7
+ import { ArrowUp } from "reicon-react/icons/ArrowUp";
8
+ import { ChevronDown } from "reicon-react/icons/ChevronDown";
9
+ import { Folder } from "reicon-react/icons/Folder";
10
+ import { Plus } from "reicon-react/icons/Plus";
11
+ import { Sliders } from "reicon-react/icons/Sliders";
12
+ import { X } from "reicon-react/icons/X";
13
+ import { cn } from "../internal/classes.js";
14
+ import { FileAttachment } from "./attachment.js";
15
+ import { BrandIcon } from "./brand-icon.js";
16
+ import { Button } from "./button.js";
17
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command.js";
18
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from "./dropdown-menu.js";
19
+ import { InputGroup, InputGroupAddon, InputGroupButton, } from "./input-group.js";
20
+ import { MentionList, MentionOption, MentionStatus } from "./mention-list.js";
21
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
22
+ import { RichTextEditor, } from "./rich-text-editor.js";
23
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
24
+ export const chatInputReferenceKindNames = ["agent", "skill", "file"];
25
+ export const chatInputStatusNames = ["idle", "streaming"];
26
+ const triggers = {
27
+ agent: { char: "@", label: "Agents", empty: "No agents match." },
28
+ skill: { char: "/", label: "Skills", empty: "No skills match." },
29
+ file: { char: "#", label: "Files", empty: "No files match." },
30
+ };
31
+ const referenceNode = Node.create({
32
+ name: "chatReference",
33
+ inline: true,
34
+ group: "inline",
35
+ atom: true,
36
+ selectable: true,
37
+ addAttributes() {
38
+ return {
39
+ kind: {
40
+ default: "file",
41
+ rendered: false,
42
+ parseHTML: (el) => el.getAttribute("data-chat-reference"),
43
+ },
44
+ id: {
45
+ default: "",
46
+ rendered: false,
47
+ parseHTML: (el) => el.getAttribute("data-id"),
48
+ },
49
+ label: {
50
+ default: "",
51
+ rendered: false,
52
+ parseHTML: (el) => el.getAttribute("data-label"),
53
+ },
54
+ };
55
+ },
56
+ parseHTML() {
57
+ return [{ tag: "span[data-chat-reference]" }];
58
+ },
59
+ renderHTML({ node, HTMLAttributes }) {
60
+ const kind = node.attrs.kind;
61
+ return [
62
+ "span",
63
+ mergeAttributes(HTMLAttributes, {
64
+ "data-chat-reference": kind,
65
+ "data-id": node.attrs.id,
66
+ "data-label": node.attrs.label,
67
+ class: "sprout-chat-input-reference",
68
+ }),
69
+ `${triggers[kind]?.char ?? ""}${node.attrs.label}`,
70
+ ];
71
+ },
72
+ renderText({ node }) {
73
+ const kind = node.attrs.kind;
74
+ return `${triggers[kind]?.char ?? ""}${node.attrs.label}`;
75
+ },
76
+ });
77
+ const linkShortcut = Extension.create({
78
+ name: "chatInputLinkShortcut",
79
+ addKeyboardShortcuts() {
80
+ return {
81
+ "Mod-k": ({ editor }) => {
82
+ if (editor.isActive("link"))
83
+ return editor.chain().focus().unsetLink().run();
84
+ const { from, to } = editor.state.selection;
85
+ const text = editor.state.doc.textBetween(from, to).trim();
86
+ const href = /^\S+@\S+\.\S+$/.test(text)
87
+ ? `mailto:${text}`
88
+ : /^(https?:\/\/|www\.)\S+$/i.test(text)
89
+ ? text.startsWith("www.")
90
+ ? `https://${text}`
91
+ : text
92
+ : null;
93
+ if (!href)
94
+ return false;
95
+ return editor.chain().focus().setLink({ href }).run();
96
+ },
97
+ };
98
+ },
99
+ });
100
+ function triggerExtension(kind, source, onState) {
101
+ const key = new PluginKey(`chatInput-${kind}`);
102
+ return Extension.create({
103
+ name: `chatInput-${kind}`,
104
+ addProseMirrorPlugins() {
105
+ return [
106
+ Suggestion({
107
+ editor: this.editor,
108
+ pluginKey: key,
109
+ char: triggers[kind].char,
110
+ allowSpaces: false,
111
+ items: async ({ query }) => (await source.current?.(query)) ?? [],
112
+ command: ({ editor, range, props }) => editor
113
+ .chain()
114
+ .focus()
115
+ .insertContentAt(range, [
116
+ {
117
+ type: "chatReference",
118
+ attrs: { kind, id: props.id, label: props.label },
119
+ },
120
+ { type: "text", text: " " },
121
+ ])
122
+ .run(),
123
+ render: () => {
124
+ let current = null;
125
+ let active = 0;
126
+ const publish = () => {
127
+ if (!current)
128
+ return;
129
+ const items = current.items;
130
+ onState({
131
+ kind,
132
+ items,
133
+ active: Math.min(active, Math.max(0, items.length - 1)),
134
+ select: (index) => {
135
+ const item = items[index];
136
+ if (item)
137
+ current?.command(item);
138
+ },
139
+ });
140
+ };
141
+ return {
142
+ onStart: (props) => {
143
+ current = props;
144
+ active = 0;
145
+ publish();
146
+ },
147
+ onUpdate: (props) => {
148
+ current = props;
149
+ active = 0;
150
+ publish();
151
+ },
152
+ onKeyDown: ({ event, view }) => {
153
+ if (event.key === "Escape") {
154
+ exitSuggestion(view, key);
155
+ return true;
156
+ }
157
+ const count = current?.items.length ?? 0;
158
+ if (!count)
159
+ return false;
160
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
161
+ active =
162
+ (active + (event.key === "ArrowDown" ? 1 : count - 1)) %
163
+ count;
164
+ publish();
165
+ return true;
166
+ }
167
+ if ((event.key === "Enter" && !event.shiftKey) ||
168
+ event.key === "Tab") {
169
+ current.command(current.items[active]);
170
+ return true;
171
+ }
172
+ return false;
173
+ },
174
+ onExit: () => {
175
+ current = null;
176
+ onState(null);
177
+ },
178
+ };
179
+ },
180
+ }),
181
+ ];
182
+ },
183
+ });
184
+ }
185
+ function hasContent(node) {
186
+ if (node.type === "chatReference")
187
+ return true;
188
+ if (node.text?.trim())
189
+ return true;
190
+ return node.content?.some(hasContent) ?? false;
191
+ }
192
+ function textDoc(text) {
193
+ return {
194
+ type: "doc",
195
+ content: [{ type: "paragraph", content: [{ type: "text", text }] }],
196
+ };
197
+ }
198
+ function ModelMark({ model }) {
199
+ if (model.brand)
200
+ return _jsx(BrandIcon, { brand: model.brand, size: "inline" });
201
+ return model.icon ?? null;
202
+ }
203
+ function OptionPicker({ label, search, options, value, onChange, disabled, icon, align, }) {
204
+ const [open, setOpen] = useState(false);
205
+ const selected = options.find((option) => option.id === value);
206
+ return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsxs(PopoverTrigger, { render: _jsx(InputGroupButton, { size: "sm", "aria-label": `${label}: ${selected?.label ?? "None"}`, disabled: disabled, className: "sprout-chat-input-picker" }), children: [icon(selected), _jsx("span", { className: "sprout-chat-input-picker-label", children: selected?.label ?? label }), _jsx(ChevronDown, { size: 12, "aria-hidden": "true" })] }), _jsx(PopoverContent, { size: "picker", side: "top", align: align, sideOffset: 8, className: "sprout-chat-input-menu", children: _jsxs(Command, { children: [_jsx(CommandInput, { "aria-label": search, placeholder: `${search}…` }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "No matches." }), _jsx(CommandGroup, { heading: label, children: options.map((option) => (_jsxs(CommandItem, { value: `${option.label} ${option.description ?? ""}`, "data-checked": option.id === value, disabled: "disabled" in option && !!option.disabled, onSelect: () => {
207
+ onChange(option.id);
208
+ setOpen(false);
209
+ }, children: [icon(option), _jsxs("span", { className: "sprout-chat-input-option-text", children: [_jsx("span", { children: option.label }), option.description && (_jsx("span", { className: "sprout-chat-input-option-description", children: option.description }))] })] }, option.id))) })] })] }) })] }));
210
+ }
211
+ /**
212
+ * A chat message box on `RichTextEditor`. `@` tags agents, `/` adds skills,
213
+ * and `#` adds files. The bar holds attach and mode menus, a project picker,
214
+ * a model picker, and send or stop. Starter prompts sit under an empty box.
215
+ */
216
+ export function ChatInput({ value, onValueChange, onSend, onStop, status = "idle", placeholder = "Ask anything, @agents, /skills, #files…", streamingPlaceholder = "Working on your request…", disabled = false, label = "Message", agents, skills, files, attachments = [], onRemoveAttachment, attachActions, modes, mode = null, onModeChange, projects, project, onProjectChange, models, model, onModelChange, prompts, onPromptSelect, className, }) {
217
+ const editor = useRef(null);
218
+ const form = useRef(null);
219
+ const listId = useId();
220
+ const [panel, setPanel] = useState(null);
221
+ const [panelBelow, setPanelBelow] = useState(false);
222
+ const sources = {
223
+ agent: useRef(agents),
224
+ skill: useRef(skills),
225
+ file: useRef(files),
226
+ };
227
+ sources.agent.current = agents;
228
+ sources.skill.current = skills;
229
+ sources.file.current = files;
230
+ const enabled = `${!!agents}${!!skills}${!!files}`;
231
+ const extensions = useMemo(() => [
232
+ referenceNode,
233
+ linkShortcut,
234
+ ...chatInputReferenceKindNames
235
+ .filter((kind) => sources[kind].current)
236
+ .map((kind) => triggerExtension(kind, sources[kind], setPanel)),
237
+ ],
238
+ // Sources are refs, so only adding or removing a trigger rebuilds the editor.
239
+ [enabled]);
240
+ const streaming = status === "streaming";
241
+ const filled = hasContent(value) || attachments.length > 0;
242
+ const canSend = filled && !disabled && !streaming;
243
+ const activeMode = modes?.find((option) => option.id === mode);
244
+ const optionId = (index) => `${listId}-option-${index}`;
245
+ const panelOpen = !!panel;
246
+ useLayoutEffect(() => {
247
+ const bounds = form.current?.getBoundingClientRect();
248
+ // Near the top of a short screen there's no room above, so the panel opens below.
249
+ if (panel && bounds)
250
+ setPanelBelow(bounds.top < 320 && window.innerHeight - bounds.bottom > bounds.top);
251
+ }, [panelOpen]);
252
+ const send = () => {
253
+ if (canSend)
254
+ onSend();
255
+ };
256
+ const fill = (prompt) => {
257
+ editor.current?.setContent(textDoc(prompt.prompt ?? prompt.label));
258
+ editor.current?.editor()?.commands.focus("end");
259
+ onPromptSelect?.(prompt);
260
+ };
261
+ return (_jsxs("div", { "data-slot": "chat-input", "data-status": status, "data-mode-color": activeMode?.color ? "" : undefined, style: activeMode?.color
262
+ ? {
263
+ "--sprout-chat-input-mode": activeMode.color,
264
+ }
265
+ : undefined, className: cn("sprout-chat-input", className), children: [_jsxs("form", { ref: form, "aria-label": label, className: "sprout-chat-input-form", onSubmit: (event) => {
266
+ event.preventDefault();
267
+ send();
268
+ }, children: [_jsxs(InputGroup, { variant: "composer", className: "sprout-chat-input-box", children: [attachments.length > 0 && (_jsx(InputGroupAddon, { align: "block-start", className: "sprout-chat-input-attachments", children: attachments.map((file) => (_jsx(FileAttachment, { file: file, variant: "chip", viewable: true, preview: true, onRemove: onRemoveAttachment && !disabled
269
+ ? () => onRemoveAttachment(file.id)
270
+ : undefined }, file.id))) })), _jsx(RichTextEditor, { ref: editor, value: value, onChange: onValueChange, onSend: send, extensions: extensions, placeholder: streaming ? streamingPlaceholder : placeholder, disabled: disabled, ariaLabel: label, ariaExpanded: !!panel, ariaControls: panel ? listId : undefined, ariaActiveDescendant: panel?.items.length ? optionId(panel.active) : undefined, className: "sprout-chat-input-editor", toolbarAddon: _jsxs("div", { className: "sprout-chat-input-bar", children: [_jsxs("div", { className: "sprout-chat-input-bar-start", children: [attachActions && attachActions.length > 0 && (_jsxs(DropdownMenu, { children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx(DropdownMenuTrigger, { render: _jsx(InputGroupButton, { size: "icon-sm", "aria-label": "Add files and more", disabled: disabled }) }), children: _jsx(Plus, { size: 16, "aria-hidden": "true" }) }), _jsx(TooltipContent, { children: "Add files and more" })] }), _jsx(DropdownMenuContent, { size: "menu", side: "top", align: "start", className: "sprout-chat-input-menu", children: _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Add" }), attachActions.map((action) => (_jsxs(DropdownMenuItem, { onClick: action.onSelect, children: [action.icon, _jsxs("span", { className: "sprout-chat-input-option-text", children: [_jsx("span", { children: action.label }), action.description && (_jsx("span", { className: "sprout-chat-input-option-description", children: action.description }))] })] }, action.id)))] }) })] })), modes && modes.length > 0 && (_jsxs(DropdownMenu, { children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx(DropdownMenuTrigger, { render: _jsx(InputGroupButton, { size: "icon-sm", "aria-label": `Mode: ${activeMode?.label ?? "Default"}`, disabled: disabled }) }), children: _jsx(Sliders, { size: 16, "aria-hidden": "true" }) }), _jsx(TooltipContent, { children: "Modes" })] }), _jsx(DropdownMenuContent, { size: "menu", side: "top", align: "start", className: "sprout-chat-input-menu", children: _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Mode" }), _jsx(DropdownMenuRadioGroup, { value: mode ?? "", onValueChange: (next) => onModeChange?.(next === mode || !next ? null : String(next)), children: modes.map((option) => (_jsxs(DropdownMenuRadioItem, { value: option.id, closeOnClick: true, style: option.color
271
+ ? {
272
+ "--sprout-chat-input-mode": option.color,
273
+ }
274
+ : undefined, className: option.color
275
+ ? "sprout-chat-input-mode-option"
276
+ : undefined, children: [option.icon, _jsxs("span", { className: "sprout-chat-input-option-text", children: [_jsx("span", { children: option.label }), option.description && (_jsx("span", { className: "sprout-chat-input-option-description", children: option.description }))] })] }, option.id))) })] }) })] })), activeMode && (_jsxs(InputGroupButton, { size: "sm", variant: "secondary", className: "sprout-chat-input-mode", "aria-label": `Turn off ${activeMode.label}`, disabled: disabled, onClick: () => onModeChange?.(null), children: [activeMode.icon, _jsx("span", { className: "sprout-chat-input-picker-label", children: activeMode.label }), _jsx(X, { size: 12, "aria-hidden": "true" })] })), projects && projects.length > 0 && (_jsxs(_Fragment, { children: [_jsx("span", { className: "sprout-chat-input-divider", "aria-hidden": "true" }), _jsx(OptionPicker, { label: "Project", search: "Search projects", options: projects, value: project, onChange: (id) => onProjectChange?.(id), disabled: disabled, icon: (option) => option?.icon ?? (_jsx(Folder, { size: 16, "aria-hidden": "true" })), align: "start" })] }))] }), _jsxs("div", { className: "sprout-chat-input-bar-end", children: [models && models.length > 0 && (_jsx(OptionPicker, { label: "Model", search: "Search models", options: models, value: model, onChange: (id) => onModelChange?.(id), disabled: disabled, icon: (option) => option && _jsx(ModelMark, { model: option }), align: "end" })), streaming ? (_jsx(InputGroupButton, { size: "icon-sm", variant: "default", className: "sprout-chat-input-send", "aria-label": "Stop", onClick: onStop, children: _jsx("span", { className: "sprout-chat-input-stop", "aria-hidden": "true" }) })) : (_jsx(InputGroupButton, { type: "submit", size: "icon-sm", variant: canSend ? "default" : "secondary", className: "sprout-chat-input-send", "aria-label": "Send message", disabled: !canSend, children: _jsx(ArrowUp, { size: 16, "aria-hidden": "true" }) }))] })] }) })] }), panel && (_jsx("div", { className: "sprout-chat-input-panel", "data-side": panelBelow ? "bottom" : "top", children: _jsx(MentionList, { id: listId, label: triggers[panel.kind].label, announcement: panel.items.length
277
+ ? `${panel.items.length} ${triggers[panel.kind].label.toLowerCase()}. Use Up and Down to choose, Enter to add, Escape to close.`
278
+ : triggers[panel.kind].empty, status: panel.items.length ? undefined : (_jsx(MentionStatus, { tone: "empty", children: triggers[panel.kind].empty })), children: panel.items.map((item, index) => (_jsx(MentionOption, { id: optionId(index), active: index === panel.active, onSelect: () => panel.select(index), media: item.media, title: item.label, description: item.description }, item.id))) }) }))] }), prompts && prompts.length > 0 && !filled && !streaming && (_jsx("div", { role: "group", "aria-label": "Suggested prompts", className: "sprout-chat-input-prompts", children: prompts.map((prompt) => (_jsxs(Button, { size: "sm", variant: "outline", disabled: disabled, className: "sprout-chat-input-prompt", onClick: () => fill(prompt), children: [prompt.icon, prompt.label] }, prompt.id))) }))] }));
279
+ }
@@ -53,9 +53,18 @@ export const citationStateKindNames = [
53
53
  */
54
54
  export function CheckedCitation({ label, state, onOpen, onRetry, }) {
55
55
  const popup = useRef(null);
56
- return (_jsxs(Popover, { onOpenChange: (open) => {
57
- if (open)
56
+ const openReason = useRef(undefined);
57
+ return (_jsxs(Popover, { onOpenChange: (open, details) => {
58
+ if (open) {
59
+ openReason.current = details.reason;
58
60
  onOpen();
61
+ return;
62
+ }
63
+ // Base UI keeps a hover-close timer from before a reopen; a passage
64
+ // opened by click or keyboard closes only by click, Escape, or outside press.
65
+ if (details.reason === "trigger-hover" &&
66
+ openReason.current === "trigger-press")
67
+ details.cancel();
59
68
  }, children: [_jsx(PopoverTrigger, { openOnHover: true, delay: hoverDelay, closeDelay: hoverCloseDelay, "data-slot": "checked-citation", className: "sprout-chat-sources-checked", render: _jsx(Button, { variant: "link", size: "task-link", "aria-label": `Source: ${label}` }), children: label }), _jsx(PopoverContent, { ref: popup, "data-citation-state": state.kind, className: "sprout-chat-sources-preview", children: state.kind === "idle" || state.kind === "loading" ? (_jsxs("p", { role: "status", className: "sprout-chat-sources-checking", children: [_jsx(Spinner, { "aria-hidden": "true" }), "Checking access to this source\u2026"] })) : state.kind === "ready" ? (_jsxs("div", { "data-slot": "chat-source", className: "sprout-chat-sources-source", children: [_jsx("p", { className: "sprout-chat-sources-title", children: state.title }), _jsx("p", { className: "sprout-chat-sources-location", children: state.location }), _jsx("p", { className: "sprout-chat-sources-excerpt", "data-preserve-lines": "", children: state.excerpt })] })) : (_jsxs("div", { className: "sprout-chat-sources-source", children: [_jsx("p", { className: "sprout-chat-sources-title", children: state.kind === "unavailable"
60
69
  ? "Source unavailable"
61
70
  : "Couldn’t load this source" }), _jsx("p", { role: "alert", className: "sprout-chat-sources-excerpt", children: state.message }), state.kind === "error" && (_jsx(Button, { variant: "outline", size: "sm", className: "sprout-chat-sources-retry", onClick: () => {
@@ -5,7 +5,8 @@ import { Check } from "reicon-react/icons/Check";
5
5
  import { Copy } from "reicon-react/icons/Copy";
6
6
  import { cn } from "../internal/classes.js";
7
7
  import { codeLanguageIds, codeLanguageNames, } from "../internal/languages.js";
8
- import { codeTokens, embeddedLanguages, isLanguageReady, languageForFilename, loadCodeLanguage, maxHighlightedLength, resolveCodeLanguage, } from "../internal/syntax.js";
8
+ import { tokenClass, useCodeTokens } from "../internal/code-tokens.js";
9
+ import { languageForFilename, loadCodeLanguage, resolveCodeLanguage, } from "../internal/syntax.js";
9
10
  import { Button } from "./button.js";
10
11
  export { languageForFilename };
11
12
  export const codeBlockVariantNames = ["inline", "file", "preview"];
@@ -30,38 +31,6 @@ export async function preloadCodeLanguages(...languages) {
30
31
  return resolved ? [loadCodeLanguage(resolved)] : [];
31
32
  }));
32
33
  }
33
- function useCodeTokens(code, language) {
34
- const [loaded, setLoaded] = useState(0);
35
- const highlightable = language !== null && code.length <= maxHighlightedLength;
36
- const needed = useMemo(() => highlightable ? [language, ...embeddedLanguages(code, language)] : [], [code, language, highlightable]);
37
- const missing = needed.filter((name) => !isLanguageReady(name));
38
- const key = missing.join(" ");
39
- useEffect(() => {
40
- if (!key)
41
- return;
42
- let live = true;
43
- void Promise.all(key.split(" ").map((name) => loadCodeLanguage(name))).then(() => live && setLoaded((value) => value + 1), () => undefined);
44
- return () => {
45
- live = false;
46
- };
47
- }, [key]);
48
- return useMemo(() => (highlightable ? codeTokens(code, language) : null),
49
- // `loaded` and `key` re-run this once the grammars arrive.
50
- [code, language, highlightable, loaded, key]);
51
- }
52
- const fontStyleClasses = [
53
- [1, "sprout-syntax-italic"],
54
- [2, "sprout-syntax-bold"],
55
- [4, "sprout-syntax-underline"],
56
- ];
57
- function tokenClass(token) {
58
- const role = /--sprout-syntax-(?:token-)?([a-z-]+)/.exec(token.color ?? "")?.[1];
59
- const classes = role && role !== "foreground" ? [`sprout-syntax-${role}`] : [];
60
- for (const [flag, name] of fontStyleClasses)
61
- if ((token.fontStyle ?? 0) & flag)
62
- classes.push(name);
63
- return classes.length ? classes.join(" ") : undefined;
64
- }
65
34
  export function CodeBlock({ code, language, filename, lineNumbers = true, wrap = false, variant = "inline", className, ...props }) {
66
35
  const [copyState, setCopyState] = useState("idle");
67
36
  const timer = useRef(undefined);
@@ -0,0 +1,27 @@
1
+ import { type HTMLAttributes } from "react";
2
+ export interface FileExplorerTextFile {
3
+ /** A slash-separated path, such as `agents/scout/persona.md`. Folders come from the paths. */
4
+ path: string;
5
+ content: string;
6
+ }
7
+ export interface FileExplorerUnavailableFile {
8
+ path: string;
9
+ /** Why the file has no text to show, such as “Binary files cannot be displayed.” */
10
+ unavailable: string;
11
+ }
12
+ export type FileExplorerFile = FileExplorerTextFile | FileExplorerUnavailableFile;
13
+ export interface FileExplorerProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
14
+ files: FileExplorerFile[];
15
+ /** Heads the sidebar and names the tree, such as “Soul”. */
16
+ label?: string | undefined;
17
+ /** The file open at first. Its folders start expanded. */
18
+ defaultPath?: string | undefined;
19
+ }
20
+ /** Search stops after this many matching lines, so one common word cannot flood the list. */
21
+ export declare const fileExplorerMaxMatches = 500;
22
+ /**
23
+ * A read-only code explorer: a folder tree, open tabs, and a syntax-colored
24
+ * file pane. Search filters file names and lists matching lines across every
25
+ * file; picking a line opens the file there.
26
+ */
27
+ export declare function FileExplorer({ files, label, defaultPath, className, ...props }: FileExplorerProps): import("react").JSX.Element;