@cosmoops/connect-sdk 0.7.0 → 0.15.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 (154) hide show
  1. package/CHANGELOG.md +662 -0
  2. package/README.md +376 -124
  3. package/dist/cjs/cli/doctor.js +3 -3
  4. package/dist/cjs/cli/init.js +3 -3
  5. package/dist/cjs/client/components/access-gate.js +59 -5
  6. package/dist/cjs/client/components/ai-format.js +135 -0
  7. package/dist/cjs/client/components/ai-widgets.js +232 -0
  8. package/dist/cjs/client/components/app-shell.js +80 -45
  9. package/dist/cjs/client/components/apps-grid.js +3 -2
  10. package/dist/cjs/client/components/ask-ai.js +353 -0
  11. package/dist/cjs/client/components/confirm-dialog.js +40 -0
  12. package/dist/cjs/client/components/cursor-pagination.js +22 -0
  13. package/dist/cjs/client/components/filter-tabs.js +24 -0
  14. package/dist/cjs/client/components/grievance-notice.js +30 -0
  15. package/dist/cjs/client/components/link-button.js +17 -10
  16. package/dist/cjs/client/components/logo.js +4 -3
  17. package/dist/cjs/client/components/nav.js +42 -1
  18. package/dist/cjs/client/components/plan-pricing.js +27 -0
  19. package/dist/cjs/client/components/plans.js +1 -1
  20. package/dist/cjs/client/components/platform-gate.js +48 -0
  21. package/dist/cjs/client/components/shell-context.js +149 -0
  22. package/dist/cjs/client/components/side-nav.js +50 -24
  23. package/dist/cjs/client/components/sidebar.js +127 -0
  24. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  25. package/dist/cjs/client/components/subscription.js +1 -1
  26. package/dist/cjs/client/components/user-menu.js +122 -40
  27. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  28. package/dist/cjs/client/components/workspace-type-gate.js +48 -0
  29. package/dist/cjs/client/index.js +98 -3
  30. package/dist/cjs/client/provider.js +100 -2
  31. package/dist/cjs/client/use-checkout.js +9 -5
  32. package/dist/cjs/errors.js +1 -1
  33. package/dist/cjs/index.js +11 -1
  34. package/dist/cjs/next/index.js +2 -2
  35. package/dist/cjs/server/client.js +25 -2
  36. package/dist/cjs/server/handler.js +237 -16
  37. package/dist/cjs/server/http.js +52 -26
  38. package/dist/cjs/server/resources/acting-as.js +16 -0
  39. package/dist/cjs/server/resources/ai.js +15 -2
  40. package/dist/cjs/server/resources/data.js +374 -0
  41. package/dist/cjs/server/resources/entitlements.js +41 -0
  42. package/dist/cjs/server/resources/identity.js +9 -3
  43. package/dist/cjs/server/resources/kv.js +84 -0
  44. package/dist/cjs/server/resources/platform.js +43 -0
  45. package/dist/cjs/server/resources/sessions.js +1 -1
  46. package/dist/cjs/server/resources/workspaces.js +74 -0
  47. package/dist/cjs/store.js +152 -0
  48. package/dist/cjs/ui/empty-state.js +28 -0
  49. package/dist/cjs/ui/index.js +44 -0
  50. package/dist/cjs/ui/page-header.js +27 -0
  51. package/dist/cjs/ui/page-shell.js +44 -0
  52. package/dist/cjs/ui/section.js +20 -0
  53. package/dist/cjs/ui/setting-row.js +14 -0
  54. package/dist/cjs/ui/status-chip.js +17 -0
  55. package/dist/esm/cli/doctor.js +3 -3
  56. package/dist/esm/cli/init.js +3 -3
  57. package/dist/esm/client/components/access-gate.js +60 -6
  58. package/dist/esm/client/components/ai-format.js +129 -0
  59. package/dist/esm/client/components/ai-widgets.js +223 -0
  60. package/dist/esm/client/components/app-shell.js +85 -50
  61. package/dist/esm/client/components/apps-grid.js +3 -2
  62. package/dist/esm/client/components/ask-ai.js +350 -0
  63. package/dist/esm/client/components/confirm-dialog.js +37 -0
  64. package/dist/esm/client/components/cursor-pagination.js +19 -0
  65. package/dist/esm/client/components/filter-tabs.js +21 -0
  66. package/dist/esm/client/components/grievance-notice.js +27 -0
  67. package/dist/esm/client/components/link-button.js +17 -10
  68. package/dist/esm/client/components/logo.js +4 -3
  69. package/dist/esm/client/components/nav.js +41 -1
  70. package/dist/esm/client/components/plan-pricing.js +25 -0
  71. package/dist/esm/client/components/plans.js +2 -2
  72. package/dist/esm/client/components/platform-gate.js +45 -0
  73. package/dist/esm/client/components/shell-context.js +141 -0
  74. package/dist/esm/client/components/side-nav.js +49 -22
  75. package/dist/esm/client/components/sidebar.js +120 -0
  76. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  77. package/dist/esm/client/components/subscription.js +2 -2
  78. package/dist/esm/client/components/user-menu.js +121 -41
  79. package/dist/esm/client/components/workspace-switcher.js +97 -0
  80. package/dist/esm/client/components/workspace-type-gate.js +45 -0
  81. package/dist/esm/client/index.js +58 -5
  82. package/dist/esm/client/provider.js +94 -2
  83. package/dist/esm/client/use-checkout.js +9 -5
  84. package/dist/esm/errors.js +1 -1
  85. package/dist/esm/index.js +1 -0
  86. package/dist/esm/next/index.js +2 -2
  87. package/dist/esm/server/client.js +25 -2
  88. package/dist/esm/server/handler.js +237 -16
  89. package/dist/esm/server/http.js +52 -26
  90. package/dist/esm/server/resources/acting-as.js +13 -0
  91. package/dist/esm/server/resources/ai.js +15 -2
  92. package/dist/esm/server/resources/data.js +369 -0
  93. package/dist/esm/server/resources/entitlements.js +41 -0
  94. package/dist/esm/server/resources/identity.js +9 -3
  95. package/dist/esm/server/resources/kv.js +80 -0
  96. package/dist/esm/server/resources/platform.js +39 -0
  97. package/dist/esm/server/resources/sessions.js +1 -1
  98. package/dist/esm/server/resources/workspaces.js +70 -0
  99. package/dist/esm/store.js +141 -0
  100. package/dist/esm/ui/empty-state.js +25 -0
  101. package/dist/esm/ui/index.js +34 -0
  102. package/dist/esm/ui/page-header.js +24 -0
  103. package/dist/esm/ui/page-shell.js +41 -0
  104. package/dist/esm/ui/section.js +17 -0
  105. package/dist/esm/ui/setting-row.js +11 -0
  106. package/dist/esm/ui/status-chip.js +14 -0
  107. package/dist/theme.css +87 -0
  108. package/dist/types/client/components/access-gate.d.ts +17 -3
  109. package/dist/types/client/components/ai-format.d.ts +24 -0
  110. package/dist/types/client/components/ai-widgets.d.ts +42 -0
  111. package/dist/types/client/components/app-shell.d.ts +85 -31
  112. package/dist/types/client/components/apps-grid.d.ts +11 -2
  113. package/dist/types/client/components/ask-ai.d.ts +73 -0
  114. package/dist/types/client/components/confirm-dialog.d.ts +34 -0
  115. package/dist/types/client/components/cursor-pagination.d.ts +27 -0
  116. package/dist/types/client/components/filter-tabs.d.ts +26 -0
  117. package/dist/types/client/components/grievance-notice.d.ts +31 -0
  118. package/dist/types/client/components/link-button.d.ts +32 -9
  119. package/dist/types/client/components/logo.d.ts +18 -2
  120. package/dist/types/client/components/nav.d.ts +27 -3
  121. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  122. package/dist/types/client/components/platform-gate.d.ts +53 -0
  123. package/dist/types/client/components/shell-context.d.ts +116 -0
  124. package/dist/types/client/components/side-nav.d.ts +28 -14
  125. package/dist/types/client/components/sidebar.d.ts +95 -0
  126. package/dist/types/client/components/user-menu.d.ts +70 -23
  127. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  128. package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
  129. package/dist/types/client/index.d.ts +36 -7
  130. package/dist/types/client/provider.d.ts +149 -3
  131. package/dist/types/client/use-checkout.d.ts +8 -4
  132. package/dist/types/errors.d.ts +1 -1
  133. package/dist/types/index.d.ts +2 -0
  134. package/dist/types/next/index.d.ts +1 -1
  135. package/dist/types/server/client.d.ts +25 -2
  136. package/dist/types/server/http.d.ts +14 -3
  137. package/dist/types/server/index.d.ts +4 -0
  138. package/dist/types/server/resources/acting-as.d.ts +11 -0
  139. package/dist/types/server/resources/ai.d.ts +9 -1
  140. package/dist/types/server/resources/data.d.ts +258 -0
  141. package/dist/types/server/resources/entitlements.d.ts +33 -1
  142. package/dist/types/server/resources/kv.d.ts +63 -0
  143. package/dist/types/server/resources/platform.d.ts +32 -0
  144. package/dist/types/server/resources/workspaces.d.ts +55 -0
  145. package/dist/types/store.d.ts +96 -0
  146. package/dist/types/types.d.ts +491 -13
  147. package/dist/types/ui/empty-state.d.ts +34 -0
  148. package/dist/types/ui/index.d.ts +40 -0
  149. package/dist/types/ui/page-header.d.ts +34 -0
  150. package/dist/types/ui/page-shell.d.ts +39 -0
  151. package/dist/types/ui/section.d.ts +26 -0
  152. package/dist/types/ui/setting-row.d.ts +18 -0
  153. package/dist/types/ui/status-chip.d.ts +36 -0
  154. package/package.json +13 -3
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.ConnectAppShell = ConnectAppShell;
4
+ exports.AppShell = AppShell;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const react_1 = require("react");
7
7
  const react_2 = require("@heroui/react");
@@ -9,9 +9,14 @@ const lucide_react_1 = require("lucide-react");
9
9
  const provider_js_1 = require("../provider.js");
10
10
  const access_gate_js_1 = require("./access-gate.js");
11
11
  const apps_grid_js_1 = require("./apps-grid.js");
12
+ const ask_ai_js_1 = require("./ask-ai.js");
12
13
  const auth_buttons_js_1 = require("./auth-buttons.js");
13
14
  const logo_js_1 = require("./logo.js");
15
+ const motion_js_1 = require("./motion.js");
16
+ const sidebar_js_1 = require("./sidebar.js");
14
17
  const side_nav_js_1 = require("./side-nav.js");
18
+ const shell_context_js_1 = require("./shell-context.js");
19
+ const workspace_switcher_js_1 = require("./workspace-switcher.js");
15
20
  const user_menu_js_1 = require("./user-menu.js");
16
21
  const nav_js_1 = require("./nav.js");
17
22
  const use_active_path_js_1 = require("../use-active-path.js");
@@ -36,7 +41,7 @@ function collapsedSnapshot() {
36
41
  /**
37
42
  * No preference on the server, which is the point: `useSyncExternalStore` uses
38
43
  * this for the hydrating render and only then swaps in the real reading, so a
39
- * returning user's collapsed rail can never make the server's HTML and the
44
+ * returning user's rail state can never make the server's HTML and the
40
45
  * client's first pass disagree. Reading storage during render instead is a
41
46
  * hydration mismatch waiting for its first user.
42
47
  */
@@ -54,65 +59,73 @@ function subscribeCollapsed(onStoreChange) {
54
59
  };
55
60
  }
56
61
  /**
57
- * The sidebar's brand row: the lockup when expanded, the badge alone when not.
58
- */
59
- function BrandLink({ name, icon, homeHref, collapsed, renderLink, }) {
60
- return renderLink({
61
- href: homeHref,
62
- className: `flex items-center rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus ${collapsed ? "justify-center" : ""}`,
63
- // `ConnectLogo` names itself, but it does it with `role="img"` on a
64
- // `<span>`, which the `<a>` wrapping it does not inherit as its own name.
65
- "aria-label": `${name} home`,
66
- children: (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon, collapsed: collapsed }),
67
- });
68
- }
69
- /**
70
- * The application frame every CosmoOps application platform runs inside: a
71
- * collapsible sidebar, a top bar carrying the app launcher and the account
72
- * menu, and your pages in the middle.
62
+ * The application frame every CosmoOps application platform runs inside: one
63
+ * rail down the side holding the brand, the workspace, the navigation and the
64
+ * account, and your pages in the rest.
73
65
  *
74
66
  * This is the piece each partner was otherwise writing by hand, and writing
75
- * slightly differently - which is how a family of applications stops looking like
76
- * one product. Everything that should be identical across platforms (the
77
- * lockup, the rail geometry, the active-route treatment, the launcher, the
78
- * account menu, the access gate) lives here; everything that genuinely
79
- * differs is a prop:
67
+ * slightly differently - which is how a family of applications stops looking
68
+ * like one product. Everything that should be identical across platforms
69
+ * lives here; everything that genuinely differs is a prop:
80
70
  *
81
71
  * ```tsx
82
72
  * "use client";
83
73
  * import Link from "next/link";
84
74
  * import { usePathname } from "next/navigation";
85
- * import { ConnectAppShell } from "@cosmoops/connect-sdk/client";
86
- * import { LayoutDashboard, Truck } from "lucide-react";
75
+ * import { AppShell } from "@cosmoops/connect-sdk/client";
76
+ * import { LayoutDashboard, Settings, Truck } from "lucide-react";
87
77
  *
88
78
  * export function Shell({ children }: { children: React.ReactNode }) {
89
79
  * return (
90
- * <ConnectAppShell
80
+ * <AppShell
91
81
  * name="Dispatch"
92
82
  * icon={<Truck />}
93
83
  * activePath={usePathname()}
94
84
  * renderLink={(props) => <Link {...props} />}
95
85
  * nav={[
96
86
  * { label: "Overview", href: "/", icon: <LayoutDashboard className="size-5" /> },
97
- * { label: "Plans", href: "/plans", icon: <Truck className="size-5" />, gateExempt: true },
87
+ * { label: "Shipments", href: "/shipments", icon: <Truck className="size-5" /> },
88
+ * ]}
89
+ * secondaryNav={[
90
+ * { label: "Settings", href: "/settings", icon: <Settings className="size-5" /> },
98
91
  * ]}
99
92
  * >
100
93
  * {children}
101
- * </ConnectAppShell>
94
+ * </AppShell>
102
95
  * );
103
96
  * }
104
97
  * ```
105
98
  *
99
+ * **There is no top bar.** There used to be one, carrying a page title, the
100
+ * launcher and the account menu - and it was a second, half-width strip of
101
+ * chrome saying things the rail was better placed to say. The launcher and
102
+ * the account row moved into the rail's footer, where they are one press from
103
+ * wherever the pointer already is; the page title moved into the page, where
104
+ * `ConnectPageShell` renders it next to that page's own actions and
105
+ * breadcrumbs. What is left is a single column of chrome and a full-height
106
+ * content pane, which is what every application this shell is modelled on
107
+ * settled at.
108
+ *
109
+ * The one exception is below `md`, where a slim bar carries the drawer
110
+ * trigger. A phone has no rail to put it in, and a navigation you cannot open
111
+ * is not a smaller navigation.
112
+ *
113
+ * **The rail starts collapsed.** `defaultCollapsed` is `true`: the icon rail
114
+ * is the resting state, and expanding is the deliberate act. That is the right
115
+ * default because the rail's job is to be *there*, not to be read - a person
116
+ * learns five glyphs in a day and then wants the 200px back for their data.
117
+ * A stored preference from the user's last visit always wins over it.
118
+ *
106
119
  * There is no `isSignedIn` branch anywhere in here, and there should be none
107
- * in yours: the auth buttons render nothing once a session exists, the user
108
- * menu renders nothing without one, and the launcher renders nothing when
120
+ * in yours: the auth buttons render nothing once a session exists, the account
121
+ * row renders nothing without one, and the launcher renders nothing when
109
122
  * signed out. The swap happens inside the components.
110
123
  *
111
124
  * It renders no provider of its own - mount `ConnectProvider` above it, once,
112
125
  * so a page with a shell, a plan table and a usage meter still makes one
113
126
  * session read.
114
127
  */
115
- function ConnectAppShell({ name, icon, homeHref = "/", nav, activePath, renderLink = side_nav_js_1.defaultRenderLink, title, headerActions, sidebarFooter, footer, defaultCollapsed = false, collapsed: controlledCollapsed, onCollapsedChange, collapsible = true, showAppsGrid = true, showAuthButtons = true, theme, onThemeChange, gate = true, gateExemptRoutes, className, contentClassName, children, }) {
128
+ function AppShell({ name, icon, homeHref = "/", nav, secondaryNav, activePath, renderLink = shell_context_js_1.defaultRenderLink, sidebarFooter, workspaceSwitcher, footer, defaultCollapsed = true, collapsed: controlledCollapsed, onCollapsedChange, collapsible = true, showAppsGrid = true, showAuthButtons = true, theme, onThemeChange, gate = true, gateExemptRoutes, askAi = true, className, contentClassName, children, }) {
116
129
  const { session, isLoading } = (0, provider_js_1.useConnect)();
117
130
  const pathname = (0, use_active_path_js_1.useActivePath)(activePath);
118
131
  const [mobileOpen, setMobileOpen] = (0, react_1.useState)(false);
@@ -122,8 +135,7 @@ function ConnectAppShell({ name, icon, homeHref = "/", nav, activePath, renderLi
122
135
  const stored = (0, react_1.useSyncExternalStore)(subscribeCollapsed, collapsedSnapshot, serverCollapsedSnapshot);
123
136
  const isControlled = controlledCollapsed !== undefined;
124
137
  const collapsed = collapsible ? (isControlled ? controlledCollapsed : (stored ?? defaultCollapsed)) : false;
125
- const toggleCollapsed = (0, react_1.useCallback)(() => {
126
- const next = !collapsed;
138
+ const setCollapsed = (0, react_1.useCallback)((next) => {
127
139
  onCollapsedChange?.(next);
128
140
  if (isControlled)
129
141
  return;
@@ -134,24 +146,47 @@ function ConnectAppShell({ name, icon, homeHref = "/", nav, activePath, renderLi
134
146
  // Not persisted - but the event below still moves the rail for this session.
135
147
  }
136
148
  window.dispatchEvent(new Event(COLLAPSED_EVENT));
137
- }, [collapsed, isControlled, onCollapsedChange]);
149
+ }, [isControlled, onCollapsedChange]);
138
150
  const closeMobile = (0, react_1.useCallback)(() => setMobileOpen(false), []);
151
+ // Connect platform staff see a different set of routes - see
152
+ // `ConnectNavItem.audience`. Derived from the session rather than passed in,
153
+ // so a host app never has to thread "is this an operator" through its own
154
+ // layout to get the right rail.
155
+ const isStaff = session?.staff === true;
156
+ const visibleNav = (0, react_1.useMemo)(() => (0, nav_js_1.filterNavForAudience)(nav, isStaff), [nav, isStaff]);
157
+ const visibleSecondaryNav = (0, react_1.useMemo)(() => (secondaryNav ? (0, nav_js_1.filterNavForAudience)(secondaryNav, isStaff) : undefined), [secondaryNav, isStaff]);
139
158
  const currentPath = (0, nav_js_1.normalizePath)(pathname);
140
- const exempt = [...(0, nav_js_1.gateExemptPaths)(nav), ...(gateExemptRoutes ?? []).map(nav_js_1.normalizePath)];
159
+ // Computed from the *unfiltered* nav on purpose: `gateExempt` is a fact
160
+ // about a route, not about who can see it, and reading it from the filtered
161
+ // list would quietly re-gate a route the moment it became staff-only. Both
162
+ // navs contribute - a `gateExempt` "Plans" route is just as often in the
163
+ // secondary group as the primary one.
164
+ const exempt = [
165
+ ...(0, nav_js_1.gateExemptPaths)(nav),
166
+ ...(secondaryNav ? (0, nav_js_1.gateExemptPaths)(secondaryNav) : []),
167
+ ...(gateExemptRoutes ?? []).map(nav_js_1.normalizePath),
168
+ ];
141
169
  const isExempt = exempt.some((path) => currentPath === path || currentPath.startsWith(`${path}/`));
142
170
  // A signed-out visitor has no readiness to check and nothing to be blocked
143
171
  // by; `ConnectAccessGate` passes them straight through, but mounting it at
144
172
  // all costs an entitlements read that cannot succeed.
145
173
  const signedOut = !isLoading && !session?.user;
146
174
  const gated = gate && !isExempt && !signedOut;
147
- const sideNav = (mobile) => ((0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSideNav, { nav: nav, activePath: pathname, renderLink: renderLink, collapsed: mobile ? false : collapsed, onNavigate: mobile ? closeMobile : undefined }));
148
- return ((0, jsx_runtime_1.jsxs)("div", { className: `flex h-screen bg-background ${className ?? ""}`, children: [(0, jsx_runtime_1.jsxs)("aside", { "aria-label": "Sidebar", className: `hidden shrink-0 flex-col border-r border-border bg-surface transition-[width] duration-200 ease-in-out will-change-[width] md:flex ${collapsed ? "w-16" : "w-64"}`, children: [(0, jsx_runtime_1.jsx)("div", { className: `flex h-16 shrink-0 items-center border-b border-border ${collapsed ? "justify-center px-2" : "px-4"}`, children: (0, jsx_runtime_1.jsx)(BrandLink, { name: name, icon: icon, homeHref: homeHref, collapsed: collapsed, renderLink: renderLink }) }), (0, jsx_runtime_1.jsx)("div", { className: `flex-1 overflow-y-auto py-4 ${collapsed ? "px-2" : "px-3"}`, children: sideNav(false) }), (0, jsx_runtime_1.jsxs)("div", { className: `shrink-0 border-t border-border ${collapsed ? "flex flex-col items-center gap-2 p-2" : "flex flex-col gap-2 p-3"}`, children: [!collapsed && sidebarFooter, collapsible && (
149
- // The button goes in as a *direct* child, the way `Modal` and
150
- // `Dropdown` take theirs. `Tooltip.Trigger` renders a wrapper
151
- // `<div>` and puts the props it is given on that div - so an
152
- // `aria-label` passed there names a div nobody can reach and
153
- // leaves this icon-only button with no accessible name at all.
154
- // The wrapper is for trigger elements React Aria cannot drive
155
- // directly; a HeroUI `Button` is not one.
156
- (0, jsx_runtime_1.jsxs)(react_2.Tooltip, { delay: 200, children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar", className: collapsed ? "" : "self-start", onPress: toggleCollapsed, children: collapsed ? (0, jsx_runtime_1.jsx)(lucide_react_1.PanelLeftOpen, { className: "size-4" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.PanelLeftClose, { className: "size-4" }) }), (0, jsx_runtime_1.jsx)(react_2.Tooltip.Content, { placement: "right", children: collapsed ? "Expand sidebar" : "Collapse sidebar" })] }))] })] }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Content, { placement: "left", className: "w-72 max-w-[80vw]", children: (0, jsx_runtime_1.jsxs)(react_2.Drawer.Dialog, { "aria-label": "Navigation menu", children: [(0, jsx_runtime_1.jsx)(react_2.Drawer.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_2.Drawer.Header, { children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Heading, { "aria-label": name, children: (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon }) }) }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Body, { children: sideNav(true) }), sidebarFooter && (0, jsx_runtime_1.jsx)(react_2.Drawer.Footer, { className: "flex flex-col items-stretch gap-3", children: sidebarFooter })] }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex h-16 shrink-0 items-center gap-3 border-b border-border bg-surface px-4 md:px-6", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", className: "md:hidden", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Menu, { className: "size-5" }) }), typeof title === "string" ? ((0, jsx_runtime_1.jsx)("h1", { className: "truncate text-lg font-semibold text-foreground", children: title })) : (title), (0, jsx_runtime_1.jsxs)("div", { className: "ml-auto flex items-center gap-1", children: [headerActions, showAppsGrid && (0, jsx_runtime_1.jsx)(apps_grid_js_1.ConnectAppsGrid, {}), (0, jsx_runtime_1.jsx)(user_menu_js_1.ConnectUserMenu, { compact: true, theme: theme, onThemeChange: onThemeChange }), showAuthButtons && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectLoginButton, { size: "sm" }), (0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectRegisterButton, { size: "sm" })] }))] })] }), (0, jsx_runtime_1.jsx)("main", { className: "flex-1 overflow-y-auto", children: (0, jsx_runtime_1.jsxs)("div", { className: contentClassName ?? "mx-auto w-full max-w-6xl px-4 py-6 md:px-6 md:py-8", children: [gated ? (0, jsx_runtime_1.jsx)(access_gate_js_1.ConnectAccessGate, { children: children }) : children, footer] }) })] })] }));
175
+ // Two shell contexts, one set of children. The drawer's is always expanded
176
+ // and closes itself on navigation; the rail's carries the real collapse
177
+ // state. Building them here rather than inside the sidebar is what lets the
178
+ // same `railContent` render in both places with no `mobile` prop threaded
179
+ // through every part.
180
+ const railShell = (0, react_1.useMemo)(() => ({ collapsed, collapsible, setCollapsed, renderLink, activePath }), [collapsed, collapsible, setCollapsed, renderLink, activePath]);
181
+ const drawerShell = (0, react_1.useMemo)(() => ({
182
+ collapsed: false,
183
+ collapsible: false,
184
+ setCollapsed: () => { },
185
+ renderLink,
186
+ activePath,
187
+ onNavigate: closeMobile,
188
+ }), [renderLink, activePath, closeMobile]);
189
+ const accountRow = signedOut ? (showAuthButtons && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectLoginButton, { size: "sm", fullWidth: true }), (0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectRegisterButton, { size: "sm", fullWidth: true })] }))) : ((0, jsx_runtime_1.jsx)(user_menu_js_1.ConnectUserButton, { variant: "row", theme: theme, onThemeChange: onThemeChange }));
190
+ const railContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarHeader, { name: name, icon: icon, homeHref: homeHref }), workspaceSwitcher !== false && ((0, jsx_runtime_1.jsx)("div", { className: "shrink-0 px-3 pb-1", children: (0, jsx_runtime_1.jsx)(workspace_switcher_js_1.ConnectWorkspaceSwitcher, {}) })), (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarBody, { children: (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleNav }) }), (visibleSecondaryNav || sidebarFooter || showAppsGrid) && ((0, jsx_runtime_1.jsxs)(sidebar_js_1.ConnectSidebarFooter, { separated: false, children: [visibleSecondaryNav && (0, jsx_runtime_1.jsx)(side_nav_js_1.ConnectSidebarNav, { nav: visibleSecondaryNav }), sidebarFooter, showAppsGrid && (0, jsx_runtime_1.jsx)(apps_grid_js_1.ConnectAppsGrid, { variant: "row" })] })), accountRow && (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebarFooter, { children: accountRow })] }));
191
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)("div", { className: `flex h-screen bg-background ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: railShell, children: (0, jsx_runtime_1.jsx)(sidebar_js_1.ConnectSidebar, { children: railContent }) }), (0, jsx_runtime_1.jsx)(react_2.Drawer.Backdrop, { isOpen: mobileOpen, onOpenChange: setMobileOpen, children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Content, { placement: "left", className: "w-[min(20rem,85vw)]", children: (0, jsx_runtime_1.jsx)(react_2.Drawer.Dialog, { "aria-label": "Navigation menu", className: "p-0", children: (0, jsx_runtime_1.jsx)(shell_context_js_1.ConnectShellProvider, { value: drawerShell, children: (0, jsx_runtime_1.jsx)("div", { className: "flex h-full flex-col", children: railContent }) }) }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border bg-surface px-3 md:hidden", children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, variant: "tertiary", size: "sm", "aria-label": "Open navigation menu", onPress: () => setMobileOpen(true), children: (0, jsx_runtime_1.jsx)(lucide_react_1.Menu, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(logo_js_1.ConnectLogo, { name: name, icon: icon })] }), (0, jsx_runtime_1.jsx)("main", { className: "flex-1 overflow-y-auto", children: (0, jsx_runtime_1.jsxs)("div", { className: contentClassName ?? "mx-auto w-full max-w-6xl px-4 py-6 md:px-8 md:py-8", children: [gated ? (0, jsx_runtime_1.jsx)(access_gate_js_1.ConnectAccessGate, { children: children }) : children, footer] }) })] }), askAi && (0, jsx_runtime_1.jsx)(ask_ai_js_1.ConnectAskAi, { ...(askAi === true ? {} : askAi) })] }) }));
157
192
  }
@@ -7,6 +7,7 @@ const react_1 = require("@heroui/react");
7
7
  const lucide_react_1 = require("lucide-react");
8
8
  const provider_js_1 = require("../provider.js");
9
9
  const motion_js_1 = require("./motion.js");
10
+ const shell_context_js_1 = require("./shell-context.js");
10
11
  function AppTile({ app }) {
11
12
  const disabled = app.launchUrl === null;
12
13
  const body = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("span", { className: "relative flex size-12 items-center justify-center overflow-hidden rounded-xl bg-surface-secondary", children: [app.iconUrl ? (
@@ -33,13 +34,13 @@ function AppTile({ app }) {
33
34
  * Renders nothing when signed out: a launcher with nothing to launch is just a
34
35
  * button that opens an empty box.
35
36
  */
36
- function ConnectAppsGrid({ className, label = "Switch app" }) {
37
+ function ConnectAppsGrid({ className, label = "Switch app", variant = "row" }) {
37
38
  const { session, isLoading: sessionLoading } = (0, provider_js_1.useConnect)();
38
39
  const { data, isLoading, error } = (0, provider_js_1.useConnectApps)();
39
40
  if (sessionLoading || !session?.user)
40
41
  return null;
41
42
  const apps = data?.apps ?? [];
42
- return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [(0, jsx_runtime_1.jsx)(react_1.Button, { isIconOnly: true, variant: "primary", "aria-label": label, className: className, children: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { className: "sm:max-w-md", 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: "Your apps" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { children: [isLoading && ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-3 gap-2", children: Array.from({ length: 6 }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center gap-2 p-3", children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-12 rounded-xl" }), (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-3 w-14" })] }, index))) })), !isLoading && error && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-danger", children: "Could not load your apps." })), !isLoading && !error && apps.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-muted", children: "No other apps yet. Anything this workspace subscribes to shows up here." })), !isLoading && !error && apps.length > 0 && ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: "grid grid-cols-3 gap-2", variants: { show: { transition: { staggerChildren: 0.04 } } }, initial: "hidden", animate: "show", children: apps.map((app) => ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsx)(AppTile, { app: app }) }, app.applicationId))) }))] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsx)(ConnectBrowseAllLink, {}) })] }) }) })] }) }));
43
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [variant === "row" ? ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": label, className: (0, shell_context_js_1.sidebarRowClass)({ className }), children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-5 shrink-0 items-center justify-center", children: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(shell_context_js_1.SidebarLabel, { children: label })] })) : ((0, jsx_runtime_1.jsx)(react_1.Button, { isIconOnly: true, variant: "primary", "aria-label": label, className: className, children: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-5" }) })), (0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { className: "sm:max-w-md", 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: "Your apps" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { children: [isLoading && ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-3 gap-2", children: Array.from({ length: 6 }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center gap-2 p-3", children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-12 rounded-xl" }), (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-3 w-14" })] }, index))) })), !isLoading && error && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-danger", children: "Could not load your apps." })), !isLoading && !error && apps.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-muted", children: "No other apps yet. Anything this workspace subscribes to shows up here." })), !isLoading && !error && apps.length > 0 && ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: "grid grid-cols-3 gap-2", variants: { show: { transition: { staggerChildren: 0.04 } } }, initial: "hidden", animate: "show", children: apps.map((app) => ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 8 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsx)(AppTile, { app: app }) }, app.applicationId))) }))] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsx)(ConnectBrowseAllLink, {}) })] }) }) })] }) }));
43
44
  }
44
45
  function ConnectBrowseAllLink() {
45
46
  const { appUrl } = (0, provider_js_1.useConnect)();
@@ -0,0 +1,353 @@
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
+ /**
13
+ * The panel's open width. The *inner* pane always carries it, so the content
14
+ * keeps its layout while the outer shell's width animates to zero around it.
15
+ */
16
+ const PANEL_WIDTH = "w-full lg:w-[400px]";
17
+ /**
18
+ * The outer shell's width, which is the thing that animates.
19
+ *
20
+ * Two mutually exclusive branches rather than `PANEL_WIDTH` plus an
21
+ * `lg:w-0` override, because that override never applied: `lg:w-[400px]` and
22
+ * `lg:w-0` are the same utility at the same breakpoint, so the winner is
23
+ * whichever Tailwind emits later - and it emits arbitrary values after scale
24
+ * values. The closed panel therefore measured a full 400px at `lg` and up and
25
+ * sat there permanently, eating that much of the content pane on every page
26
+ * with `askAi` on. Only the `translate-x` hid it below `lg`, which is why this
27
+ * looked correct on a phone and wrong on a laptop.
28
+ */
29
+ function panelShellWidth(isOpen) {
30
+ return isOpen ? PANEL_WIDTH : "w-full lg:w-0 lg:overflow-hidden lg:border-s-0";
31
+ }
32
+ /**
33
+ * `blurIn` over the default `fadeIn`: Gemini arrives in bursts rather than an
34
+ * even token drip, and a blur masks a batch landing at once where a plain
35
+ * opacity fade just flickers. Hoisted to module scope because Streamdown
36
+ * compares `animated` by reference.
37
+ */
38
+ const ANIMATED = {
39
+ animation: "blurIn",
40
+ duration: 220,
41
+ easing: "ease-out",
42
+ sep: "word",
43
+ };
44
+ /**
45
+ * The "working on it" state - a highlight sweeping through the word rather
46
+ * than a spinner beside it. `connect-ai-shimmer` is defined in the SDK's
47
+ * `theme.css`, which every application platform already imports.
48
+ */
49
+ function ThinkingShimmer({ label = "Thinking" }) {
50
+ return ((0, jsx_runtime_1.jsx)("span", { className: "connect-ai-shimmer text-xs font-medium", role: "status", children: label }));
51
+ }
52
+ /** Open while it streams, collapsed once the answer lands. */
53
+ function ThinkingDisclosure({ text, isStreaming }) {
54
+ if (!text)
55
+ return null;
56
+ 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 })] }));
57
+ }
58
+ /**
59
+ * Structural guards, one per widget.
60
+ *
61
+ * The published package carries no schema validator, and should not gain one
62
+ * for this - so each tool input is checked for the fields its renderer would
63
+ * otherwise crash on. Anything that fails renders nothing: the model's prose
64
+ * sits around it either way, so a missing widget reads as terse rather than
65
+ * broken, which is the better failure.
66
+ */
67
+ const isArray = (value) => Array.isArray(value) && value.length > 0;
68
+ const field = (input, key) => typeof input === "object" && input !== null ? input[key] : undefined;
69
+ function renderTool(name, input, renderMarkdown) {
70
+ switch (name) {
71
+ case "showTable":
72
+ return isArray(field(input, "columns")) && Array.isArray(field(input, "rows")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiTable, { ...input })) : null;
73
+ case "showChart":
74
+ return isArray(field(input, "series")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiChart, { ...input }) : null;
75
+ case "showMetrics":
76
+ return isArray(field(input, "metrics")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiMetrics, { ...input }) : null;
77
+ case "showTimeline":
78
+ return isArray(field(input, "events")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiTimeline, { ...input }) : null;
79
+ case "showDetails":
80
+ return isArray(field(input, "fields")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiDetails, { ...input }) : null;
81
+ case "showSteps":
82
+ return isArray(field(input, "steps")) ? ((0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSteps, { ...input, renderDetail: renderMarkdown })) : null;
83
+ case "showSources":
84
+ return isArray(field(input, "sources")) ? (0, jsx_runtime_1.jsx)(ai_widgets_js_1.AiSources, { ...input }) : null;
85
+ default:
86
+ return null;
87
+ }
88
+ }
89
+ /** Placeholder while a tool's arguments are still arriving, sized like the real thing. */
90
+ function ToolSkeleton() {
91
+ 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 }) }));
92
+ }
93
+ /**
94
+ * One server-sent event stream, folded into the turn being built.
95
+ *
96
+ * Hand-rolled rather than pulled from `@ai-sdk/react`, and that is a deliberate
97
+ * trade: this package ships to partners, and `ai` + `@ai-sdk/react` is a large
98
+ * dependency to add to every application platform for one panel. The wire format
99
+ * is small, both ends of it are ours, and the chunk kinds below are all this
100
+ * panel can render.
101
+ *
102
+ * Tool arguments are read from `tool-input-available` - the complete object -
103
+ * rather than reassembled from `tool-input-delta` fragments. `tool-input-start`
104
+ * is enough to put a skeleton on screen in the meantime, and partial-JSON
105
+ * parsing buys nothing a placeholder does not.
106
+ *
107
+ * Chunks are newline-delimited `data:` frames; a frame can split across network
108
+ * reads, so the tail of each read is carried forward rather than parsed.
109
+ */
110
+ async function readStream(response, onChunk) {
111
+ const body = response.body;
112
+ if (!body)
113
+ throw new Error("Ask AI returned no stream");
114
+ const reader = body.pipeThrough(new TextDecoderStream()).getReader();
115
+ let buffer = "";
116
+ for (;;) {
117
+ const { done, value } = await reader.read();
118
+ if (done)
119
+ break;
120
+ buffer += value;
121
+ // Everything up to the last newline is whole; whatever follows it is a
122
+ // partial frame and waits for the next read.
123
+ const lines = buffer.split("\n");
124
+ buffer = lines.pop() ?? "";
125
+ for (const line of lines) {
126
+ if (!line.startsWith("data:"))
127
+ continue;
128
+ const payload = line.slice(5).trim();
129
+ if (!payload || payload === "[DONE]")
130
+ continue;
131
+ let chunk;
132
+ try {
133
+ chunk = JSON.parse(payload);
134
+ }
135
+ catch {
136
+ // A frame we cannot parse is a frame we cannot render. Dropping it
137
+ // keeps the rest of the answer flowing, which is strictly better than
138
+ // failing the whole turn over one malformed chunk.
139
+ continue;
140
+ }
141
+ switch (chunk.type) {
142
+ case "text-delta":
143
+ if (chunk.delta)
144
+ onChunk({ text: chunk.delta });
145
+ break;
146
+ case "reasoning-delta":
147
+ if (chunk.delta)
148
+ onChunk({ reasoning: chunk.delta });
149
+ break;
150
+ case "tool-input-start":
151
+ if (chunk.toolCallId && chunk.toolName) {
152
+ onChunk({ toolStart: { id: chunk.toolCallId, name: chunk.toolName } });
153
+ }
154
+ break;
155
+ case "tool-input-available":
156
+ if (chunk.toolCallId)
157
+ onChunk({ toolInput: { id: chunk.toolCallId, input: chunk.input } });
158
+ break;
159
+ case "error":
160
+ throw new Error(chunk.errorText || "Ask AI failed");
161
+ }
162
+ }
163
+ }
164
+ }
165
+ /**
166
+ * Index of the final text block, or `-1`. A reverse loop rather than
167
+ * `findLastIndex`, which is ES2023 - this package targets ES2022 so its
168
+ * published output stays usable on the runtimes its `engines` field claims.
169
+ */
170
+ function lastTextBlockIndex(blocks) {
171
+ for (let index = blocks.length - 1; index >= 0; index--) {
172
+ if (blocks[index]?.kind === "text")
173
+ return index;
174
+ }
175
+ return -1;
176
+ }
177
+ /** Appends a text delta to the open text block, or opens one after a widget. */
178
+ function appendText(blocks, text) {
179
+ const last = blocks.at(-1);
180
+ if (last?.kind === "text") {
181
+ return [...blocks.slice(0, -1), { kind: "text", text: last.text + text }];
182
+ }
183
+ return [...blocks, { kind: "text", text }];
184
+ }
185
+ /**
186
+ * Ask AI - a chat panel grounded in the signed-in workspace's Context Index,
187
+ * streamed through your own bridge.
188
+ *
189
+ * Answers are not only prose. Connect's model can call a small set of
190
+ * presentation tools - `showTable`, `showChart`, `showMetrics`, `showTimeline`,
191
+ * `showDetails`, `showSteps`, `showSources` - and this panel renders each one
192
+ * natively: a sortable table with a CSV download, a chart, a row of metric
193
+ * tiles. Values arrive raw with a format tag, so they are formatted here rather
194
+ * than by the model, which is what lets a column sort numerically.
195
+ *
196
+ * Nothing here holds a credential: it POSTs to `${basePath}/ai/chat` on your
197
+ * own origin, and `createConnectHandler` is what reaches Connect. The workspace
198
+ * is resolved from the caller's session there, never sent from the browser - so
199
+ * a user can only ever ask about the workspace they are signed into.
200
+ *
201
+ * ```tsx
202
+ * <AppShell name="Dispatch" nav={nav}>
203
+ * {children}
204
+ * </AppShell>
205
+ * ```
206
+ *
207
+ * `AppShell` mounts it by default. Mount it directly only when you are not
208
+ * using the shell: it renders a fixed corner trigger plus a panel that is an
209
+ * *in-flow flex sibling* at `lg` and up, so opening it pushes your content
210
+ * aside rather than covering it. That requires it to be the last child of a
211
+ * `flex` row owning the viewport height - which is exactly what `AppShell` is.
212
+ * Below `lg` it is a full-screen sheet with a backdrop instead; there is no
213
+ * room to push anything on a phone.
214
+ *
215
+ * **Host setup:** markdown rendering needs `streamdown` installed, one Tailwind
216
+ * `@source "…/node_modules/streamdown/dist/*.js"` line so its utility classes
217
+ * are generated, and `import "streamdown/styles.css"` for the streaming
218
+ * animation. The colour tokens Streamdown expects are already mapped in this
219
+ * SDK's `theme.css`.
220
+ *
221
+ * Answers require the workspace's Context Index to be turned on (a workspace
222
+ * admin toggle in Connect) and your API key to carry the `ai:chat` scope.
223
+ * Neither is something the browser can discover, so both surface as a message
224
+ * in the panel rather than as a hidden button.
225
+ */
226
+ 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, }) {
227
+ const { session, isLoading, streamResource } = (0, provider_js_1.useConnect)();
228
+ const [isOpen, setIsOpen] = (0, react_1.useState)(defaultOpen);
229
+ const [turns, setTurns] = (0, react_1.useState)([]);
230
+ const [input, setInput] = (0, react_1.useState)("");
231
+ const [isBusy, setIsBusy] = (0, react_1.useState)(false);
232
+ const [error, setError] = (0, react_1.useState)(null);
233
+ const abortRef = (0, react_1.useRef)(null);
234
+ const bottomRef = (0, react_1.useRef)(null);
235
+ // Ids only have to be unique within one mounted conversation, so a counter is
236
+ // enough - and unlike `crypto.randomUUID()` it cannot differ between the
237
+ // server's render and the browser's.
238
+ const nextId = (0, react_1.useRef)(0);
239
+ (0, react_1.useEffect)(() => {
240
+ bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
241
+ }, [turns]);
242
+ // A conversation left mid-stream should stop costing tokens the moment the
243
+ // panel goes away.
244
+ (0, react_1.useEffect)(() => () => abortRef.current?.abort(), []);
245
+ 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,
246
+ // Retrieved documents cite real URLs; an interstitial on every one of
247
+ // them turns a citation into a chore. The renderer still sanitises the
248
+ // markup itself, which is the part that matters.
249
+ linkSafety: LINK_SAFETY, children: text })), [markdownPlugins]);
250
+ const stop = (0, react_1.useCallback)(() => {
251
+ abortRef.current?.abort();
252
+ abortRef.current = null;
253
+ setIsBusy(false);
254
+ }, []);
255
+ const send = (0, react_1.useCallback)(async (text) => {
256
+ const controller = new AbortController();
257
+ abortRef.current = controller;
258
+ setError(null);
259
+ setIsBusy(true);
260
+ const userTurn = {
261
+ id: `u${nextId.current++}`,
262
+ role: "user",
263
+ blocks: [{ kind: "text", text }],
264
+ reasoning: "",
265
+ };
266
+ const answerId = `a${nextId.current++}`;
267
+ // The wire format Connect reads - part-based `UIMessage`s, not flat
268
+ // strings. Built from the turns *plus* this one rather than from state,
269
+ // which has not been committed yet at this point. Only text is sent back:
270
+ // a widget is a rendering of something the model already said.
271
+ const history = [...turns, userTurn].map((turn) => ({
272
+ id: turn.id,
273
+ role: turn.role,
274
+ parts: [
275
+ {
276
+ type: "text",
277
+ text: turn.blocks
278
+ .filter((block) => block.kind === "text")
279
+ .map((block) => block.text)
280
+ .join(""),
281
+ },
282
+ ],
283
+ }));
284
+ setTurns((current) => [...current, userTurn]);
285
+ const patchAnswer = (patch) => setTurns((current) => current.map((turn) => (turn.id === answerId ? patch(turn) : turn)));
286
+ try {
287
+ // Relative to the provider's own `basePath` - `streamResource` prefixes
288
+ // it, the same way every other bridge call is addressed.
289
+ const response = await streamResource("/ai/chat", { messages: history }, { signal: controller.signal });
290
+ // The assistant's turn is appended only once the stream is live, so a
291
+ // failed request leaves an error under the question rather than an
292
+ // empty bubble that never fills.
293
+ setTurns((current) => [...current, { id: answerId, role: "assistant", blocks: [], reasoning: "" }]);
294
+ await readStream(response, (chunk) => {
295
+ if (chunk.text) {
296
+ patchAnswer((turn) => ({ ...turn, blocks: appendText(turn.blocks, chunk.text) }));
297
+ }
298
+ if (chunk.reasoning) {
299
+ patchAnswer((turn) => ({ ...turn, reasoning: turn.reasoning + chunk.reasoning }));
300
+ }
301
+ if (chunk.toolStart) {
302
+ const { id, name } = chunk.toolStart;
303
+ patchAnswer((turn) => ({ ...turn, blocks: [...turn.blocks, { kind: "tool", id, name, input: null }] }));
304
+ }
305
+ if (chunk.toolInput) {
306
+ const { id, input: toolInput } = chunk.toolInput;
307
+ patchAnswer((turn) => ({
308
+ ...turn,
309
+ blocks: turn.blocks.map((block) => block.kind === "tool" && block.id === id ? { ...block, input: toolInput } : block),
310
+ }));
311
+ }
312
+ });
313
+ }
314
+ catch (cause) {
315
+ // Pressing Stop aborts the fetch, which lands here - a decision, not a
316
+ // failure, and nothing to report.
317
+ if (!controller.signal.aborted) {
318
+ setError(cause instanceof Error ? cause.message : "Ask AI failed");
319
+ }
320
+ }
321
+ finally {
322
+ if (abortRef.current === controller)
323
+ abortRef.current = null;
324
+ setIsBusy(false);
325
+ }
326
+ }, [streamResource, turns]);
327
+ const lastTurn = turns.at(-1);
328
+ const isAnswering = isBusy && lastTurn?.role === "assistant";
329
+ const rendered = (0, react_1.useMemo)(() => turns.map((turn) => {
330
+ const isLive = isAnswering && turn.id === lastTurn?.id;
331
+ const lastTextIndex = lastTextBlockIndex(turn.blocks);
332
+ 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" ? (
333
+ // The user's own turn is shown as they typed it - running a
334
+ // question that happens to start with `#` through the markdown
335
+ // renderer would turn it into a heading.
336
+ (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));
337
+ }), [turns, isAnswering, lastTurn?.id, assistantName, renderMarkdown]);
338
+ // Signed out there is no workspace to ground an answer in, and the bridge
339
+ // would answer 401 - so there is nothing to offer, not even a button.
340
+ if (isLoading || !session?.user)
341
+ return null;
342
+ const handleSubmit = (event) => {
343
+ event.preventDefault();
344
+ const text = input.trim();
345
+ if (!text || isBusy)
346
+ return;
347
+ setInput("");
348
+ void send(text);
349
+ };
350
+ 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 ${panelShellWidth(isOpen)} ${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]`, 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" }) }))] })] }) })] }));
351
+ }
352
+ /** Hoisted for the same reference-equality reason as `ANIMATED` - Streamdown compares it by identity. */
353
+ const LINK_SAFETY = { enabled: false };