@object-ui/app-shell 6.0.1 → 6.0.3

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 6.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 58f0af6: Fix marketplace install dialog showing "No environments found" even when the
8
+ signed-in user has cloud environments. Cloud's data API returns rows under
9
+ `records`, not `data`/`items`; the dialog now reads the correct key. As a
10
+ hardening pass, also filter `sys_member` rows by the caller's session
11
+ `user_id` so a leaky data endpoint cannot widen the install target list to
12
+ other tenants' organizations.
13
+ - @object-ui/types@6.0.3
14
+ - @object-ui/core@6.0.3
15
+ - @object-ui/i18n@6.0.3
16
+ - @object-ui/react@6.0.3
17
+ - @object-ui/components@6.0.3
18
+ - @object-ui/fields@6.0.3
19
+ - @object-ui/layout@6.0.3
20
+ - @object-ui/data-objectstack@6.0.3
21
+ - @object-ui/auth@6.0.3
22
+ - @object-ui/permissions@6.0.3
23
+ - @object-ui/collaboration@6.0.3
24
+ - @object-ui/providers@6.0.3
25
+
26
+ ## 6.0.2
27
+
28
+ ### Patch Changes
29
+
30
+ - d0e63f1: Migrate AI chat history from localStorage to the server-backed
31
+ `ai_conversations` / `ai_messages` REST API. The studio `AiChatPanel`,
32
+ the console `ConsoleFloatingChatbot`, and any other consumer of the new
33
+ `useChatConversation` hook (in `@object-ui/app-shell`) now resolve a
34
+ durable conversation id per signed-in user, hydrate prior messages on
35
+ mount, and rotate the conversation on reset. The previous
36
+ `objectstack:ai-chat-messages` localStorage entries are no longer read
37
+ or written.
38
+ - @object-ui/types@6.0.2
39
+ - @object-ui/core@6.0.2
40
+ - @object-ui/i18n@6.0.2
41
+ - @object-ui/react@6.0.2
42
+ - @object-ui/components@6.0.2
43
+ - @object-ui/fields@6.0.2
44
+ - @object-ui/layout@6.0.2
45
+ - @object-ui/data-objectstack@6.0.2
46
+ - @object-ui/auth@6.0.2
47
+ - @object-ui/permissions@6.0.2
48
+ - @object-ui/collaboration@6.0.2
49
+ - @object-ui/providers@6.0.2
50
+
3
51
  ## 6.0.1
4
52
 
5
53
  ### Patch Changes
@@ -237,7 +237,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
237
237
  const expressionUser = user
238
238
  ? { name: user.name, email: user.email, role: user.role ?? 'user' }
239
239
  : { name: 'Anonymous', email: '', role: 'guest' };
240
- return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange, dataSource: dataSource }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsx(RouteFader, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: resolveLandingRoute(activeApp), replace: true }) }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), _jsx(Route, { path: "system/marketplace", element: _jsx(MarketplacePage, {}) }), _jsx(Route, { path: "system/marketplace/installed", element: _jsx(MarketplaceInstalledPage, {}) }), _jsx(Route, { path: "system/marketplace/:packageId", element: _jsx(MarketplacePackagePage, {}) }), extraRoutes, _jsx(Route, { path: ":objectName/:maybeRecordId", element: _jsx(ShorthandRecordRedirect, {}) }), _jsx(Route, { path: "*", element: _jsx(RouteNotFound, {}) })] }) }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
240
+ return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, userId: user?.id, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange, dataSource: dataSource }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsx(RouteFader, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: resolveLandingRoute(activeApp), replace: true }) }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), _jsx(Route, { path: "system/marketplace", element: _jsx(MarketplacePage, {}) }), _jsx(Route, { path: "system/marketplace/installed", element: _jsx(MarketplaceInstalledPage, {}) }), _jsx(Route, { path: "system/marketplace/:packageId", element: _jsx(MarketplacePackagePage, {}) }), extraRoutes, _jsx(Route, { path: ":objectName/:maybeRecordId", element: _jsx(ShorthandRecordRedirect, {}) }), _jsx(Route, { path: "*", element: _jsx(RouteNotFound, {}) })] }) }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
241
241
  type: 'object-form',
242
242
  formType: 'modal',
243
243
  objectName: currentObjectDef.name,
@@ -10,6 +10,11 @@
10
10
  import React from 'react';
11
11
  interface HomeLayoutProps {
12
12
  children: React.ReactNode;
13
+ /**
14
+ * Signed-in user id. Forwarded to the floating chatbot so it can hydrate
15
+ * server-backed conversation history.
16
+ */
17
+ userId?: string;
13
18
  }
14
- export declare function HomeLayout({ children }: HomeLayoutProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function HomeLayout({ children, userId }: HomeLayoutProps): import("react/jsx-runtime").JSX.Element;
15
20
  export {};
@@ -15,7 +15,7 @@ import { useDiscovery } from '@object-ui/react';
15
15
  // Lightweight FAB stub — the heavy chat chunk graph only downloads on
16
16
  // first hover/click. See ../../layout/ConsoleChatbotFab.tsx.
17
17
  import { ConsoleChatbotFab } from '../../layout/ConsoleChatbotFab';
18
- export function HomeLayout({ children }) {
18
+ export function HomeLayout({ children, userId }) {
19
19
  const { setContext } = useNavigationContext();
20
20
  const { isAiEnabled } = useDiscovery();
21
21
  // Render the chatbot whenever AI is reachable. If the developer has explicitly
@@ -26,5 +26,5 @@ export function HomeLayout({ children }) {
26
26
  useEffect(() => {
27
27
  setContext('home');
28
28
  }, [setContext]);
29
- return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "home-layout", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "home" }) }), _jsx("main", { className: "flex-1 min-w-0 overflow-auto pb-20 sm:pb-0", children: children }), showChatbot && _jsx(ConsoleChatbotFab, { appLabel: "Workspace", objects: [] })] }));
29
+ return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "home-layout", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "home" }) }), _jsx("main", { className: "flex-1 min-w-0 overflow-auto pb-20 sm:pb-0", children: children }), showChatbot && _jsx(ConsoleChatbotFab, { appLabel: "Workspace", objects: [], userId: userId })] }));
30
30
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * Marketplace Package Detail Page.
4
4
  *
@@ -7,8 +7,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
7
7
  */
8
8
  import { useEffect, useState } from 'react';
9
9
  import { useNavigate, useParams } from 'react-router-dom';
10
- import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Skeleton, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Label, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
11
- import { ArrowLeft, ExternalLink, Download, AlertCircle, Package, Trash2 } from 'lucide-react';
10
+ import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Skeleton, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Label, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from '@object-ui/components';
11
+ import { ArrowLeft, ExternalLink, Download, AlertCircle, Package, Trash2, MoreHorizontal, CheckCircle2 } from 'lucide-react';
12
12
  import { PackageIcon } from './PackageIcon';
13
13
  import { MarkdownText } from './MarkdownText';
14
14
  import { getMarketplacePackage, installPackage, installLocal, uninstallLocal, listLocalInstalls, listCloudEnvironments, listInstallableOrgIds, cloudInstallDeepLink, } from './marketplaceApi';
@@ -186,18 +186,29 @@ export function MarketplacePackagePage() {
186
186
  }
187
187
  };
188
188
  if (loading) {
189
- return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsx(Skeleton, { className: "h-8 w-32" }), _jsx(Skeleton, { className: "h-16 w-full" }), _jsx(Skeleton, { className: "h-64 w-full" })] }));
189
+ return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsx(Skeleton, { className: "h-8 w-32" }), _jsx(Skeleton, { className: "h-16 w-full" }), _jsx(Skeleton, { className: "h-64 w-full" })] }));
190
190
  }
191
191
  if (error || !data) {
192
- return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load package" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error ?? 'Not found.' })] })] })] }));
192
+ return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load package" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error ?? 'Not found.' })] })] })] }));
193
193
  }
194
194
  const pkg = data.package;
195
195
  const latestVersion = pkg.latest_version?.version ?? data.versions[0]?.version ?? null;
196
196
  const localInstall = localInstalls.find((i) => i.manifestId === pkg.manifest_id) ?? null;
197
- return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6 max-w-5xl", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-5 flex-wrap rounded-2xl border bg-gradient-to-br from-primary/5 via-background to-background p-6", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id, className: "h-20 w-20 rounded-2xl shadow-sm ring-1 ring-border", initialClassName: "text-3xl font-bold" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("h1", { className: "text-3xl font-bold tracking-tight truncate", children: pkg.display_name || pkg.manifest_id }), _jsxs("div", { className: "text-sm text-muted-foreground mt-2 flex flex-wrap items-center gap-2", children: [_jsx("code", { className: "font-mono text-xs px-1.5 py-0.5 rounded bg-muted", children: pkg.manifest_id }), latestVersion && (_jsxs(Badge, { variant: "outline", children: ["v", latestVersion] })), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', children: pkg.publisher })), pkg.category && _jsx(Badge, { variant: "outline", children: pkg.category }), pkg.license && _jsx(Badge, { variant: "outline", className: "font-normal", children: pkg.license }), localInstall && (_jsxs(Badge, { variant: "default", className: "bg-green-600 hover:bg-green-600", children: ["Installed \u00B7 v", localInstall.version] }))] }), pkg.description && (_jsx("p", { className: "text-base text-foreground/80 mt-4 max-w-2xl leading-relaxed", children: pkg.description }))] }), _jsxs("div", { className: "flex flex-col gap-2 shrink-0 min-w-[14rem]", children: [getRuntimeConfig().features.installLocal && (_jsxs(_Fragment, { children: [_jsxs(Button, { onClick: doInstallLocal, disabled: !latestVersion || installingLocal, size: "lg", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), installingLocal
198
- ? 'Working…'
199
- : localInstall
200
- ? `Reinstall to this runtime`
201
- : 'Install to this runtime'] }), localInstall && (_jsxs(Button, { variant: "outline", onClick: doUninstallLocal, disabled: installingLocal, children: [_jsx(Trash2, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Uninstall from this runtime"] }))] })), _jsxs(Button, { variant: "ghost", onClick: openInstall, disabled: !latestVersion, size: "sm", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Install to cloud environment\u2026"] }), pkg.homepage_url && (_jsx("a", { href: pkg.homepage_url, target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "ghost", size: "sm", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Homepage"] }) })), localResult && (_jsx("div", { className: `rounded-md border p-2 text-xs whitespace-pre-wrap ${localResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: localResult.message }))] })] }), _jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [_jsx("div", { className: "lg:col-span-2 space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "About" }) }), _jsx(CardContent, { children: pkg.readme ? (_jsx(MarkdownText, { source: pkg.readme })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No readme provided." })) })] }) }), _jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Versions" }) }), _jsx(CardContent, { children: data.versions.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No approved versions." })) : (_jsx("ul", { className: "space-y-2", children: data.versions.map((v) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Package, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("code", { className: "font-mono", children: ["v", v.version] }), v.is_prerelease && _jsx(Badge, { variant: "outline", className: "text-xs", children: "pre" })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: v.published_at ? new Date(v.published_at).toLocaleDateString() : '—' })] }, v.id))) })) })] }) })] }), _jsx(Dialog, { open: installOpen, onOpenChange: (o) => { setInstallOpen(o); if (!o)
197
+ const supportsLocal = getRuntimeConfig().features.installLocal;
198
+ const primaryDisabled = !latestVersion || installingLocal || installing;
199
+ const primaryAction = supportsLocal
200
+ ? {
201
+ label: installingLocal
202
+ ? 'Working…'
203
+ : localInstall
204
+ ? 'Reinstall'
205
+ : 'Install',
206
+ onClick: doInstallLocal,
207
+ }
208
+ : {
209
+ label: installing ? 'Installing…' : 'Install to cloud…',
210
+ onClick: openInstall,
211
+ };
212
+ return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start -ml-2 text-muted-foreground hover:text-foreground", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-5 flex-wrap sm:flex-nowrap rounded-2xl border bg-gradient-to-br from-primary/5 via-background to-background p-6 sm:p-8", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id, className: "h-20 w-20 rounded-2xl shadow-sm ring-1 ring-border shrink-0", initialClassName: "text-3xl font-bold" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("h1", { className: "text-2xl sm:text-3xl font-bold tracking-tight truncate", children: pkg.display_name || pkg.manifest_id }), pkg.homepage_url && (_jsxs("a", { href: pkg.homepage_url, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors", title: "Homepage", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), _jsx("span", { className: "hidden sm:inline", children: "Homepage" })] }))] }), _jsxs("div", { className: "text-sm text-muted-foreground mt-2 flex flex-wrap items-center gap-1.5", children: [_jsx("code", { className: "font-mono text-xs px-1.5 py-0.5 rounded bg-muted", children: pkg.manifest_id }), latestVersion && _jsxs(Badge, { variant: "outline", children: ["v", latestVersion] }), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', children: pkg.publisher })), pkg.category && _jsx(Badge, { variant: "outline", children: pkg.category }), pkg.license && _jsx(Badge, { variant: "outline", className: "font-normal", children: pkg.license }), localInstall && (_jsxs(Badge, { variant: "default", className: "bg-green-600 hover:bg-green-600 gap-1", children: [_jsx(CheckCircle2, { className: "h-3 w-3", "aria-hidden": "true" }), "Installed \u00B7 v", localInstall.version] }))] }), pkg.description && (_jsx("p", { className: "text-sm sm:text-base text-foreground/80 mt-3 max-w-2xl leading-relaxed", children: pkg.description }))] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0 self-start", children: [_jsxs(Button, { onClick: primaryAction.onClick, disabled: primaryDisabled, size: "lg", className: "min-w-[8rem]", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), primaryAction.label] }), localInstall && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "lg", className: "px-2.5", "aria-label": "More install options", children: _jsx(MoreHorizontal, { className: "h-4 w-4", "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-56", children: _jsxs(DropdownMenuItem, { onSelect: doUninstallLocal, disabled: installingLocal, className: "text-destructive focus:text-destructive", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2", "aria-hidden": "true" }), "Uninstall from this runtime"] }) })] }))] })] }), localResult && (_jsxs("div", { role: "status", className: `flex items-start gap-2 rounded-md border p-3 text-sm whitespace-pre-wrap ${localResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: [localResult.ok ? _jsx(CheckCircle2, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }) : _jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: localResult.message }), _jsx("button", { type: "button", className: "text-xs underline opacity-60 hover:opacity-100", onClick: () => setLocalResult(null), children: "Dismiss" })] })), _jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [_jsx("div", { className: "lg:col-span-2 space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "About" }) }), _jsx(CardContent, { children: pkg.readme ? (_jsx(MarkdownText, { source: pkg.readme })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No readme provided." })) })] }) }), _jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Versions" }) }), _jsx(CardContent, { children: data.versions.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No approved versions." })) : (_jsx("ul", { className: "space-y-2", children: data.versions.map((v) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Package, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("code", { className: "font-mono", children: ["v", v.version] }), v.is_prerelease && _jsx(Badge, { variant: "outline", className: "text-xs", children: "pre" })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: v.published_at ? new Date(v.published_at).toLocaleDateString() : '—' })] }, v.id))) })) })] }) })] }), _jsx(Dialog, { open: installOpen, onOpenChange: (o) => { setInstallOpen(o); if (!o)
202
213
  setInstallResult(null); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Install ", pkg.display_name || pkg.manifest_id] }), _jsx(DialogDescription, { children: "Choose an environment to install this app into. You need to be signed into ObjectStack Cloud." })] }), envsLoading ? (_jsx(Skeleton, { className: "h-10 w-full" })) : envsError ? (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm space-y-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-amber-600", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: envsError })] }), _jsx("a", { href: cloudInstallDeepLink(pkg.id), target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Open on cloud"] }) })] })) : envs.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No environments found in your active organization." })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "env-select", children: "Environment" }), _jsxs(Select, { value: selectedEnv, onValueChange: setSelectedEnv, children: [_jsx(SelectTrigger, { id: "env-select", children: _jsx(SelectValue, { placeholder: "Pick an environment" }) }), _jsx(SelectContent, { children: envs.map((e) => (_jsxs(SelectItem, { value: e.id, children: [e.display_name || e.hostname || e.id, e.plan && _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", e.plan] })] }, e.id))) })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "seed", checked: seedSampleData, onCheckedChange: (c) => setSeedSampleData(c === true) }), _jsx(Label, { htmlFor: "seed", className: "text-sm font-normal cursor-pointer", children: "Include sample data" })] })] })), installResult && (_jsx("div", { className: `rounded-md border p-3 text-sm ${installResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: installResult.message })), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInstallOpen(false), children: "Close" }), !envsError && (_jsx(Button, { onClick: doInstall, disabled: !selectedEnv || installing || installResult?.ok === true, children: installing ? 'Installing…' : 'Install' }))] })] }) })] }));
203
214
  }
@@ -108,6 +108,11 @@ export declare function listCloudEnvironments(): Promise<CloudEnvironment[]>;
108
108
  *
109
109
  * Returns an empty set on 401 / network failure so the install dialog
110
110
  * can render a clean "no installable environments" state.
111
+ *
112
+ * Hardening: we *always* re-filter rows by the caller's session
113
+ * `user_id` because the data API currently returns sys_member rows
114
+ * without per-caller scoping. Without this, the dialog would pick up
115
+ * every org in the system and offer their envs as install targets.
111
116
  */
112
117
  export declare function listInstallableOrgIds(): Promise<Set<string>>;
113
118
  export declare function cloudInstallDeepLink(packageId: string): string;
@@ -94,7 +94,9 @@ export async function listCloudEnvironments() {
94
94
  throw err;
95
95
  }
96
96
  const payload = await res.json().catch(() => ({}));
97
- const rows = payload?.data ?? payload?.items ?? payload ?? [];
97
+ // Cloud's data API returns `{ object, records, total, hasMore }`.
98
+ // We keep `data` / `items` as fallbacks for older builds.
99
+ const rows = payload?.records ?? payload?.data ?? payload?.items ?? payload ?? [];
98
100
  return Array.isArray(rows) ? rows : [];
99
101
  }
100
102
  /**
@@ -105,10 +107,30 @@ export async function listCloudEnvironments() {
105
107
  *
106
108
  * Returns an empty set on 401 / network failure so the install dialog
107
109
  * can render a clean "no installable environments" state.
110
+ *
111
+ * Hardening: we *always* re-filter rows by the caller's session
112
+ * `user_id` because the data API currently returns sys_member rows
113
+ * without per-caller scoping. Without this, the dialog would pick up
114
+ * every org in the system and offer their envs as install targets.
108
115
  */
109
116
  export async function listInstallableOrgIds() {
110
117
  const base = getCloudBase() || SERVER_URL;
111
- // sys_member rows are scoped to the caller; better-auth-managed table.
118
+ let meId = null;
119
+ try {
120
+ const meRes = await fetch(`${base}/api/v1/auth/get-session`, {
121
+ credentials: 'include',
122
+ headers: { 'Accept': 'application/json' },
123
+ });
124
+ if (meRes.ok) {
125
+ const meBody = await meRes.json().catch(() => ({}));
126
+ meId = meBody?.user?.id ?? null;
127
+ }
128
+ }
129
+ catch {
130
+ /* fall through — meId stays null and we return empty set */
131
+ }
132
+ if (!meId)
133
+ return new Set();
112
134
  const url = `${base}/api/v1/data/sys_member?limit=200`;
113
135
  let payload = null;
114
136
  try {
@@ -123,11 +145,14 @@ export async function listInstallableOrgIds() {
123
145
  catch {
124
146
  return new Set();
125
147
  }
126
- const rows = payload?.data ?? payload?.items ?? payload ?? [];
148
+ const rows = payload?.records ?? payload?.data ?? payload?.items ?? payload ?? [];
127
149
  if (!Array.isArray(rows))
128
150
  return new Set();
129
151
  const ids = new Set();
130
152
  for (const row of rows) {
153
+ const rowUserId = String(row?.user_id ?? row?.userId ?? '');
154
+ if (rowUserId !== meId)
155
+ continue;
131
156
  const role = String(row?.role ?? '').toLowerCase();
132
157
  const orgId = row?.organization_id ?? row?.organizationId;
133
158
  if (orgId && (role === 'owner' || role === 'admin')) {
@@ -2,18 +2,17 @@
2
2
  * Resolve the absolute URL of the console "home" route used after switching
3
3
  * the active organization.
4
4
  *
5
- * Earlier versions concatenated `import.meta.env.BASE_URL` to
6
- * `window.location.origin` ("`${origin}${base}home`"). That was correct for
7
- * apps built with an absolute Vite base (e.g. `base: '/_console/'`), but
8
- * silently broken for portable builds that ship with `base: './'` — the
9
- * resulting URL `https://host./home` has a trailing-dot host (`host.` is a
10
- * fully-qualified-domain marker the browser keeps) AND drops the mount
11
- * prefix, landing the user on a 404.
5
+ * History:
6
+ * - `${origin}${import.meta.env.BASE_URL}home` broke portable builds
7
+ * (`base: './'`), producing `https://host./home` — trailing-dot host.
8
+ * - `new URL('home', document.baseURI)` fixed that, but `document.baseURI`
9
+ * falls back to the current page URL when no `<base>` tag is present.
10
+ * From `/home/home/` that resolves to `/home/home/home`, and each
11
+ * subsequent navigation appended another `/home` segment.
12
12
  *
13
- * The fix is to resolve `home` against `document.baseURI`, which already
14
- * accounts for any `<base href="...">` injected by the host page. This works
15
- * for both `<base href="/_console/">` (tenant deployments) and
16
- * `<base href="/">` (root-mount deployments), and also for hosts that omit
17
- * `<base>` entirely (falls back to the current document URL's directory).
13
+ * The robust resolution: read `<base href>` explicitly. When present it
14
+ * carries the deployment mount (`/_console/`, `/`, or `./`); when absent
15
+ * we resolve against the document origin root, which is independent of the
16
+ * current SPA route.
18
17
  */
19
18
  export declare function resolveHomeUrl(baseURI?: string): string;
@@ -2,20 +2,26 @@
2
2
  * Resolve the absolute URL of the console "home" route used after switching
3
3
  * the active organization.
4
4
  *
5
- * Earlier versions concatenated `import.meta.env.BASE_URL` to
6
- * `window.location.origin` ("`${origin}${base}home`"). That was correct for
7
- * apps built with an absolute Vite base (e.g. `base: '/_console/'`), but
8
- * silently broken for portable builds that ship with `base: './'` — the
9
- * resulting URL `https://host./home` has a trailing-dot host (`host.` is a
10
- * fully-qualified-domain marker the browser keeps) AND drops the mount
11
- * prefix, landing the user on a 404.
5
+ * History:
6
+ * - `${origin}${import.meta.env.BASE_URL}home` broke portable builds
7
+ * (`base: './'`), producing `https://host./home` — trailing-dot host.
8
+ * - `new URL('home', document.baseURI)` fixed that, but `document.baseURI`
9
+ * falls back to the current page URL when no `<base>` tag is present.
10
+ * From `/home/home/` that resolves to `/home/home/home`, and each
11
+ * subsequent navigation appended another `/home` segment.
12
12
  *
13
- * The fix is to resolve `home` against `document.baseURI`, which already
14
- * accounts for any `<base href="...">` injected by the host page. This works
15
- * for both `<base href="/_console/">` (tenant deployments) and
16
- * `<base href="/">` (root-mount deployments), and also for hosts that omit
17
- * `<base>` entirely (falls back to the current document URL's directory).
13
+ * The robust resolution: read `<base href>` explicitly. When present it
14
+ * carries the deployment mount (`/_console/`, `/`, or `./`); when absent
15
+ * we resolve against the document origin root, which is independent of the
16
+ * current SPA route.
18
17
  */
19
- export function resolveHomeUrl(baseURI = document.baseURI) {
20
- return new URL('home', baseURI).toString();
18
+ export function resolveHomeUrl(baseURI) {
19
+ if (baseURI !== undefined) {
20
+ return new URL('home', baseURI).toString();
21
+ }
22
+ const baseHref = document.querySelector('base')?.getAttribute('href');
23
+ const root = baseHref
24
+ ? new URL(baseHref, window.location.origin)
25
+ : new URL('/', window.location.origin);
26
+ return new URL('home', root).toString();
21
27
  }
@@ -7,3 +7,4 @@ export { useRecentItems, type RecentItem } from './useRecentItems';
7
7
  export { useRecordApprovals, type ApprovalProcessLite, type ApprovalRequestLite } from './useRecordApprovals';
8
8
  export { useResponsiveSidebar } from './useResponsiveSidebar';
9
9
  export { useTrackRouteAsRecent, type UseTrackRouteAsRecentOptions } from './useTrackRouteAsRecent';
10
+ export { useChatConversation, type HydratedUIMessage, type UseChatConversationOptions, type UseChatConversationReturn, } from './useChatConversation';
@@ -7,3 +7,4 @@ export { useRecentItems } from './useRecentItems';
7
7
  export { useRecordApprovals } from './useRecordApprovals';
8
8
  export { useResponsiveSidebar } from './useResponsiveSidebar';
9
9
  export { useTrackRouteAsRecent } from './useTrackRouteAsRecent';
10
+ export { useChatConversation, } from './useChatConversation';
@@ -0,0 +1,31 @@
1
+ /** Minimal UIMessage shape compatible with `@ai-sdk/react`'s `useChat`. */
2
+ export interface HydratedUIMessage {
3
+ id: string;
4
+ role: 'user' | 'assistant' | 'system';
5
+ parts: Array<{
6
+ type: 'text';
7
+ text: string;
8
+ }>;
9
+ }
10
+ export interface UseChatConversationOptions {
11
+ /** Authenticated user id; hook is inert until this is defined. */
12
+ userId: string | undefined;
13
+ /**
14
+ * Optional scope (e.g. agent name) for keying separate conversations under
15
+ * the same user.
16
+ */
17
+ scope?: string;
18
+ /**
19
+ * Base URL of the AI service (no trailing slash). Hook calls
20
+ * `${apiBase}/conversations[/...]`. Required.
21
+ */
22
+ apiBase: string;
23
+ }
24
+ export interface UseChatConversationReturn {
25
+ conversationId: string | undefined;
26
+ initialMessages: HydratedUIMessage[];
27
+ isLoading: boolean;
28
+ /** Delete the current conversation + start a fresh one. */
29
+ reset: () => Promise<void>;
30
+ }
31
+ export declare function useChatConversation(options: UseChatConversationOptions): UseChatConversationReturn;
@@ -0,0 +1,188 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ /**
3
+ * Server-backed AI chat conversation lifecycle.
4
+ *
5
+ * Binds a chat UI to an `ai_conversations` row owned by the signed-in user.
6
+ * On mount it tries the cached id (per user, optionally per scope); falls
7
+ * back to creating a fresh conversation when the cached one is gone
8
+ * (404/403).
9
+ */
10
+ import { useCallback, useEffect, useRef, useState } from 'react';
11
+ const CACHE_PREFIX = 'objectstack:ai-chat-conversation-id';
12
+ function cacheKey(userId, scope) {
13
+ return scope ? `${CACHE_PREFIX}:${userId}:${scope}` : `${CACHE_PREFIX}:${userId}`;
14
+ }
15
+ function readCache(key) {
16
+ try {
17
+ return localStorage.getItem(key) ?? undefined;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ function writeCache(key, value) {
24
+ try {
25
+ if (value)
26
+ localStorage.setItem(key, value);
27
+ else
28
+ localStorage.removeItem(key);
29
+ }
30
+ catch {
31
+ /* ignore — private mode, quota, etc. */
32
+ }
33
+ }
34
+ function contentToText(content) {
35
+ if (typeof content === 'string')
36
+ return content;
37
+ if (Array.isArray(content)) {
38
+ return content
39
+ .map((part) => {
40
+ if (typeof part === 'string')
41
+ return part;
42
+ if (part &&
43
+ typeof part === 'object' &&
44
+ 'text' in part &&
45
+ typeof part.text === 'string') {
46
+ return part.text;
47
+ }
48
+ return '';
49
+ })
50
+ .join('');
51
+ }
52
+ return '';
53
+ }
54
+ function toUIMessages(rows) {
55
+ if (!rows)
56
+ return [];
57
+ const out = [];
58
+ rows.forEach((row, idx) => {
59
+ const role = row.role;
60
+ if (role !== 'user' && role !== 'assistant' && role !== 'system')
61
+ return;
62
+ const text = contentToText(row.content);
63
+ if (!text)
64
+ return;
65
+ out.push({
66
+ id: row.id ?? `msg-${idx}`,
67
+ role,
68
+ parts: [{ type: 'text', text }],
69
+ });
70
+ });
71
+ return out;
72
+ }
73
+ async function fetchConversation(apiBase, id) {
74
+ const res = await fetch(`${apiBase}/conversations/${encodeURIComponent(id)}`, {
75
+ credentials: 'include',
76
+ });
77
+ if (res.status === 404 || res.status === 403)
78
+ return null;
79
+ if (!res.ok)
80
+ throw new Error(`GET conversation failed: ${res.status}`);
81
+ return (await res.json());
82
+ }
83
+ async function createConversation(apiBase) {
84
+ const res = await fetch(`${apiBase}/conversations`, {
85
+ method: 'POST',
86
+ credentials: 'include',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: '{}',
89
+ });
90
+ if (!res.ok)
91
+ throw new Error(`POST conversation failed: ${res.status}`);
92
+ return (await res.json());
93
+ }
94
+ async function deleteConversation(apiBase, id) {
95
+ await fetch(`${apiBase}/conversations/${encodeURIComponent(id)}`, {
96
+ method: 'DELETE',
97
+ credentials: 'include',
98
+ }).catch(() => {
99
+ /* best-effort */
100
+ });
101
+ }
102
+ export function useChatConversation(options) {
103
+ const { userId, scope, apiBase } = options;
104
+ const [conversationId, setConversationId] = useState(undefined);
105
+ const [initialMessages, setInitialMessages] = useState([]);
106
+ const [isLoading, setIsLoading] = useState(Boolean(userId));
107
+ const mountedRef = useRef(true);
108
+ useEffect(() => {
109
+ mountedRef.current = true;
110
+ return () => {
111
+ mountedRef.current = false;
112
+ };
113
+ }, []);
114
+ useEffect(() => {
115
+ if (!userId) {
116
+ setConversationId(undefined);
117
+ setInitialMessages([]);
118
+ setIsLoading(false);
119
+ return;
120
+ }
121
+ let cancelled = false;
122
+ const key = cacheKey(userId, scope);
123
+ setIsLoading(true);
124
+ (async () => {
125
+ try {
126
+ const cached = readCache(key);
127
+ if (cached) {
128
+ const existing = await fetchConversation(apiBase, cached);
129
+ if (cancelled)
130
+ return;
131
+ if (existing) {
132
+ setConversationId(existing.id);
133
+ setInitialMessages(toUIMessages(existing.messages));
134
+ return;
135
+ }
136
+ writeCache(key, undefined);
137
+ }
138
+ const fresh = await createConversation(apiBase);
139
+ if (cancelled)
140
+ return;
141
+ writeCache(key, fresh.id);
142
+ setConversationId(fresh.id);
143
+ setInitialMessages(toUIMessages(fresh.messages));
144
+ }
145
+ catch {
146
+ if (!cancelled) {
147
+ setConversationId(undefined);
148
+ setInitialMessages([]);
149
+ }
150
+ }
151
+ finally {
152
+ if (!cancelled)
153
+ setIsLoading(false);
154
+ }
155
+ })();
156
+ return () => {
157
+ cancelled = true;
158
+ };
159
+ }, [userId, scope, apiBase]);
160
+ const reset = useCallback(async () => {
161
+ if (!userId)
162
+ return;
163
+ const key = cacheKey(userId, scope);
164
+ setIsLoading(true);
165
+ try {
166
+ if (conversationId)
167
+ await deleteConversation(apiBase, conversationId);
168
+ writeCache(key, undefined);
169
+ const fresh = await createConversation(apiBase);
170
+ writeCache(key, fresh.id);
171
+ if (!mountedRef.current)
172
+ return;
173
+ setConversationId(fresh.id);
174
+ setInitialMessages([]);
175
+ }
176
+ catch {
177
+ if (mountedRef.current) {
178
+ setConversationId(undefined);
179
+ setInitialMessages([]);
180
+ }
181
+ }
182
+ finally {
183
+ if (mountedRef.current)
184
+ setIsLoading(false);
185
+ }
186
+ }, [conversationId, userId, scope, apiBase]);
187
+ return { conversationId, initialMessages, isLoading, reset };
188
+ }
@@ -14,6 +14,12 @@ export interface ConsoleFloatingChatbotProps {
14
14
  defaultAgent?: string;
15
15
  /** Whether the floating panel should open immediately on mount. */
16
16
  defaultOpen?: boolean;
17
+ /**
18
+ * Authenticated user id. When provided, the chat hydrates from (and writes
19
+ * to) a server-backed `ai_conversations` row keyed by `userId` + agent.
20
+ * Inert until defined — the floating panel still works in local-only mode.
21
+ */
22
+ userId?: string;
17
23
  }
18
- export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen, }: ConsoleFloatingChatbotProps): import("react/jsx-runtime").JSX.Element;
24
+ export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen, userId, }: ConsoleFloatingChatbotProps): import("react/jsx-runtime").JSX.Element;
19
25
  export {};
@@ -14,6 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  import React from 'react';
15
15
  import { FloatingChatbot, useObjectChat, useAgents, useHitlInChat, } from '@object-ui/plugin-chatbot';
16
16
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
17
+ import { useChatConversation } from '../hooks';
17
18
  const DEFAULT_AI_PATH = '/api/v1/ai';
18
19
  function resolveApiBase(explicit) {
19
20
  if (explicit)
@@ -25,7 +26,7 @@ function resolveApiBase(explicit) {
25
26
  const serverUrl = env.VITE_SERVER_URL ?? '';
26
27
  return `${serverUrl.replace(/\/$/, '')}${DEFAULT_AI_PATH}`;
27
28
  }
28
- function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, activeAgent, onAgentChange, chatApi, apiBase, defaultOpen = false, }) {
29
+ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, activeAgent, onAgentChange, chatApi, apiBase, defaultOpen = false, conversationId, }) {
29
30
  const objectNames = objects.map((o) => o.label || o.name).join(', ');
30
31
  const activeAgentLabel = React.useMemo(() => {
31
32
  const found = agents.find((a) => a.name === activeAgent);
@@ -36,7 +37,7 @@ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, a
36
37
  : `Hello! I'm your **${appLabel}** assistant. ${agentsError ? '(Backend unreachable — running in offline demo mode.)' : ''}`;
37
38
  const { messages, isLoading, error, sendMessage, stop, reload, clear, } = useObjectChat({
38
39
  api: chatApi,
39
- conversationId: activeAgent ? `${appLabel}:${activeAgent}` : undefined,
40
+ conversationId,
40
41
  body: {
41
42
  context: {
42
43
  activeApp: appLabel,
@@ -85,7 +86,7 @@ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, a
85
86
  ? 'Loading agents...'
86
87
  : 'Ask anything...', onSendMessage: (content) => sendMessage(content), onClear: clear, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: error, enableMarkdown: true, onToolApprove: hitl.decide, toolDecisions: hitl.decisions, toolApproveLabel: "Approve & run", toolDenyLabel: "Reject", toolDenyReason: "Operator rejected from chat" }));
87
88
  }
88
- export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen = false, }) {
89
+ export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen = false, userId, }) {
89
90
  const apiBase = React.useMemo(() => resolveApiBase(apiBaseProp), [apiBaseProp]);
90
91
  const env = import.meta.env ?? {};
91
92
  const envDefaultAgent = env.VITE_AI_DEFAULT_AGENT;
@@ -101,8 +102,16 @@ export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: api
101
102
  const chatApi = activeAgent
102
103
  ? `${apiBase}/agents/${encodeURIComponent(activeAgent)}/chat`
103
104
  : undefined;
104
- // `key` forces a clean remount whenever the active agent (and therefore the
105
- // chat API URL) changes required because `useObjectChat` locks its mode
106
- // (api vs local) on first render.
107
- return (_jsx(ChatbotInner, { appLabel: appLabel, objects: objects, agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, onAgentChange: setActiveAgent, chatApi: chatApi, apiBase: apiBase, defaultOpen: defaultOpen }, chatApi ?? 'local'));
105
+ // Server-backed conversation. Scoped by agent so each agent gets its own
106
+ // persistent history. Hook is inert until `userId` is provided; without it
107
+ // the FAB continues to work in local-only mode (no persistence).
108
+ const { conversationId } = useChatConversation({
109
+ userId,
110
+ scope: activeAgent,
111
+ apiBase,
112
+ });
113
+ // `key` forces a clean remount whenever the chat endpoint OR the resolved
114
+ // conversation id changes — required because `useObjectChat` locks its mode
115
+ // (api vs local) and its `conversationId` on first render.
116
+ return (_jsx(ChatbotInner, { appLabel: appLabel, objects: objects, agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, onAgentChange: setActiveAgent, chatApi: chatApi, apiBase: apiBase, defaultOpen: defaultOpen, conversationId: conversationId }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`));
108
117
  }
@@ -16,7 +16,12 @@ interface ConsoleLayoutProps {
16
16
  onAppChange: (name: string) => void;
17
17
  objects: any[];
18
18
  connectionState?: ConnectionState;
19
+ /**
20
+ * Signed-in user id. Forwarded to the floating chatbot so it can hydrate
21
+ * server-backed conversation history. Omit for unauthenticated/local-only.
22
+ */
23
+ userId?: string;
19
24
  }
20
25
  /** Floating chatbot wired with useObjectChat for demo auto-response */
21
- export declare function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState }: ConsoleLayoutProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState, userId, }: ConsoleLayoutProps): import("react/jsx-runtime").JSX.Element;
22
27
  export {};
@@ -28,7 +28,7 @@ function ConsoleLayoutInner({ children }) {
28
28
  }
29
29
  /** Floating chatbot wired with useObjectChat for demo auto-response */
30
30
  // (moved to ./ConsoleFloatingChatbot.tsx for code-splitting)
31
- export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState }) {
31
+ export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState, userId, }) {
32
32
  const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
33
33
  const { isAiEnabled } = useDiscovery();
34
34
  // Trust an explicit `VITE_AI_BASE_URL` opt-in even when discovery reports
@@ -51,5 +51,5 @@ export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange,
51
51
  ? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
52
52
  : undefined,
53
53
  }
54
- : undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(ConsoleChatbotFab, { appLabel: appLabel, objects: objects }))] }) }));
54
+ : undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(ConsoleChatbotFab, { appLabel: appLabel, objects: objects, userId: userId }))] }) }));
55
55
  }
@@ -386,7 +386,8 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
386
386
  const shouldRefresh = action.refreshAfter !== false;
387
387
  if (shouldRefresh)
388
388
  setActionRefreshKey(k => k + 1);
389
- return { success: true, data: json?.data, reload: shouldRefresh };
389
+ const result = json?.data;
390
+ return { success: true, data: result, reload: shouldRefresh };
390
391
  }
391
392
  catch (error) {
392
393
  return { success: false, error: error.message };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "6.0.1",
3
+ "version": "6.0.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
@@ -28,35 +28,35 @@
28
28
  "@sentry/react": "^8.55.2",
29
29
  "lucide-react": "^1.16.0",
30
30
  "sonner": "^2.0.7",
31
- "@object-ui/auth": "6.0.1",
32
- "@object-ui/collaboration": "6.0.1",
33
- "@object-ui/components": "6.0.1",
34
- "@object-ui/core": "6.0.1",
35
- "@object-ui/data-objectstack": "6.0.1",
36
- "@object-ui/fields": "6.0.1",
37
- "@object-ui/i18n": "6.0.1",
38
- "@object-ui/layout": "6.0.1",
39
- "@object-ui/permissions": "6.0.1",
40
- "@object-ui/providers": "6.0.1",
41
- "@object-ui/react": "6.0.1",
42
- "@object-ui/types": "6.0.1"
31
+ "@object-ui/auth": "6.0.3",
32
+ "@object-ui/collaboration": "6.0.3",
33
+ "@object-ui/components": "6.0.3",
34
+ "@object-ui/core": "6.0.3",
35
+ "@object-ui/data-objectstack": "6.0.3",
36
+ "@object-ui/fields": "6.0.3",
37
+ "@object-ui/i18n": "6.0.3",
38
+ "@object-ui/layout": "6.0.3",
39
+ "@object-ui/permissions": "6.0.3",
40
+ "@object-ui/providers": "6.0.3",
41
+ "@object-ui/react": "6.0.3",
42
+ "@object-ui/types": "6.0.3"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "react": "^18.0.0 || ^19.0.0",
46
46
  "react-dom": "^18.0.0 || ^19.0.0",
47
47
  "react-router-dom": "^6.0.0 || ^7.0.0",
48
- "@object-ui/plugin-calendar": "^6.0.1",
49
- "@object-ui/plugin-charts": "^6.0.1",
50
- "@object-ui/plugin-chatbot": "^6.0.1",
51
- "@object-ui/plugin-dashboard": "^6.0.1",
52
- "@object-ui/plugin-designer": "^6.0.1",
53
- "@object-ui/plugin-detail": "^6.0.1",
54
- "@object-ui/plugin-form": "^6.0.1",
55
- "@object-ui/plugin-grid": "^6.0.1",
56
- "@object-ui/plugin-kanban": "^6.0.1",
57
- "@object-ui/plugin-list": "^6.0.1",
58
- "@object-ui/plugin-report": "^6.0.1",
59
- "@object-ui/plugin-view": "^6.0.1"
48
+ "@object-ui/plugin-calendar": "^6.0.3",
49
+ "@object-ui/plugin-charts": "^6.0.3",
50
+ "@object-ui/plugin-chatbot": "^6.0.3",
51
+ "@object-ui/plugin-dashboard": "^6.0.3",
52
+ "@object-ui/plugin-designer": "^6.0.3",
53
+ "@object-ui/plugin-detail": "^6.0.3",
54
+ "@object-ui/plugin-form": "^6.0.3",
55
+ "@object-ui/plugin-grid": "^6.0.3",
56
+ "@object-ui/plugin-kanban": "^6.0.3",
57
+ "@object-ui/plugin-list": "^6.0.3",
58
+ "@object-ui/plugin-report": "^6.0.3",
59
+ "@object-ui/plugin-view": "^6.0.3"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^25.9.0",