@cosmoops/connect-sdk 0.7.0 → 0.9.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 (51) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +273 -91
  3. package/dist/cjs/client/components/access-gate.js +39 -4
  4. package/dist/cjs/client/components/ai-format.js +135 -0
  5. package/dist/cjs/client/components/ai-widgets.js +232 -0
  6. package/dist/cjs/client/components/app-shell.js +7 -6
  7. package/dist/cjs/client/components/ask-ai.js +334 -0
  8. package/dist/cjs/client/components/nav.js +1 -1
  9. package/dist/cjs/client/components/side-nav.js +1 -1
  10. package/dist/cjs/client/components/user-menu.js +28 -5
  11. package/dist/cjs/client/components/workspace-type-gate.js +48 -0
  12. package/dist/cjs/client/index.js +31 -3
  13. package/dist/cjs/client/provider.js +45 -2
  14. package/dist/cjs/server/client.js +1 -1
  15. package/dist/cjs/server/handler.js +43 -0
  16. package/dist/cjs/server/http.js +46 -24
  17. package/dist/cjs/server/resources/entitlements.js +41 -0
  18. package/dist/cjs/server/resources/identity.js +2 -0
  19. package/dist/esm/client/components/access-gate.js +40 -5
  20. package/dist/esm/client/components/ai-format.js +129 -0
  21. package/dist/esm/client/components/ai-widgets.js +223 -0
  22. package/dist/esm/client/components/app-shell.js +6 -5
  23. package/dist/esm/client/components/ask-ai.js +331 -0
  24. package/dist/esm/client/components/nav.js +1 -1
  25. package/dist/esm/client/components/side-nav.js +1 -1
  26. package/dist/esm/client/components/user-menu.js +28 -5
  27. package/dist/esm/client/components/workspace-type-gate.js +45 -0
  28. package/dist/esm/client/index.js +13 -2
  29. package/dist/esm/client/provider.js +43 -2
  30. package/dist/esm/server/client.js +1 -1
  31. package/dist/esm/server/handler.js +43 -0
  32. package/dist/esm/server/http.js +46 -24
  33. package/dist/esm/server/resources/entitlements.js +41 -0
  34. package/dist/esm/server/resources/identity.js +2 -0
  35. package/dist/theme.css +87 -0
  36. package/dist/types/client/components/access-gate.d.ts +17 -3
  37. package/dist/types/client/components/ai-format.d.ts +24 -0
  38. package/dist/types/client/components/ai-widgets.d.ts +42 -0
  39. package/dist/types/client/components/app-shell.d.ts +21 -5
  40. package/dist/types/client/components/ask-ai.d.ts +73 -0
  41. package/dist/types/client/components/nav.d.ts +3 -3
  42. package/dist/types/client/components/side-nav.d.ts +1 -1
  43. package/dist/types/client/components/user-menu.d.ts +2 -1
  44. package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
  45. package/dist/types/client/index.d.ts +10 -3
  46. package/dist/types/client/provider.d.ts +44 -1
  47. package/dist/types/server/client.d.ts +1 -1
  48. package/dist/types/server/http.d.ts +6 -1
  49. package/dist/types/server/resources/entitlements.d.ts +33 -1
  50. package/dist/types/types.d.ts +174 -0
  51. package/package.json +8 -2
@@ -0,0 +1,334 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectAskAi = ConnectAskAi;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const react_2 = require("@heroui/react");
8
+ const lucide_react_1 = require("lucide-react");
9
+ const streamdown_1 = require("streamdown");
10
+ const provider_js_1 = require("../provider.js");
11
+ const ai_widgets_js_1 = require("./ai-widgets.js");
12
+ const PANEL_WIDTH = "w-full lg:w-[400px]";
13
+ /**
14
+ * `blurIn` over the default `fadeIn`: Gemini arrives in bursts rather than an
15
+ * even token drip, and a blur masks a batch landing at once where a plain
16
+ * opacity fade just flickers. Hoisted to module scope because Streamdown
17
+ * compares `animated` by reference.
18
+ */
19
+ const ANIMATED = {
20
+ animation: "blurIn",
21
+ duration: 220,
22
+ easing: "ease-out",
23
+ sep: "word",
24
+ };
25
+ /**
26
+ * The "working on it" state - a highlight sweeping through the word rather
27
+ * than a spinner beside it. `connect-ai-shimmer` is defined in the SDK's
28
+ * `theme.css`, which every application platform already imports.
29
+ */
30
+ function ThinkingShimmer({ label = "Thinking" }) {
31
+ return ((0, jsx_runtime_1.jsx)("span", { className: "connect-ai-shimmer text-xs font-medium", role: "status", children: label }));
32
+ }
33
+ /** Open while it streams, collapsed once the answer lands. */
34
+ function ThinkingDisclosure({ text, isStreaming }) {
35
+ if (!text)
36
+ return null;
37
+ return ((0, jsx_runtime_1.jsxs)("details", { className: "group mb-2", open: isStreaming, children: [(0, jsx_runtime_1.jsxs)("summary", { className: "flex cursor-pointer list-none items-center gap-1 text-[11px] font-medium text-muted select-none hover:text-foreground", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Brain, { className: "size-3" }), isStreaming ? (0, jsx_runtime_1.jsx)(ThinkingShimmer, {}) : "Thinking", (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: "size-3 transition-transform group-open:rotate-180" })] }), (0, jsx_runtime_1.jsx)("div", { className: "mt-1.5 border-s-2 border-border ps-2 text-xs whitespace-pre-wrap text-muted italic", children: text })] }));
38
+ }
39
+ /**
40
+ * Structural guards, one per widget.
41
+ *
42
+ * The published package carries no schema validator, and should not gain one
43
+ * for this - so each tool input is checked for the fields its renderer would
44
+ * otherwise crash on. Anything that fails renders nothing: the model's prose
45
+ * sits around it either way, so a missing widget reads as terse rather than
46
+ * broken, which is the better failure.
47
+ */
48
+ const isArray = (value) => Array.isArray(value) && value.length > 0;
49
+ const field = (input, key) => typeof input === "object" && input !== null ? input[key] : undefined;
50
+ function renderTool(name, input, renderMarkdown) {
51
+ switch (name) {
52
+ case "showTable":
53
+ return isArray(field(input, "columns")) && Array.isArray(field(input, "rows")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiTable, { ...input })) : null;
54
+ case "showChart":
55
+ return isArray(field(input, "series")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiChart, { ...input }) : null;
56
+ case "showMetrics":
57
+ return isArray(field(input, "metrics")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiMetrics, { ...input }) : null;
58
+ case "showTimeline":
59
+ return isArray(field(input, "events")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiTimeline, { ...input }) : null;
60
+ case "showDetails":
61
+ return isArray(field(input, "fields")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiDetails, { ...input }) : null;
62
+ case "showSteps":
63
+ return isArray(field(input, "steps")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSteps, { ...input, renderDetail: renderMarkdown })) : null;
64
+ case "showSources":
65
+ return isArray(field(input, "sources")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSources, { ...input }) : null;
66
+ default:
67
+ return null;
68
+ }
69
+ }
70
+ /** Placeholder while a tool's arguments are still arriving, sized like the real thing. */
71
+ function ToolSkeleton() {
72
+ return ((0, jsx_runtime_1.jsx)("div", { className: "my-3 flex h-24 animate-pulse items-center justify-center rounded-lg border border-border bg-surface-secondary", role: "status", "aria-label": "Preparing result", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 text-accent/60", "aria-hidden": true }) }));
73
+ }
74
+ /**
75
+ * One server-sent event stream, folded into the turn being built.
76
+ *
77
+ * Hand-rolled rather than pulled from `@ai-sdk/react`, and that is a deliberate
78
+ * trade: this package ships to partners, and `ai` + `@ai-sdk/react` is a large
79
+ * dependency to add to every application platform for one panel. The wire format
80
+ * is small, both ends of it are ours, and the chunk kinds below are all this
81
+ * panel can render.
82
+ *
83
+ * Tool arguments are read from `tool-input-available` - the complete object -
84
+ * rather than reassembled from `tool-input-delta` fragments. `tool-input-start`
85
+ * is enough to put a skeleton on screen in the meantime, and partial-JSON
86
+ * parsing buys nothing a placeholder does not.
87
+ *
88
+ * Chunks are newline-delimited `data:` frames; a frame can split across network
89
+ * reads, so the tail of each read is carried forward rather than parsed.
90
+ */
91
+ async function readStream(response, onChunk) {
92
+ const body = response.body;
93
+ if (!body)
94
+ throw new Error("Ask AI returned no stream");
95
+ const reader = body.pipeThrough(new TextDecoderStream()).getReader();
96
+ let buffer = "";
97
+ for (;;) {
98
+ const { done, value } = await reader.read();
99
+ if (done)
100
+ break;
101
+ buffer += value;
102
+ // Everything up to the last newline is whole; whatever follows it is a
103
+ // partial frame and waits for the next read.
104
+ const lines = buffer.split("\n");
105
+ buffer = lines.pop() ?? "";
106
+ for (const line of lines) {
107
+ if (!line.startsWith("data:"))
108
+ continue;
109
+ const payload = line.slice(5).trim();
110
+ if (!payload || payload === "[DONE]")
111
+ continue;
112
+ let chunk;
113
+ try {
114
+ chunk = JSON.parse(payload);
115
+ }
116
+ catch {
117
+ // A frame we cannot parse is a frame we cannot render. Dropping it
118
+ // keeps the rest of the answer flowing, which is strictly better than
119
+ // failing the whole turn over one malformed chunk.
120
+ continue;
121
+ }
122
+ switch (chunk.type) {
123
+ case "text-delta":
124
+ if (chunk.delta)
125
+ onChunk({ text: chunk.delta });
126
+ break;
127
+ case "reasoning-delta":
128
+ if (chunk.delta)
129
+ onChunk({ reasoning: chunk.delta });
130
+ break;
131
+ case "tool-input-start":
132
+ if (chunk.toolCallId && chunk.toolName) {
133
+ onChunk({ toolStart: { id: chunk.toolCallId, name: chunk.toolName } });
134
+ }
135
+ break;
136
+ case "tool-input-available":
137
+ if (chunk.toolCallId)
138
+ onChunk({ toolInput: { id: chunk.toolCallId, input: chunk.input } });
139
+ break;
140
+ case "error":
141
+ throw new Error(chunk.errorText || "Ask AI failed");
142
+ }
143
+ }
144
+ }
145
+ }
146
+ /**
147
+ * Index of the final text block, or `-1`. A reverse loop rather than
148
+ * `findLastIndex`, which is ES2023 - this package targets ES2022 so its
149
+ * published output stays usable on the runtimes its `engines` field claims.
150
+ */
151
+ function lastTextBlockIndex(blocks) {
152
+ for (let index = blocks.length - 1; index >= 0; index--) {
153
+ if (blocks[index]?.kind === "text")
154
+ return index;
155
+ }
156
+ return -1;
157
+ }
158
+ /** Appends a text delta to the open text block, or opens one after a widget. */
159
+ function appendText(blocks, text) {
160
+ const last = blocks.at(-1);
161
+ if (last?.kind === "text") {
162
+ return [...blocks.slice(0, -1), { kind: "text", text: last.text + text }];
163
+ }
164
+ return [...blocks, { kind: "text", text }];
165
+ }
166
+ /**
167
+ * Ask AI - a chat panel grounded in the signed-in workspace's Context Index,
168
+ * streamed through your own bridge.
169
+ *
170
+ * Answers are not only prose. Connect's model can call a small set of
171
+ * presentation tools - `showTable`, `showChart`, `showMetrics`, `showTimeline`,
172
+ * `showDetails`, `showSteps`, `showSources` - and this panel renders each one
173
+ * natively: a sortable table with a CSV download, a chart, a row of metric
174
+ * tiles. Values arrive raw with a format tag, so they are formatted here rather
175
+ * than by the model, which is what lets a column sort numerically.
176
+ *
177
+ * Nothing here holds a credential: it POSTs to `${basePath}/ai/chat` on your
178
+ * own origin, and `createConnectHandler` is what reaches Connect. The workspace
179
+ * is resolved from the caller's session there, never sent from the browser - so
180
+ * a user can only ever ask about the workspace they are signed into.
181
+ *
182
+ * ```tsx
183
+ * <AppShell name="Dispatch" nav={nav}>
184
+ * {children}
185
+ * </AppShell>
186
+ * ```
187
+ *
188
+ * `AppShell` mounts it by default. Mount it directly only when you are not
189
+ * using the shell: it renders a fixed corner trigger plus a panel that is an
190
+ * *in-flow flex sibling* at `lg` and up, so opening it pushes your content
191
+ * aside rather than covering it. That requires it to be the last child of a
192
+ * `flex` row owning the viewport height - which is exactly what `AppShell` is.
193
+ * Below `lg` it is a full-screen sheet with a backdrop instead; there is no
194
+ * room to push anything on a phone.
195
+ *
196
+ * **Host setup:** markdown rendering needs `streamdown` installed, one Tailwind
197
+ * `@source "…/node_modules/streamdown/dist/*.js"` line so its utility classes
198
+ * are generated, and `import "streamdown/styles.css"` for the streaming
199
+ * animation. The colour tokens Streamdown expects are already mapped in this
200
+ * SDK's `theme.css`.
201
+ *
202
+ * Answers require the workspace's Context Index to be turned on (a workspace
203
+ * admin toggle in Connect) and your API key to carry the `ai:chat` scope.
204
+ * Neither is something the browser can discover, so both surface as a message
205
+ * in the panel rather than as a hidden button.
206
+ */
207
+ function ConnectAskAi({ title = "Ask AI", assistantName = "Assistant", placeholder = "Ask a question...", hint = "Ask a question about your workspace - answers are grounded in what your team has indexed.", defaultOpen = false, markdownPlugins, }) {
208
+ const { session, isLoading, streamResource } = (0, provider_js_1.useConnect)();
209
+ const [isOpen, setIsOpen] = (0, react_1.useState)(defaultOpen);
210
+ const [turns, setTurns] = (0, react_1.useState)([]);
211
+ const [input, setInput] = (0, react_1.useState)("");
212
+ const [isBusy, setIsBusy] = (0, react_1.useState)(false);
213
+ const [error, setError] = (0, react_1.useState)(null);
214
+ const abortRef = (0, react_1.useRef)(null);
215
+ const bottomRef = (0, react_1.useRef)(null);
216
+ // Ids only have to be unique within one mounted conversation, so a counter is
217
+ // enough - and unlike `crypto.randomUUID()` it cannot differ between the
218
+ // server's render and the browser's.
219
+ const nextId = (0, react_1.useRef)(0);
220
+ (0, react_1.useEffect)(() => {
221
+ bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
222
+ }, [turns]);
223
+ // A conversation left mid-stream should stop costing tokens the moment the
224
+ // panel goes away.
225
+ (0, react_1.useEffect)(() => () => abortRef.current?.abort(), []);
226
+ const renderMarkdown = (0, react_1.useCallback)((text, isStreaming = false) => ((0, jsx_runtime_1.jsx)(streamdown_1.Streamdown, { className: "connect-ai-body", mode: isStreaming ? "streaming" : "static", isAnimating: isStreaming, animated: ANIMATED, caret: isStreaming ? "block" : undefined, plugins: markdownPlugins,
227
+ // Retrieved documents cite real URLs; an interstitial on every one of
228
+ // them turns a citation into a chore. The renderer still sanitises the
229
+ // markup itself, which is the part that matters.
230
+ linkSafety: LINK_SAFETY, children: text })), [markdownPlugins]);
231
+ const stop = (0, react_1.useCallback)(() => {
232
+ abortRef.current?.abort();
233
+ abortRef.current = null;
234
+ setIsBusy(false);
235
+ }, []);
236
+ const send = (0, react_1.useCallback)(async (text) => {
237
+ const controller = new AbortController();
238
+ abortRef.current = controller;
239
+ setError(null);
240
+ setIsBusy(true);
241
+ const userTurn = {
242
+ id: `u${nextId.current++}`,
243
+ role: "user",
244
+ blocks: [{ kind: "text", text }],
245
+ reasoning: "",
246
+ };
247
+ const answerId = `a${nextId.current++}`;
248
+ // The wire format Connect reads - part-based `UIMessage`s, not flat
249
+ // strings. Built from the turns *plus* this one rather than from state,
250
+ // which has not been committed yet at this point. Only text is sent back:
251
+ // a widget is a rendering of something the model already said.
252
+ const history = [...turns, userTurn].map((turn) => ({
253
+ id: turn.id,
254
+ role: turn.role,
255
+ parts: [
256
+ {
257
+ type: "text",
258
+ text: turn.blocks
259
+ .filter((block) => block.kind === "text")
260
+ .map((block) => block.text)
261
+ .join(""),
262
+ },
263
+ ],
264
+ }));
265
+ setTurns((current) => [...current, userTurn]);
266
+ const patchAnswer = (patch) => setTurns((current) => current.map((turn) => (turn.id === answerId ? patch(turn) : turn)));
267
+ try {
268
+ // Relative to the provider's own `basePath` - `streamResource` prefixes
269
+ // it, the same way every other bridge call is addressed.
270
+ const response = await streamResource("/ai/chat", { messages: history }, { signal: controller.signal });
271
+ // The assistant's turn is appended only once the stream is live, so a
272
+ // failed request leaves an error under the question rather than an
273
+ // empty bubble that never fills.
274
+ setTurns((current) => [...current, { id: answerId, role: "assistant", blocks: [], reasoning: "" }]);
275
+ await readStream(response, (chunk) => {
276
+ if (chunk.text) {
277
+ patchAnswer((turn) => ({ ...turn, blocks: appendText(turn.blocks, chunk.text) }));
278
+ }
279
+ if (chunk.reasoning) {
280
+ patchAnswer((turn) => ({ ...turn, reasoning: turn.reasoning + chunk.reasoning }));
281
+ }
282
+ if (chunk.toolStart) {
283
+ const { id, name } = chunk.toolStart;
284
+ patchAnswer((turn) => ({ ...turn, blocks: [...turn.blocks, { kind: "tool", id, name, input: null }] }));
285
+ }
286
+ if (chunk.toolInput) {
287
+ const { id, input: toolInput } = chunk.toolInput;
288
+ patchAnswer((turn) => ({
289
+ ...turn,
290
+ blocks: turn.blocks.map((block) => block.kind === "tool" && block.id === id ? { ...block, input: toolInput } : block),
291
+ }));
292
+ }
293
+ });
294
+ }
295
+ catch (cause) {
296
+ // Pressing Stop aborts the fetch, which lands here - a decision, not a
297
+ // failure, and nothing to report.
298
+ if (!controller.signal.aborted) {
299
+ setError(cause instanceof Error ? cause.message : "Ask AI failed");
300
+ }
301
+ }
302
+ finally {
303
+ if (abortRef.current === controller)
304
+ abortRef.current = null;
305
+ setIsBusy(false);
306
+ }
307
+ }, [streamResource, turns]);
308
+ const lastTurn = turns.at(-1);
309
+ const isAnswering = isBusy && lastTurn?.role === "assistant";
310
+ const rendered = (0, react_1.useMemo)(() => turns.map((turn) => {
311
+ const isLive = isAnswering && turn.id === lastTurn?.id;
312
+ const lastTextIndex = lastTextBlockIndex(turn.blocks);
313
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `connect-ai-host rounded-md border border-border p-2.5 text-sm ${turn.role === "user" ? "bg-surface-secondary" : "bg-accent/5"}`, children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: turn.role === "user" ? "You" : assistantName }), turn.role === "assistant" && ((0, jsx_runtime_1.jsx)(ThinkingDisclosure, { text: turn.reasoning, isStreaming: isLive && lastTextIndex === -1 })), turn.blocks.map((block, index) => block.kind === "text" ? (
314
+ // The user's own turn is shown as they typed it - running a
315
+ // question that happens to start with `#` through the markdown
316
+ // renderer would turn it into a heading.
317
+ (0, jsx_runtime_1.jsx)("div", { children: turn.role === "user" ? ((0, jsx_runtime_1.jsx)("p", { className: "whitespace-pre-wrap", children: block.text })) : (renderMarkdown(block.text, isLive && index === lastTextIndex)) }, index)) : block.input === null ? ((0, jsx_runtime_1.jsx)(ToolSkeleton, {}, block.id)) : ((0, jsx_runtime_1.jsx)("div", { children: renderTool(block.name, block.input, renderMarkdown) }, block.id))), isLive && lastTextIndex === -1 && turn.blocks.length === 0 && (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] }, turn.id));
318
+ }), [turns, isAnswering, lastTurn?.id, assistantName, renderMarkdown]);
319
+ // Signed out there is no workspace to ground an answer in, and the bridge
320
+ // would answer 401 - so there is nothing to offer, not even a button.
321
+ if (isLoading || !session?.user)
322
+ return null;
323
+ const handleSubmit = (event) => {
324
+ event.preventDefault();
325
+ const text = input.trim();
326
+ if (!text || isBusy)
327
+ return;
328
+ setInput("");
329
+ void send(text);
330
+ };
331
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!isOpen && ((0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "primary", "aria-label": title, onPress: () => setIsOpen(true), className: "fixed bottom-4 end-4 z-30 size-12 rounded-full shadow-lg sm:bottom-6 sm:end-6", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)("div", { className: `fixed inset-0 z-40 bg-backdrop transition-opacity lg:hidden ${isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`, onClick: () => setIsOpen(false), "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("aside", { "aria-hidden": !isOpen, className: `fixed inset-y-0 end-0 z-50 flex flex-col border-s border-border bg-surface shadow-xl transition-transform duration-300 ease-in-out ${PANEL_WIDTH} ${isOpen ? "translate-x-0" : "translate-x-full rtl:-translate-x-full"} lg:static lg:z-auto lg:translate-x-0 lg:shadow-none lg:transition-[width] ${isOpen ? "" : "lg:w-0 lg:overflow-hidden lg:border-s-0"}`, children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-full shrink-0 flex-col ${PANEL_WIDTH}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 border-b border-border px-4 py-3", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0 text-accent" }), (0, jsx_runtime_1.jsx)("span", { className: "flex-1 text-sm font-semibold", children: title }), (0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "tertiary", "aria-label": "Close", onPress: () => setIsOpen(false), children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "size-4" }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 space-y-3 overflow-y-auto p-4", children: [turns.length === 0 && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: hint }), rendered, isBusy && lastTurn?.role === "user" && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded-md border border-border bg-accent/5 p-2.5 text-sm", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-1 text-[11px] font-semibold tracking-wide text-muted uppercase", children: assistantName }), (0, jsx_runtime_1.jsx)(ThinkingShimmer, {})] })), error && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-danger", children: error }), (0, jsx_runtime_1.jsx)("div", { ref: bottomRef })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "flex gap-2 border-t border-border p-3", children: [(0, jsx_runtime_1.jsxs)(react_2.TextField, { variant: "secondary", className: "flex-1", children: [(0, jsx_runtime_1.jsx)(react_2.Label, { className: "sr-only", children: placeholder }), (0, jsx_runtime_1.jsx)(react_2.Input, { placeholder: placeholder, value: input, onChange: (event) => setInput(event.currentTarget.value), disabled: isBusy })] }), isBusy ? ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "button", isIconOnly: true, variant: "secondary", "aria-label": "Stop generating", onPress: stop, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Square, { className: "size-3.5" }) })) : ((0, jsx_runtime_1.jsx)(react_2.Button, { type: "submit", isIconOnly: true, variant: "primary", "aria-label": "Send", isDisabled: !input.trim(), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Send, { className: "size-3.5" }) }))] })] }) })] }));
332
+ }
333
+ /** Hoisted for the same reference-equality reason as `ANIMATED` - Streamdown compares it by identity. */
334
+ const LINK_SAFETY = { enabled: false };
@@ -89,7 +89,7 @@ function isNavItemActive(item, activeHref) {
89
89
  return false;
90
90
  return normalizePath(item.href) === activeHref;
91
91
  }
92
- /** Hrefs declared `gateExempt` - the routes `ConnectAppShell` renders outside its `ConnectAccessGate`. */
92
+ /** Hrefs declared `gateExempt` - the routes `AppShell` renders outside its `ConnectAccessGate`. */
93
93
  function gateExemptPaths(nav) {
94
94
  return flattenNav(nav)
95
95
  .filter((item) => item.gateExempt)
@@ -39,7 +39,7 @@ function NavRow({ item, isActive, hasActiveChild, collapsed, depth, renderLink,
39
39
  * The sidebar's nav column on its own - sections, items, one level of
40
40
  * children, and the active highlight.
41
41
  *
42
- * `ConnectAppShell` renders this inside its rail and its mobile drawer, and it
42
+ * `AppShell` renders this inside its rail and its mobile drawer, and it
43
43
  * is exported for a host that wants the shell's navigation without its
44
44
  * chrome: the same items, the same active resolution, dropped into a layout
45
45
  * you already have.
@@ -27,6 +27,28 @@ function initials(name, email) {
27
27
  function openExternal(url) {
28
28
  window.open(url, "_blank", "noopener,noreferrer");
29
29
  }
30
+ /**
31
+ * Connect's profile page, with the context it needs to offer a way back here.
32
+ *
33
+ * Connect answers `/profile` for any signed-in user, so the bare URL works on
34
+ * its own - these two params only add the "Back to <your app>" banner. It
35
+ * validates `return_to` against this `client_id`'s registered redirect
36
+ * origins and silently drops the banner if they don't match, so a wrong or
37
+ * stale `appUrl`/`clientId` pairing degrades to what it did before rather
38
+ * than erroring.
39
+ *
40
+ * `window.location.href` rather than a configured home URL: the point is to
41
+ * come back to the page they left, and only the browser knows which one that
42
+ * is.
43
+ */
44
+ function profileUrl(appUrl, clientId) {
45
+ const url = new URL("/profile", appUrl);
46
+ url.searchParams.set("client_id", clientId);
47
+ if (typeof window !== "undefined") {
48
+ url.searchParams.set("return_to", window.location.href);
49
+ }
50
+ return url.toString();
51
+ }
30
52
  /**
31
53
  * The member's photo, with their initials underneath it.
32
54
  *
@@ -122,7 +144,7 @@ function DestinationTile({ icon, label, url, }) {
122
144
  * the modal programmatically; sign-out uses `slot="close"` instead.
123
145
  */
124
146
  function ConnectUserMenu({ compact = false, onSignOut, className, theme, onThemeChange, }) {
125
- const { session, isLoading, signOut, appUrl } = (0, provider_js_1.useConnect)();
147
+ const { session, isLoading, signOut, appUrl, clientId } = (0, provider_js_1.useConnect)();
126
148
  if (isLoading) {
127
149
  return ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center gap-2 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-8 shrink-0 rounded-full" }), !compact && (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-4 w-24" })] }));
128
150
  }
@@ -136,7 +158,7 @@ function ConnectUserMenu({ compact = false, onSignOut, className, theme, onTheme
136
158
  // `pl-1` against the default right padding is optical, not sloppy: a
137
159
  // circular avatar needs less inset than a text label to look evenly
138
160
  // seated in the button.
139
- className: `gap-2 pl-1 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "sm" }), !compact && (0, jsx_runtime_1.jsx)("span", { className: "text-sm font-medium text-foreground", children: label })] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "md", scroll: "inside", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Account" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: `flex items-start gap-3 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-4`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.email }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.workspaceName }), (role || planChip || !user.emailVerified) && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-3 gap-2", children: [(0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Profile", url: `${appUrl}/profile` }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: surfaces_js_1.ICON }), label: "Connect", url: appUrl })] }), theme && onThemeChange && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
161
+ className: `gap-2 pl-1 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "sm" }), !compact && (0, jsx_runtime_1.jsx)("span", { className: "text-sm font-medium text-foreground", children: label })] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "md", scroll: "inside", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Account" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: `flex items-start gap-3 rounded-xl border border-border ${surfaces_js_1.PANEL_FILL} p-4`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.email }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.workspaceName }), (role || planChip || !user.emailVerified) && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-3 gap-2", children: [(0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Profile", url: profileUrl(appUrl, clientId) }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: surfaces_js_1.ICON, "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: surfaces_js_1.ICON }), label: "Connect", url: appUrl })] }), theme && onThemeChange && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
140
162
  const [next] = [...keys];
141
163
  if (typeof next === "string")
142
164
  onThemeChange(next);
@@ -175,13 +197,14 @@ function ConnectInvoicesButton({ className }) {
175
197
  * `ConnectHomeButton`.
176
198
  *
177
199
  * It was reachable only from inside `ConnectUserMenu`, which meant an app that
178
- * builds its own account menu had to hardcode `${appUrl}/profile` to offer it.
200
+ * builds its own account menu had to hardcode `${appUrl}/profile` to offer it -
201
+ * and got no `return_to`, so the user had no way back.
179
202
  */
180
203
  function ConnectProfileButton({ className }) {
181
- const { session, appUrl } = (0, provider_js_1.useConnect)();
204
+ const { session, appUrl, clientId } = (0, provider_js_1.useConnect)();
182
205
  if (!session?.user)
183
206
  return null;
184
- return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal(`${appUrl}/profile`), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4 shrink-0" }), "Profile", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
207
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal(profileUrl(appUrl, clientId)), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4 shrink-0" }), "Profile", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
185
208
  }
186
209
  /** A plain "Open Connect" button, for a footer or a help menu. */
187
210
  function ConnectHomeButton({ className }) {
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectWorkspaceTypeGate = ConnectWorkspaceTypeGate;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const provider_js_1 = require("../provider.js");
7
+ /**
8
+ * Renders `children` only for a workspace of the given type.
9
+ *
10
+ * ```tsx
11
+ * <ConnectWorkspaceTypeGate type="business">
12
+ * <TeamMembersTable />
13
+ * </ConnectWorkspaceTypeGate>
14
+ * ```
15
+ *
16
+ * The point is dead controls, not access: an `"individual"` workspace has
17
+ * exactly one seat and can never have another, so a members table, an invite
18
+ * button and a seat meter are all things that can only ever fail there.
19
+ * Hiding them is a better answer than letting someone press one and read an
20
+ * error about an account shape they did not choose.
21
+ *
22
+ * This is emphatically **not** the audience gate. It does not know what your
23
+ * application is sold to and will happily render a business-only subtree to a
24
+ * business workspace that has no business being in your application at all -
25
+ * that is `<ConnectAccessGate>`'s job, off the `audience_mismatch` readiness
26
+ * blocker. Use this one *inside* an application both audiences can reach, to
27
+ * vary what each sees.
28
+ *
29
+ * Like every gate in this package it is a **UX gate, not a security
30
+ * boundary**: the browser can ignore it. Anything that must not happen for an
31
+ * individual workspace has to be refused on your server too.
32
+ *
33
+ * Renders `loading` - not `fallback` - while the session is unknown, so a
34
+ * business account never flickers through the individual-account layout.
35
+ */
36
+ function ConnectWorkspaceTypeGate({ type, children, fallback = null, loading = null, }) {
37
+ const { session, isLoading } = (0, provider_js_1.useConnect)();
38
+ const workspaceType = session?.user?.workspaceType ?? null;
39
+ if (isLoading)
40
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: loading });
41
+ // Signed out is not "the other type" - there is no workspace to be a type
42
+ // of yet, and rendering the fallback here would put an individual-account
43
+ // layout in front of a visitor who has not signed in.
44
+ if (!workspaceType)
45
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: loading });
46
+ const accepted = Array.isArray(type) ? type : [type];
47
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: accepted.includes(workspaceType) ? children : fallback });
48
+ }
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.ConnectAppsGrid = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = exports.normalizePath = exports.flattenNav = exports.normalizeNav = exports.useActivePath = exports.ConnectSideNav = exports.ConnectAppShell = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectUser = exports.useConnect = exports.ConnectProvider = void 0;
5
- exports.resolvePlanGroup = exports.groupPlansByTier = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.useConnectFeature = void 0;
4
+ exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = exports.normalizePath = exports.flattenNav = exports.normalizeNav = exports.useActivePath = exports.ConnectSideNav = exports.AppShell = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectAudience = exports.useConnectWorkspaceType = exports.useConnectUser = exports.useConnect = exports.ConnectProvider = void 0;
5
+ exports.resolvePlanGroup = exports.groupPlansByTier = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.AI_TONE_DOT = exports.AI_TONE_CHIP = void 0;
6
6
  /**
7
7
  * Browser half of `@cosmoops/connect-sdk` - React components and hooks.
8
8
  *
@@ -19,6 +19,8 @@ var provider_js_1 = require("./provider.js");
19
19
  Object.defineProperty(exports, "ConnectProvider", { enumerable: true, get: function () { return provider_js_1.ConnectProvider; } });
20
20
  Object.defineProperty(exports, "useConnect", { enumerable: true, get: function () { return provider_js_1.useConnect; } });
21
21
  Object.defineProperty(exports, "useConnectUser", { enumerable: true, get: function () { return provider_js_1.useConnectUser; } });
22
+ Object.defineProperty(exports, "useConnectWorkspaceType", { enumerable: true, get: function () { return provider_js_1.useConnectWorkspaceType; } });
23
+ Object.defineProperty(exports, "useConnectAudience", { enumerable: true, get: function () { return provider_js_1.useConnectAudience; } });
22
24
  Object.defineProperty(exports, "useConnectEntitlements", { enumerable: true, get: function () { return provider_js_1.useConnectEntitlements; } });
23
25
  Object.defineProperty(exports, "useConnectPlans", { enumerable: true, get: function () { return provider_js_1.useConnectPlans; } });
24
26
  Object.defineProperty(exports, "useConnectApps", { enumerable: true, get: function () { return provider_js_1.useConnectApps; } });
@@ -59,7 +61,7 @@ Object.defineProperty(exports, "ConnectAccountSessions", { enumerable: true, get
59
61
  // reaching for a sidebar wants to find the launcher and the account menu in
60
62
  // the same breath, since the shell already mounts both.
61
63
  var app_shell_js_1 = require("./components/app-shell.js");
62
- Object.defineProperty(exports, "ConnectAppShell", { enumerable: true, get: function () { return app_shell_js_1.ConnectAppShell; } });
64
+ Object.defineProperty(exports, "AppShell", { enumerable: true, get: function () { return app_shell_js_1.AppShell; } });
63
65
  var side_nav_js_1 = require("./components/side-nav.js");
64
66
  Object.defineProperty(exports, "ConnectSideNav", { enumerable: true, get: function () { return side_nav_js_1.ConnectSideNav; } });
65
67
  var use_active_path_js_1 = require("./use-active-path.js");
@@ -77,6 +79,30 @@ Object.defineProperty(exports, "isNavItemActive", { enumerable: true, get: funct
77
79
  Object.defineProperty(exports, "gateExemptPaths", { enumerable: true, get: function () { return nav_js_1.gateExemptPaths; } });
78
80
  var apps_grid_js_1 = require("./components/apps-grid.js");
79
81
  Object.defineProperty(exports, "ConnectAppsGrid", { enumerable: true, get: function () { return apps_grid_js_1.ConnectAppsGrid; } });
82
+ // `AppShell askAi` mounts this; exported for an app that wants the panel
83
+ // without the frame, or wants it somewhere other than the shell's flex row
84
+ // (at which point it overlays rather than pushes - see its doc comment).
85
+ var ask_ai_js_1 = require("./components/ask-ai.js");
86
+ Object.defineProperty(exports, "ConnectAskAi", { enumerable: true, get: function () { return ask_ai_js_1.ConnectAskAi; } });
87
+ // The seven widgets an Ask AI answer can draw, and the formatter they share -
88
+ // exported so a partner rendering Connect's chat somewhere else entirely (a
89
+ // support console, an embedded assistant) gets the same table, chart and metric
90
+ // tiles rather than reimplementing them from the wire types.
91
+ var ai_widgets_js_1 = require("./components/ai-widgets.js");
92
+ Object.defineProperty(exports, "AiTable", { enumerable: true, get: function () { return ai_widgets_js_1.AiTable; } });
93
+ Object.defineProperty(exports, "AiChart", { enumerable: true, get: function () { return ai_widgets_js_1.AiChart; } });
94
+ Object.defineProperty(exports, "AiMetrics", { enumerable: true, get: function () { return ai_widgets_js_1.AiMetrics; } });
95
+ Object.defineProperty(exports, "AiTimeline", { enumerable: true, get: function () { return ai_widgets_js_1.AiTimeline; } });
96
+ Object.defineProperty(exports, "AiDetails", { enumerable: true, get: function () { return ai_widgets_js_1.AiDetails; } });
97
+ Object.defineProperty(exports, "AiSteps", { enumerable: true, get: function () { return ai_widgets_js_1.AiSteps; } });
98
+ Object.defineProperty(exports, "AiSources", { enumerable: true, get: function () { return ai_widgets_js_1.AiSources; } });
99
+ var ai_format_js_1 = require("./components/ai-format.js");
100
+ Object.defineProperty(exports, "formatAiValue", { enumerable: true, get: function () { return ai_format_js_1.formatAiValue; } });
101
+ Object.defineProperty(exports, "isNumericAiFormat", { enumerable: true, get: function () { return ai_format_js_1.isNumericAiFormat; } });
102
+ Object.defineProperty(exports, "aiStatusTone", { enumerable: true, get: function () { return ai_format_js_1.aiStatusTone; } });
103
+ Object.defineProperty(exports, "AI_EMPTY_VALUE", { enumerable: true, get: function () { return ai_format_js_1.AI_EMPTY_VALUE; } });
104
+ Object.defineProperty(exports, "AI_TONE_CHIP", { enumerable: true, get: function () { return ai_format_js_1.AI_TONE_CHIP; } });
105
+ Object.defineProperty(exports, "AI_TONE_DOT", { enumerable: true, get: function () { return ai_format_js_1.AI_TONE_DOT; } });
80
106
  var integrations_modal_js_1 = require("./components/integrations-modal.js");
81
107
  Object.defineProperty(exports, "ConnectIntegrationsModal", { enumerable: true, get: function () { return integrations_modal_js_1.ConnectIntegrationsModal; } });
82
108
  var plans_js_1 = require("./components/plans.js");
@@ -104,6 +130,8 @@ Object.defineProperty(exports, "useConnectAccessGate", { enumerable: true, get:
104
130
  var feature_gate_js_1 = require("./components/feature-gate.js");
105
131
  Object.defineProperty(exports, "ConnectFeatureGate", { enumerable: true, get: function () { return feature_gate_js_1.ConnectFeatureGate; } });
106
132
  Object.defineProperty(exports, "useConnectFeature", { enumerable: true, get: function () { return feature_gate_js_1.useConnectFeature; } });
133
+ var workspace_type_gate_js_1 = require("./components/workspace-type-gate.js");
134
+ Object.defineProperty(exports, "ConnectWorkspaceTypeGate", { enumerable: true, get: function () { return workspace_type_gate_js_1.ConnectWorkspaceTypeGate; } });
107
135
  var usage_meter_js_1 = require("./components/usage-meter.js");
108
136
  Object.defineProperty(exports, "ConnectUsageMeter", { enumerable: true, get: function () { return usage_meter_js_1.ConnectUsageMeter; } });
109
137
  var plans_page_js_1 = require("./components/plans-page.js");