@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
package/README.md CHANGED
@@ -94,6 +94,117 @@ Each component imports from `@tulipfarm/sprout/<name>`, for example
94
94
  `@tulipfarm/sprout/dialog`. See the [component reference](https://github.com/TulipFarm/sprout/blob/main/docs/components.md)
95
95
  for notes on each one.
96
96
 
97
+ ### Bundle size
98
+
99
+ <!-- bundle-size:start -->
100
+
101
+ Full library: **786.3 kB** gzip. Each component path is smaller. Sizes are minified, with React and other peer dependencies left out.
102
+
103
+ <details>
104
+ <summary>Size of each component</summary>
105
+
106
+ | Component | Raw | Gzip | Brotli |
107
+ | ------------------- | -------: | -------: | -------: |
108
+ | `accordion` | 64.1 kB | 21.9 kB | 19.3 kB |
109
+ | `agent-fanout` | 260.9 kB | 86.7 kB | 74.6 kB |
110
+ | `alert` | 35.2 kB | 11.5 kB | 10.1 kB |
111
+ | `alert-dialog` | 169.1 kB | 57.0 kB | 49.8 kB |
112
+ | `aspect-ratio` | 30.2 kB | 9.7 kB | 8.5 kB |
113
+ | `attachment` | 459.9 kB | 155.6 kB | 128.6 kB |
114
+ | `avatar` | 45.5 kB | 15.4 kB | 13.6 kB |
115
+ | `badge` | 34.9 kB | 11.4 kB | 10.0 kB |
116
+ | `brand-icon` | 195.1 kB | 70.2 kB | 61.2 kB |
117
+ | `breadcrumb` | 40.5 kB | 13.4 kB | 11.7 kB |
118
+ | `button` | 50.5 kB | 16.7 kB | 14.6 kB |
119
+ | `button-group` | 37.9 kB | 12.1 kB | 10.7 kB |
120
+ | `calendar` | 312.3 kB | 103.5 kB | 88.2 kB |
121
+ | `canvas` | 235.1 kB | 80.8 kB | 69.2 kB |
122
+ | `card` | 35.4 kB | 11.6 kB | 10.1 kB |
123
+ | `carousel` | 60.4 kB | 19.9 kB | 17.6 kB |
124
+ | `chart` | 393.4 kB | 119.0 kB | 101.3 kB |
125
+ | `chat-confirmation` | 76.7 kB | 25.8 kB | 22.4 kB |
126
+ | `chat-plan` | 42.3 kB | 13.4 kB | 11.7 kB |
127
+ | `chat-sources` | 206.2 kB | 70.5 kB | 60.3 kB |
128
+ | `chat-input` | 962.4 kB | 317.0 kB | 261.5 kB |
129
+ | `checkbox` | 60.0 kB | 21.0 kB | 18.5 kB |
130
+ | `choice-list` | 43.0 kB | 14.5 kB | 12.6 kB |
131
+ | `code-block` | 74.4 kB | 25.9 kB | 22.3 kB |
132
+ | `collapsible` | 55.5 kB | 18.7 kB | 16.5 kB |
133
+ | `combobox` | 268.6 kB | 90.9 kB | 77.7 kB |
134
+ | `command` | 181.9 kB | 61.6 kB | 53.5 kB |
135
+ | `context-menu` | 242.8 kB | 81.0 kB | 69.7 kB |
136
+ | `dialog` | 170.7 kB | 57.5 kB | 50.2 kB |
137
+ | `direction` | 1.4 kB | 0.7 kB | 0.7 kB |
138
+ | `dropdown-menu` | 239.6 kB | 80.0 kB | 69.2 kB |
139
+ | `field` | 97.9 kB | 34.2 kB | 29.5 kB |
140
+ | `file-explorer` | 527.9 kB | 180.1 kB | 147.5 kB |
141
+ | `file-viewer` | 459.5 kB | 155.5 kB | 128.4 kB |
142
+ | `form` | 109.7 kB | 37.3 kB | 32.2 kB |
143
+ | `glass-banner` | 33.0 kB | 10.6 kB | 9.4 kB |
144
+ | `gradient-banner` | 32.2 kB | 10.3 kB | 9.1 kB |
145
+ | `hover-card` | 163.2 kB | 56.3 kB | 49.1 kB |
146
+ | `input` | 108.2 kB | 37.4 kB | 32.3 kB |
147
+ | `input-group` | 119.1 kB | 40.5 kB | 34.8 kB |
148
+ | `input-otp` | 65.3 kB | 22.9 kB | 20.3 kB |
149
+ | `kbd` | 30.4 kB | 9.7 kB | 8.5 kB |
150
+ | `label` | 30.2 kB | 9.6 kB | 8.4 kB |
151
+ | `mention-list` | 52.6 kB | 17.2 kB | 15.2 kB |
152
+ | `menubar` | 244.9 kB | 82.2 kB | 70.7 kB |
153
+ | `message` | 36.2 kB | 11.7 kB | 10.3 kB |
154
+ | `message-actions` | 71.4 kB | 24.9 kB | 20.8 kB |
155
+ | `message-response` | 232.7 kB | 74.2 kB | 64.2 kB |
156
+ | `message-scroller` | 69.0 kB | 22.1 kB | 19.6 kB |
157
+ | `navigation-menu` | 182.3 kB | 63.0 kB | 54.7 kB |
158
+ | `notice-banner` | 30.4 kB | 9.7 kB | 8.5 kB |
159
+ | `page-content` | 30.2 kB | 9.6 kB | 8.4 kB |
160
+ | `page-header` | 54.6 kB | 17.6 kB | 15.5 kB |
161
+ | `pagination` | 55.3 kB | 17.9 kB | 15.6 kB |
162
+ | `plan-outline` | 79.9 kB | 26.4 kB | 22.9 kB |
163
+ | `popover` | 178.0 kB | 60.7 kB | 52.6 kB |
164
+ | `presence-cursor` | 30.8 kB | 9.9 kB | 8.6 kB |
165
+ | `progress` | 39.8 kB | 13.2 kB | 11.6 kB |
166
+ | `prompt-list` | 199.0 kB | 71.9 kB | 62.6 kB |
167
+ | `questionnaire` | 69.5 kB | 23.3 kB | 20.4 kB |
168
+ | `radio-group` | 80.8 kB | 27.9 kB | 24.7 kB |
169
+ | `reasoning-effort` | 35.5 kB | 12.0 kB | 10.5 kB |
170
+ | `resizable` | 39.4 kB | 13.4 kB | 11.8 kB |
171
+ | `result-card` | 240.1 kB | 85.1 kB | 72.1 kB |
172
+ | `review-card` | 61.6 kB | 21.0 kB | 18.4 kB |
173
+ | `rich-text-editor` | 442.5 kB | 140.2 kB | 119.8 kB |
174
+ | `schema-diagram` | 247.9 kB | 82.9 kB | 71.2 kB |
175
+ | `sql-viewer` | 265.6 kB | 89.1 kB | 76.3 kB |
176
+ | `scroll-area` | 57.3 kB | 19.7 kB | 17.4 kB |
177
+ | `search-dialog` | 190.2 kB | 64.6 kB | 55.5 kB |
178
+ | `search-field` | 122.2 kB | 41.4 kB | 35.5 kB |
179
+ | `select` | 230.4 kB | 79.3 kB | 67.8 kB |
180
+ | `selection-slot` | 31.5 kB | 10.2 kB | 8.9 kB |
181
+ | `separator` | 36.9 kB | 11.9 kB | 10.5 kB |
182
+ | `settings-list` | 31.1 kB | 9.9 kB | 8.7 kB |
183
+ | `sheet` | 213.4 kB | 72.9 kB | 63.2 kB |
184
+ | `sidebar` | 251.1 kB | 85.8 kB | 72.8 kB |
185
+ | `skeleton` | 35.1 kB | 11.5 kB | 10.1 kB |
186
+ | `slider` | 83.6 kB | 29.5 kB | 25.9 kB |
187
+ | `spinner` | 36.5 kB | 11.6 kB | 10.1 kB |
188
+ | `state-panel` | 459.7 kB | 155.6 kB | 128.5 kB |
189
+ | `state-sample` | 33.5 kB | 10.5 kB | 9.2 kB |
190
+ | `switch` | 50.3 kB | 17.3 kB | 15.3 kB |
191
+ | `table` | 536.6 kB | 171.9 kB | 144.4 kB |
192
+ | `table-nuqs` | 2.1 kB | 1.1 kB | 1.0 kB |
193
+ | `tabs` | 80.2 kB | 27.6 kB | 24.4 kB |
194
+ | `textarea` | 98.2 kB | 34.2 kB | 29.5 kB |
195
+ | `toast` | 189.5 kB | 65.2 kB | 56.7 kB |
196
+ | `toggle` | 46.1 kB | 15.7 kB | 13.8 kB |
197
+ | `toggle-group` | 67.1 kB | 23.0 kB | 20.3 kB |
198
+ | `tool-activity` | 86.7 kB | 29.2 kB | 25.2 kB |
199
+ | `toolbar-status` | 189.3 kB | 64.3 kB | 55.6 kB |
200
+ | `tooltip` | 164.7 kB | 56.7 kB | 49.4 kB |
201
+ | `typed-text` | 31.2 kB | 10.1 kB | 8.8 kB |
202
+
203
+ </details>
204
+ <!-- bundle-size:end -->
205
+
206
+ CI compares sizes on each pull request. Run `pnpm size` to see them locally.
207
+
97
208
  | Group | Components |
98
209
  | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
210
  | **Actions** | Button · Button Group · Toggle · Toggle Group · Toolbar Status · Kbd |
@@ -0,0 +1,46 @@
1
+ import { type ReactNode } from "react";
2
+ export declare const agentFanoutStatusNames: readonly ["queued", "working", "done", "failed", "stopped"];
3
+ export declare const agentFanoutStateNames: readonly ["running", "done", "stopped"];
4
+ export type AgentFanoutStatus = (typeof agentFanoutStatusNames)[number];
5
+ export type AgentFanoutState = (typeof agentFanoutStateNames)[number];
6
+ export interface AgentFanoutAgent {
7
+ id: string;
8
+ name: string;
9
+ /** A 16px icon for the subagent's role. */
10
+ icon: ReactNode;
11
+ /** What the subagent was asked to do, such as “Web search”. */
12
+ task: string;
13
+ /** What it found. Replaces `task` once the subagent is done or failed. */
14
+ result?: string | undefined;
15
+ status: AgentFanoutStatus;
16
+ /** How long the subagent took, in ms. Shown once it is done. */
17
+ duration?: number | undefined;
18
+ /** Any CSS color. Defaults to the next of five `--sprout-chart-*` colors. */
19
+ color?: string | undefined;
20
+ /** Subagents this agent calls in turn. They take its color. */
21
+ agents?: AgentFanoutAgent[] | undefined;
22
+ }
23
+ export interface AgentFanoutProps {
24
+ /** The parent agent's task, shown above the graph. */
25
+ task: ReactNode;
26
+ agents: AgentFanoutAgent[];
27
+ /** The parent agent. `done` once it has merged the results into an answer. */
28
+ state?: AgentFanoutState | undefined;
29
+ /** Time since the parent started, in ms. */
30
+ elapsed?: number | undefined;
31
+ /** Replaces the footer text that names the current phase. */
32
+ label?: ReactNode;
33
+ /** Shows the per-agent list at first. It starts closed. */
34
+ defaultOpen?: boolean | undefined;
35
+ /** Shows a Stop button while the parent is running. */
36
+ onStop?: (() => void) | undefined;
37
+ /** Shows a Run again button once the parent is done or stopped. */
38
+ onRestart?: (() => void) | undefined;
39
+ className?: string | undefined;
40
+ }
41
+ /**
42
+ * A parent agent that hands one task to several subagents and merges what
43
+ * they find. Each status change sends a pulse along the subagent's line:
44
+ * down when it starts, back up when it finishes.
45
+ */
46
+ export declare function AgentFanout({ task, agents, state, elapsed, label, defaultOpen, onStop, onRestart, className, }: AgentFanoutProps): import("react").JSX.Element;
@@ -0,0 +1,228 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { BaseEdge, EdgeLabelRenderer, Handle, Position, ReactFlow, } from "@xyflow/react";
4
+ import { useLayoutEffect, useRef, useState, } from "react";
5
+ import { Check } from "reicon-react/icons/Check";
6
+ import { ChevronDown } from "reicon-react/icons/ChevronDown";
7
+ import { Danger } from "reicon-react/icons/Danger";
8
+ import { MinusCircle } from "reicon-react/icons/MinusCircle";
9
+ import { RotateLeft } from "reicon-react/icons/RotateLeft";
10
+ import { Sparkle } from "reicon-react/icons/Sparkle";
11
+ import { cn } from "../internal/classes.js";
12
+ import { Button } from "./button.js";
13
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "./collapsible.js";
14
+ export const agentFanoutStatusNames = [
15
+ "queued",
16
+ "working",
17
+ "done",
18
+ "failed",
19
+ "stopped",
20
+ ];
21
+ export const agentFanoutStateNames = ["running", "done", "stopped"];
22
+ const rootTop = 12;
23
+ const rootSize = 44;
24
+ const levelTop = 120;
25
+ const levelGap = 108;
26
+ const nodeLabel = 22;
27
+ const bottomGap = 36;
28
+ const colorCount = 5;
29
+ const statusText = {
30
+ queued: "Queued",
31
+ working: "Working",
32
+ done: "Done",
33
+ failed: "Failed",
34
+ stopped: "Stopped",
35
+ };
36
+ const seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
37
+ function phaseText(agents, state, elapsed) {
38
+ const time = elapsed === undefined ? "" : seconds(elapsed);
39
+ if (state === "done") {
40
+ const merged = agents.filter((agent) => agent.status === "done").length;
41
+ return {
42
+ text: `Merged ${merged} ${merged === 1 ? "result" : "results"}${time && ` in ${time}`}`,
43
+ time: "",
44
+ };
45
+ }
46
+ if (state === "stopped")
47
+ return { text: time ? `Stopped after ${time}` : "Stopped", time: "" };
48
+ const settled = agents.filter((agent) => agent.status === "done" || agent.status === "failed").length;
49
+ const text = agents.every((agent) => agent.status === "queued")
50
+ ? "Delegating work"
51
+ : settled === agents.length
52
+ ? "Composing answer"
53
+ : settled > 0
54
+ ? "Merging results"
55
+ : "Specialists working";
56
+ return { text, time };
57
+ }
58
+ function useWidth() {
59
+ const ref = useRef(null);
60
+ const [width, setWidth] = useState(0);
61
+ useLayoutEffect(() => {
62
+ const element = ref.current;
63
+ if (!element)
64
+ return;
65
+ setWidth(element.clientWidth);
66
+ const observer = new ResizeObserver(() => setWidth(element.clientWidth));
67
+ observer.observe(element);
68
+ return () => observer.disconnect();
69
+ }, []);
70
+ return [ref, width];
71
+ }
72
+ /** Every agent in tree order, with the parent it reports to. */
73
+ function flatten(agents, initial, parent = rootId, depth = 1, color) {
74
+ return agents.flatMap((agent, index) => {
75
+ const own = agent.color ?? color ?? `var(--sprout-chart-${(index % colorCount) + 1})`;
76
+ const changed = initial.get(agent.id) !== agent.status;
77
+ const line = {
78
+ agent,
79
+ parent,
80
+ depth,
81
+ changed: changed || undefined,
82
+ trip: !changed
83
+ ? null
84
+ : agent.status === "working"
85
+ ? "out"
86
+ : agent.status === "done"
87
+ ? "back"
88
+ : null,
89
+ style: {
90
+ "--sprout-agent-fanout-color": own,
91
+ "--sprout-agent-fanout-index": index,
92
+ "--sprout-agent-fanout-depth": depth - 1,
93
+ },
94
+ };
95
+ return [
96
+ line,
97
+ ...flatten(agent.agents ?? [], initial, agent.id, depth + 1, own),
98
+ ];
99
+ });
100
+ }
101
+ const rootId = "agent-fanout-root";
102
+ // Fresh node objects every render drop React Flow's measured handles, which
103
+ // unmounts the edges and restarts their pulses. Declared handles survive.
104
+ const handle = (type, x, y) => ({
105
+ type,
106
+ position: type === "source" ? Position.Bottom : Position.Top,
107
+ x,
108
+ y,
109
+ width: 0,
110
+ height: 0,
111
+ });
112
+ /** Leaves share the width; each parent sits over the middle of its children. */
113
+ function layout(lines, width, state, rootChanged) {
114
+ const children = new Map();
115
+ for (const line of lines)
116
+ children.set(line.parent, [...(children.get(line.parent) ?? []), line]);
117
+ const leaves = lines.filter((line) => !children.has(line.agent.id)).length;
118
+ const column = width / Math.max(leaves, 1);
119
+ const centers = new Map();
120
+ let leaf = 0;
121
+ const place = (id) => {
122
+ const kids = children.get(id);
123
+ const xs = kids?.map((kid) => place(kid.agent.id));
124
+ const x = xs ? (xs[0] + xs.at(-1)) / 2 : (leaf++ + 0.5) * column;
125
+ centers.set(id, x);
126
+ return x;
127
+ };
128
+ place(rootId);
129
+ const depth = Math.max(0, ...lines.map((line) => line.depth));
130
+ const dot = width ? Math.min(40, Math.max(28, column - 12)) : 40;
131
+ const nodes = [
132
+ {
133
+ id: rootId,
134
+ type: "root",
135
+ position: { x: width / 2 - rootSize / 2, y: rootTop },
136
+ width: rootSize,
137
+ height: rootSize,
138
+ handles: [handle("source", rootSize / 2, rootSize)],
139
+ data: { state, changed: rootChanged },
140
+ },
141
+ ...lines.map((line) => ({
142
+ id: line.agent.id,
143
+ type: "agent",
144
+ position: {
145
+ x: centers.get(line.agent.id) - column / 2,
146
+ y: levelTop + (line.depth - 1) * levelGap,
147
+ },
148
+ width: column,
149
+ height: dot + nodeLabel,
150
+ handles: [
151
+ handle("target", column / 2, 0),
152
+ handle("source", column / 2, dot),
153
+ ],
154
+ data: line,
155
+ })),
156
+ ];
157
+ const edges = lines.map((line) => ({
158
+ id: `${line.parent}-${line.agent.id}`,
159
+ type: "trip",
160
+ source: line.parent,
161
+ target: line.agent.id,
162
+ data: { trip: line.trip, style: line.style },
163
+ }));
164
+ return {
165
+ nodes,
166
+ edges,
167
+ dot,
168
+ height: levelTop +
169
+ Math.max(depth - 1, 0) * levelGap +
170
+ dot +
171
+ nodeLabel +
172
+ bottomGap,
173
+ };
174
+ }
175
+ function RootMark({ data: { state, changed } }) {
176
+ return (_jsxs("span", { "data-slot": "agent-fanout-root", "data-state": state, "data-changed": changed, className: "sprout-agent-fanout-root", children: [state === "done" ? (_jsx(Check, { size: 18 })) : state === "stopped" ? (_jsx(MinusCircle, { size: 18 })) : (_jsx(Sparkle, { size: 18 })), _jsx(Handle, { type: "source", position: Position.Bottom, isConnectable: false, className: "sprout-agent-fanout-handle" })] }));
177
+ }
178
+ function AgentMark({ data: { agent, changed, style } }) {
179
+ return (_jsxs("div", { "data-slot": "agent-fanout-node", "data-status": agent.status, "data-changed": changed, className: "sprout-agent-fanout-node", style: style, children: [_jsxs("span", { className: "sprout-agent-fanout-dot", children: [_jsx(NodeMark, { agent: agent }), _jsx(Handle, { type: "target", position: Position.Top, isConnectable: false, className: "sprout-agent-fanout-handle" }), agent.agents && agent.agents.length > 0 && (_jsx(Handle, { type: "source", position: Position.Bottom, isConnectable: false, className: "sprout-agent-fanout-handle" }))] }), _jsx("span", { className: "sprout-agent-fanout-node-name", children: agent.name })] }));
180
+ }
181
+ function TripLine({ id, sourceX, sourceY, targetX, targetY, data, }) {
182
+ const drop = targetY - sourceY;
183
+ const d = `M ${sourceX} ${sourceY} C ${sourceX} ${sourceY + drop * 0.5}, ${targetX} ${sourceY + drop * 0.2}, ${targetX} ${targetY}`;
184
+ const trip = data?.trip;
185
+ return (_jsxs("g", { style: data?.style, children: [_jsx(BaseEdge, { id: id, path: d, className: "sprout-agent-fanout-edge" }), trip && (_jsxs(_Fragment, { children: [_jsx("path", { d: d, pathLength: 1, "data-trip": trip, className: "sprout-agent-fanout-trail" }, trip), _jsx(EdgeLabelRenderer, { children: _jsx("span", { "data-trip": trip, className: "sprout-agent-fanout-comet", style: { ...data.style, offsetPath: `path("${d}")` } }, trip) })] }))] }));
186
+ }
187
+ const nodeTypes = { root: RootMark, agent: AgentMark };
188
+ const edgeTypes = { trip: TripLine };
189
+ const viewport = { x: 0, y: 0, zoom: 1 };
190
+ function NodeMark({ agent }) {
191
+ if (agent.status === "done")
192
+ return _jsx(Check, { size: 16, "aria-hidden": "true" });
193
+ if (agent.status === "failed")
194
+ return _jsx(Danger, { size: 16, "aria-hidden": "true" });
195
+ return _jsx("span", { "aria-hidden": "true", children: agent.icon });
196
+ }
197
+ function StatusMark({ agent }) {
198
+ if (agent.status === "done")
199
+ return (_jsxs(_Fragment, { children: [_jsx(Check, { size: 12, "aria-hidden": "true" }), agent.duration === undefined ? "Done" : seconds(agent.duration)] }));
200
+ if (agent.status === "working")
201
+ return (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", className: "sprout-agent-fanout-pulse" }), "Working"] }));
202
+ return _jsx(_Fragment, { children: statusText[agent.status] });
203
+ }
204
+ /**
205
+ * A parent agent that hands one task to several subagents and merges what
206
+ * they find. Each status change sends a pulse along the subagent's line:
207
+ * down when it starts, back up when it finishes.
208
+ */
209
+ export function AgentFanout({ task, agents, state = "running", elapsed, label, defaultOpen = false, onStop, onRestart, className, }) {
210
+ const [graphRef, width] = useWidth();
211
+ // Only changes after the first render animate, so a finished fanout
212
+ // renders still instead of replaying every pulse.
213
+ const [initial] = useState(() => ({
214
+ state,
215
+ statuses: new Map(flatten(agents, new Map()).map(({ agent }) => [agent.id, agent.status])),
216
+ }));
217
+ const phase = phaseText(agents, state, elapsed);
218
+ const lines = flatten(agents, initial.statuses);
219
+ const graph = layout(lines, width, state, initial.state !== state || undefined);
220
+ const finished = lines.filter(({ agent }) => agent.status === "done" || agent.status === "failed").length;
221
+ return (_jsxs("section", { "data-slot": "agent-fanout", "data-state": state, "aria-label": "Subagents", className: cn("sprout-agent-fanout", className), children: [_jsxs("div", { className: "sprout-agent-fanout-panel", children: [_jsx("div", { className: "sprout-agent-fanout-task", children: _jsx("span", { className: "sprout-agent-fanout-task-text", children: task }) }), _jsx("div", { ref: graphRef, "aria-hidden": "true", "data-slot": "agent-fanout-graph", className: "sprout-agent-fanout-graph", style: {
222
+ height: graph.height,
223
+ "--sprout-agent-fanout-dot": `${graph.dot}px`,
224
+ }, children: width > 0 && (_jsx(ReactFlow, { className: "sprout-agent-fanout-flow", nodes: graph.nodes, edges: graph.edges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, defaultViewport: viewport, minZoom: 1, maxZoom: 1, panOnDrag: false, panOnScroll: false, zoomOnScroll: false, zoomOnPinch: false, zoomOnDoubleClick: false, preventScrolling: false, nodesDraggable: false, nodesConnectable: false, nodesFocusable: false, edgesFocusable: false, elementsSelectable: false, proOptions: { hideAttribution: true } })) }), _jsxs(Collapsible, { defaultOpen: defaultOpen, className: "sprout-agent-fanout-details", children: [_jsxs(CollapsibleTrigger, { className: "sprout-agent-fanout-trigger", children: [_jsx("span", { className: "sprout-agent-fanout-trigger-label", children: "Details" }), _jsx("span", { className: "sprout-agent-fanout-trigger-count", children: `${finished} of ${lines.length} finished` }), _jsx(ChevronDown, { size: 14, "aria-hidden": "true", "data-slot": "collapsible-indicator" })] }), _jsx(CollapsibleContent, { keepMounted: true, children: _jsx("ul", { "aria-label": "Subagent status", className: "sprout-agent-fanout-rows", children: lines.map(({ agent, changed, style }) => {
225
+ const settled = agent.status === "done" || agent.status === "failed";
226
+ return (_jsxs("li", { "data-slot": "agent-fanout-row", "data-status": agent.status, className: "sprout-agent-fanout-row", style: style, children: [_jsx("span", { className: "sprout-agent-fanout-row-name", children: agent.name }), _jsx("span", { "data-changed": changed, className: "sprout-agent-fanout-row-detail", children: settled && agent.result ? agent.result : agent.task }, settled && agent.result ? "result" : "task"), _jsx("span", { "data-changed": changed, className: "sprout-agent-fanout-row-status", children: _jsx(StatusMark, { agent: agent }) }, agent.status)] }, agent.id));
227
+ }) }) })] })] }), _jsxs("div", { className: "sprout-agent-fanout-footer", children: [_jsxs("p", { className: "sprout-agent-fanout-phase", children: [_jsx("span", { role: "status", children: label ?? phase.text }), !label && phase.time && (_jsx("span", { className: "sprout-agent-fanout-time", children: phase.time }))] }), state === "running" && onStop && (_jsxs(Button, { variant: "outline", size: "sm", onClick: onStop, className: "sprout-agent-fanout-action", children: [_jsx(MinusCircle, { size: 14, "aria-hidden": "true" }), "Stop"] })), state !== "running" && onRestart && (_jsxs(Button, { variant: "outline", size: "sm", onClick: onRestart, className: "sprout-agent-fanout-action", children: [_jsx(RotateLeft, { size: 14, "aria-hidden": "true" }), "Run again"] }))] })] }));
228
+ }
@@ -168,7 +168,7 @@ function AttachmentCarousel({ className, children, onKeyDownCapture, ...props })
168
168
  scroll(event.key === "ArrowLeft" ? -1 : 1);
169
169
  }
170
170
  };
171
- return (_jsxs("div", { "data-slot": "attachment-group", "data-variant": "carousel", role: "region", "aria-roledescription": "carousel", className: cn("sprout-attachment-group", "sprout-attachment-group-carousel", className), onKeyDownCapture: handleKeyDown, ...props, children: [_jsx("div", { ref: viewport, className: "sprout-attachment-carousel-viewport", onScroll: measure, children: _jsx("div", { className: "sprout-attachment-carousel-track", children: Children.map(children, (child) => child == null || child === false ? null : (_jsx("div", { role: "group", "aria-roledescription": "slide", className: "sprout-attachment-carousel-item", children: child }))) }) }), edges.start && (_jsx("span", { className: "sprout-attachment-carousel-edge", "data-side": "start", children: _jsx("button", { type: "button", "aria-label": "Previous attachments", tabIndex: -1, className: "sprout-attachment-carousel-arrow", onClick: () => scroll(-1), children: _jsx(ChevronLeft, { size: 12, "aria-hidden": "true" }) }) })), edges.end && (_jsx("span", { className: "sprout-attachment-carousel-edge", "data-side": "end", children: _jsx("button", { type: "button", "aria-label": "Next attachments", tabIndex: -1, className: "sprout-attachment-carousel-arrow", onClick: () => scroll(1), children: _jsx(ChevronRight, { size: 12, "aria-hidden": "true" }) }) }))] }));
171
+ return (_jsxs("div", { "data-slot": "attachment-group", "data-variant": "carousel", role: "region", "aria-roledescription": "carousel", className: cn("sprout-attachment-group", "sprout-attachment-group-carousel", className), onKeyDownCapture: handleKeyDown, ...props, children: [_jsx("div", { ref: viewport, className: "sprout-attachment-carousel-viewport", onScroll: measure, children: _jsx("div", { className: "sprout-attachment-carousel-track", children: Children.map(children, (child) => child == null || child === false ? null : (_jsx("div", { role: "group", "aria-roledescription": "slide", className: "sprout-attachment-carousel-item", children: child }))) }) }), edges.start && (_jsx("span", { className: "sprout-attachment-carousel-edge", "data-side": "start", children: _jsx(Button, { variant: "outline", size: "icon-xs", "aria-label": "Previous attachments", tabIndex: -1, className: "sprout-attachment-carousel-arrow", onClick: () => scroll(-1), children: _jsx(ChevronLeft, { "aria-hidden": "true" }) }) })), edges.end && (_jsx("span", { className: "sprout-attachment-carousel-edge", "data-side": "end", children: _jsx(Button, { variant: "outline", size: "icon-xs", "aria-label": "Next attachments", tabIndex: -1, className: "sprout-attachment-carousel-arrow", onClick: () => scroll(1), children: _jsx(ChevronRight, { "aria-hidden": "true" }) }) }))] }));
172
172
  }
173
173
  /** A paper glyph with a colored extension badge, such as PDF or XLSX. Decorative. */
174
174
  export function FileTypeIcon({ name, mediaType, size = "sm", className, ...props }) {
@@ -231,7 +231,7 @@ export function FileAttachment({ file, variant = "list", onRemove, onRetry, onOp
231
231
  const remove = (size) => onRemove && (_jsx(Button, { variant: "ghost", size: size, "aria-label": `Remove ${file.name}`, onClick: onRemove, children: _jsx(X, { size: 14, "aria-hidden": "true" }) }));
232
232
  const viewer = inViewer && !shared && (_jsx(FileViewer, { file: target, open: open, onOpenChange: setOpen }));
233
233
  if (variant === "chip") {
234
- const inner = (_jsxs(_Fragment, { children: [image ? (_jsx("span", { className: "sprout-file-attachment-thumb", children: file.url && (_jsx(AttachmentImage, { src: file.url, name: file.name }, file.url)) })) : (_jsx(FileTypeIcon, { name: file.name, mediaType: file.mediaType })), _jsx("span", { className: "sprout-file-attachment-name", children: file.name })] }));
234
+ const inner = (_jsxs(_Fragment, { children: [image && file.url ? (_jsx("span", { className: "sprout-file-attachment-thumb", children: _jsx(AttachmentImage, { src: file.url, name: file.name }, file.url) })) : (_jsx(FileTypeIcon, { name: file.name, mediaType: file.mediaType })), _jsx("span", { className: "sprout-file-attachment-name", children: file.name })] }));
235
235
  return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: root, "data-slot": "file-attachment", "data-variant": "chip", "data-state": file.state, "data-removable": onRemove ? "" : undefined, className: cn("sprout-file-attachment", className), title: openable ? undefined : `${file.name} · ${stateLabel}`, children: [withPreview(openable ? (_jsx("button", { ...openProps, "aria-label": openLabel, title: peek ? undefined : `${file.name} · ${stateLabel}`, className: "sprout-file-attachment-open", children: inner })) : (_jsx("span", { className: "sprout-file-attachment-open", children: inner }))), busy && _jsx(Spinner, { "aria-label": stateLabel }), retry, remove("icon-xs")] }), viewer] }));
236
236
  }
237
237
  const name = (_jsxs(_Fragment, { children: [_jsx("span", { className: "sprout-file-attachment-name", title: file.name, children: file.name }), variant !== "inline" && (_jsxs("span", { className: "sprout-file-attachment-status", children: [busy && _jsx(Spinner, { size: "sm", "aria-hidden": "true" }), stateLabel] }))] }));
@@ -11,6 +11,10 @@ export const brandNames = {
11
11
  deepseek: "DeepSeek",
12
12
  grok: "Grok",
13
13
  kimi: "Kimi",
14
+ gemini: "Gemini",
15
+ mistral: "Mistral",
16
+ meta: "Meta",
17
+ qwen: "Qwen",
14
18
  google: "Google Workspace",
15
19
  gmail: "Gmail",
16
20
  "google-calendar": "Google Calendar",
@@ -3,11 +3,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
3
3
  import { createContext, useContext, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { ArrowLeft } from "reicon-react/icons/ArrowLeft";
6
- import { Loader } from "reicon-react/icons/Loader";
7
6
  import { SidebarRight } from "reicon-react/icons/SidebarRight";
8
7
  import { X } from "reicon-react/icons/X";
9
8
  import { cn } from "../internal/classes.js";
10
9
  import { Button } from "./button.js";
10
+ import { Spinner } from "./spinner.js";
11
11
  import { ResizableHandle, ResizablePanel, ResizablePanelGroup, useResizableLayout, } from "./resizable.js";
12
12
  import { Sheet, SheetContent, SheetDescription, SheetTitle } from "./sheet.js";
13
13
  /** Below this split width (px), the canvas opens as a full-screen sheet. */
@@ -153,7 +153,7 @@ export function CanvasPanel({ tabs, activeId, onSelect, onCloseTab, onClose, chi
153
153
  tabButtons.current.set(tab.id, node);
154
154
  else
155
155
  tabButtons.current.delete(tab.id);
156
- }, "data-slot": "canvas-tab", "data-active": active ? "" : undefined, "data-busy": tab.busy ? "" : undefined, "data-hover": hovered === tab.id ? "" : undefined, "aria-label": tab.busy ? `${tab.title}, loading` : undefined, "aria-selected": active, "aria-controls": bodyId, tabIndex: active || (activeIndex === -1 && index === 0) ? 0 : -1, style: { order: index * 2 }, onClick: () => onSelect(tab.id), onKeyDown: (event) => onTabKeyDown(event, index), onPointerEnter: () => setHovered(tab.id), onPointerLeave: () => setHovered(null), className: "sprout-canvas-tab", children: [tab.busy ? (_jsx(Loader, { size: 14, "aria-hidden": "true", className: "sprout-canvas-tab-spinner" })) : (Icon && (_jsx("span", { "aria-hidden": "true", className: "sprout-canvas-tab-icon", children: _jsx(Icon, { size: 14 }) }))), _jsx("span", { className: "sprout-canvas-tab-title", children: tab.title })] }, tab.id));
156
+ }, "data-slot": "canvas-tab", "data-active": active ? "" : undefined, "data-busy": tab.busy ? "" : undefined, "data-hover": hovered === tab.id ? "" : undefined, "aria-label": tab.busy ? `${tab.title}, loading` : undefined, "aria-selected": active, "aria-controls": bodyId, tabIndex: active || (activeIndex === -1 && index === 0) ? 0 : -1, style: { order: index * 2 }, onClick: () => onSelect(tab.id), onKeyDown: (event) => onTabKeyDown(event, index), onPointerEnter: () => setHovered(tab.id), onPointerLeave: () => setHovered(null), className: "sprout-canvas-tab", children: [tab.busy ? (_jsx(Spinner, { size: "sm", "aria-hidden": "true" })) : (Icon && (_jsx("span", { "aria-hidden": "true", className: "sprout-canvas-tab-icon", children: _jsx(Icon, { size: 14 }) }))), _jsx("span", { className: "sprout-canvas-tab-title", children: tab.title })] }, tab.id));
157
157
  }) }), tabs.map((tab, index) => {
158
158
  const active = index === activeIndex;
159
159
  return (_jsx("button", { type: "button", "aria-label": `Close ${tab.title}`, "data-active": active ? "" : undefined, "data-hover": hovered === tab.id ? "" : undefined, tabIndex: active ? 0 : -1, style: { order: index * 2 + 1 }, onClick: () => closeTab(index), onPointerEnter: () => setHovered(tab.id), onPointerLeave: () => setHovered(null), className: "sprout-canvas-tab-close", children: _jsx(X, { size: 12, "aria-hidden": "true" }) }, tab.id));
@@ -0,0 +1,94 @@
1
+ import { type ReactNode } from "react";
2
+ import { type AttachmentFile } from "./attachment.js";
3
+ import { type Brand } from "./brand-icon.js";
4
+ import { type RichTextValue } from "./rich-text-editor.js";
5
+ export declare const chatInputReferenceKindNames: readonly ["agent", "skill", "file"];
6
+ export declare const chatInputStatusNames: readonly ["idle", "streaming"];
7
+ export type ChatInputReferenceKind = (typeof chatInputReferenceKindNames)[number];
8
+ export type ChatInputStatus = (typeof chatInputStatusNames)[number];
9
+ /** One row in the `@`, `/`, or `#` panel. Picking it drops a chip into the message. */
10
+ export interface ChatInputSuggestion {
11
+ id: string;
12
+ label: string;
13
+ description?: string | undefined;
14
+ /** A 16px icon, file icon, brand glyph, or small avatar. */
15
+ media?: ReactNode;
16
+ }
17
+ /** Returns the rows that match what follows the trigger. It may be async. */
18
+ export type ChatInputSource = (query: string) => ChatInputSuggestion[] | Promise<ChatInputSuggestion[]>;
19
+ export interface ChatInputAction {
20
+ id: string;
21
+ label: string;
22
+ description?: string | undefined;
23
+ icon?: ReactNode;
24
+ onSelect: () => void;
25
+ }
26
+ export interface ChatInputOption {
27
+ id: string;
28
+ label: string;
29
+ description?: string | undefined;
30
+ icon?: ReactNode;
31
+ /** Modes only. Any CSS color; tints the box border, chip, and menu icon while on. */
32
+ color?: string | undefined;
33
+ }
34
+ export interface ChatInputModel {
35
+ id: string;
36
+ label: string;
37
+ description?: string | undefined;
38
+ /** Draws the vendor logo. Use `icon` for a mark that isn't a `Brand`. */
39
+ brand?: Brand | undefined;
40
+ icon?: ReactNode;
41
+ disabled?: boolean | undefined;
42
+ }
43
+ export interface ChatInputPrompt {
44
+ id: string;
45
+ label: string;
46
+ icon?: ReactNode;
47
+ /** The text put in the message. Defaults to `label`. */
48
+ prompt?: string | undefined;
49
+ }
50
+ export interface ChatInputProps {
51
+ value: RichTextValue;
52
+ onValueChange: (value: RichTextValue) => void;
53
+ /** Runs on Enter, Cmd or Ctrl+Enter, and the send button, only when the message has content. */
54
+ onSend: () => void;
55
+ /** Shown in place of send while `status` is `streaming`. */
56
+ onStop?: (() => void) | undefined;
57
+ /** `streaming` swaps send for stop and shows `streamingPlaceholder`. */
58
+ status?: ChatInputStatus | undefined;
59
+ placeholder?: string | undefined;
60
+ streamingPlaceholder?: string | undefined;
61
+ disabled?: boolean | undefined;
62
+ /** Accessible name of the form. Defaults to `Message`. */
63
+ label?: string | undefined;
64
+ /** `@` opens these. Leave unset to turn the trigger off. */
65
+ agents?: ChatInputSource | undefined;
66
+ /** `/` opens these. */
67
+ skills?: ChatInputSource | undefined;
68
+ /** `#` opens these. */
69
+ files?: ChatInputSource | undefined;
70
+ attachments?: AttachmentFile[] | undefined;
71
+ onRemoveAttachment?: ((id: string) => void) | undefined;
72
+ /** The `+` menu, such as upload from computer, Google Drive, or a URL. */
73
+ attachActions?: ChatInputAction[] | undefined;
74
+ /** The modes menu, such as Plan, Deep research, or Learning. One mode at a time. */
75
+ modes?: ChatInputOption[] | undefined;
76
+ mode?: string | null | undefined;
77
+ onModeChange?: ((mode: string | null) => void) | undefined;
78
+ projects?: ChatInputOption[] | undefined;
79
+ project?: string | undefined;
80
+ onProjectChange?: ((project: string) => void) | undefined;
81
+ models?: ChatInputModel[] | undefined;
82
+ model?: string | undefined;
83
+ onModelChange?: ((model: string) => void) | undefined;
84
+ /** Starter chips under an empty message. A click fills the message. */
85
+ prompts?: ChatInputPrompt[] | undefined;
86
+ onPromptSelect?: ((prompt: ChatInputPrompt) => void) | undefined;
87
+ className?: string | undefined;
88
+ }
89
+ /**
90
+ * A chat message box on `RichTextEditor`. `@` tags agents, `/` adds skills,
91
+ * and `#` adds files. The bar holds attach and mode menus, a project picker,
92
+ * a model picker, and send or stop. Starter prompts sit under an empty box.
93
+ */
94
+ export declare function ChatInput({ value, onValueChange, onSend, onStop, status, placeholder, streamingPlaceholder, disabled, label, agents, skills, files, attachments, onRemoveAttachment, attachActions, modes, mode, onModeChange, projects, project, onProjectChange, models, model, onModelChange, prompts, onPromptSelect, className, }: ChatInputProps): import("react").JSX.Element;