@object-ui/app-shell 5.4.2 → 6.0.1

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,74 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 6.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - dbb9a98: cloud
8
+ - @object-ui/types@6.0.1
9
+ - @object-ui/core@6.0.1
10
+ - @object-ui/i18n@6.0.1
11
+ - @object-ui/react@6.0.1
12
+ - @object-ui/components@6.0.1
13
+ - @object-ui/fields@6.0.1
14
+ - @object-ui/layout@6.0.1
15
+ - @object-ui/data-objectstack@6.0.1
16
+ - @object-ui/auth@6.0.1
17
+ - @object-ui/permissions@6.0.1
18
+ - @object-ui/collaboration@6.0.1
19
+ - @object-ui/providers@6.0.1
20
+
21
+ ## 6.0.0
22
+
23
+ ### Major Changes
24
+
25
+ - 168a4d0: ai
26
+
27
+ ### Patch Changes
28
+
29
+ - 451bbee: **HITL conversation loop:** `useHitlInChat` now accepts a
30
+ `continueConversation(prompt, ctx)` callback. After the operator approves
31
+ or rejects a tool call from inline chat buttons, the hook synthesises a
32
+ short follow-up user prompt (tagged `[HITL pa_xxx]`, with the executed
33
+ result or rejection reason) and invokes the callback so the LLM
34
+ continues the conversation with full awareness of the outcome.
35
+
36
+ `ConsoleFloatingChatbot` wires this callback to `useObjectChat`'s
37
+ `sendMessage`, closing the loop end-to-end. Execution failures stay
38
+ visible in the inline status badge but do NOT continue automatically —
39
+ the operator decides next steps.
40
+
41
+ No framework changes required. Internal `idMap` now also tracks the
42
+ tool name so the synthesised prompt is human-readable. New test suite
43
+ `useHitlInChat.test.tsx` covers approve/reject/failed/no-callback
44
+ branches.
45
+
46
+ - Updated dependencies [451bbee]
47
+ - @object-ui/plugin-chatbot@6.0.0
48
+ - @object-ui/types@6.0.0
49
+ - @object-ui/core@6.0.0
50
+ - @object-ui/i18n@6.0.0
51
+ - @object-ui/react@6.0.0
52
+ - @object-ui/components@6.0.0
53
+ - @object-ui/fields@6.0.0
54
+ - @object-ui/layout@6.0.0
55
+ - @object-ui/data-objectstack@6.0.0
56
+ - @object-ui/auth@6.0.0
57
+ - @object-ui/permissions@6.0.0
58
+ - @object-ui/plugin-calendar@6.0.0
59
+ - @object-ui/plugin-charts@6.0.0
60
+ - @object-ui/plugin-dashboard@6.0.0
61
+ - @object-ui/plugin-designer@6.0.0
62
+ - @object-ui/plugin-detail@6.0.0
63
+ - @object-ui/plugin-form@6.0.0
64
+ - @object-ui/plugin-grid@6.0.0
65
+ - @object-ui/plugin-kanban@6.0.0
66
+ - @object-ui/plugin-list@6.0.0
67
+ - @object-ui/plugin-report@6.0.0
68
+ - @object-ui/plugin-view@6.0.0
69
+ - @object-ui/collaboration@6.0.0
70
+ - @object-ui/providers@6.0.0
71
+
3
72
  ## 5.4.2
4
73
 
5
74
  ### Patch Changes
@@ -8,13 +8,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  *
9
9
  * @module
10
10
  */
11
- import { useEffect, Suspense, lazy } from 'react';
11
+ import { useEffect } from 'react';
12
12
  import { useNavigationContext } from '../../context/NavigationContext';
13
13
  import { AppHeader } from '../../layout/AppHeader';
14
14
  import { useDiscovery } from '@object-ui/react';
15
- // Lazy-load the chatbot so its heavy markdown deps stay out of the initial
16
- // paint until the AI assistant is actually enabled.
17
- const ConsoleFloatingChatbot = lazy(() => import('../../layout/ConsoleFloatingChatbot'));
15
+ // Lightweight FAB stub the heavy chat chunk graph only downloads on
16
+ // first hover/click. See ../../layout/ConsoleChatbotFab.tsx.
17
+ import { ConsoleChatbotFab } from '../../layout/ConsoleChatbotFab';
18
18
  export function HomeLayout({ children }) {
19
19
  const { setContext } = useNavigationContext();
20
20
  const { isAiEnabled } = useDiscovery();
@@ -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(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { 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: [] })] }));
30
30
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * Marketplace Package Detail Page.
4
4
  *
@@ -12,6 +12,7 @@ import { ArrowLeft, ExternalLink, Download, AlertCircle, Package, Trash2 } from
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';
15
+ import { getRuntimeConfig } from '../../runtime-config';
15
16
  export function MarketplacePackagePage() {
16
17
  const navigate = useNavigate();
17
18
  const { packageId, appName } = useParams();
@@ -32,6 +33,8 @@ export function MarketplacePackagePage() {
32
33
  const [installingLocal, setInstallingLocal] = useState(false);
33
34
  const [localResult, setLocalResult] = useState(null);
34
35
  useEffect(() => {
36
+ if (!getRuntimeConfig().features.installLocal)
37
+ return;
35
38
  let cancelled = false;
36
39
  (async () => {
37
40
  const items = await listLocalInstalls();
@@ -191,10 +194,10 @@ export function MarketplacePackagePage() {
191
194
  const pkg = data.package;
192
195
  const latestVersion = pkg.latest_version?.version ?? data.versions[0]?.version ?? null;
193
196
  const localInstall = localInstalls.find((i) => i.manifestId === pkg.manifest_id) ?? null;
194
- 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: [_jsxs(Button, { onClick: doInstallLocal, disabled: !latestVersion || installingLocal, size: "lg", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), installingLocal
195
- ? 'Working…'
196
- : localInstall
197
- ? `Reinstall to this runtime`
198
- : '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
+ 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)
199
202
  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' }))] })] }) })] }));
200
203
  }
@@ -57,6 +57,20 @@ export declare function listMarketplacePackages(params?: {
57
57
  offset?: number;
58
58
  }): Promise<MarketplaceListResponse>;
59
59
  export declare function getMarketplacePackage(id: string): Promise<MarketplaceDetailResponse>;
60
+ /**
61
+ * Install a package into an environment by calling the cloud's
62
+ * `install_package` action **directly** (not via the proxy). Requires the
63
+ * caller's browser to have a valid cloud session cookie — typically
64
+ * because the user has signed into cloud at least once and the cookie
65
+ * domain covers this origin. When that's not the case the call fails
66
+ * with 401; the UI can then surface a "Sign in on cloud" link.
67
+ *
68
+ * `cloudBaseUrl` is supplied by the server at boot via
69
+ * `/api/v1/runtime/config` (see `runtime-config.ts`); we read it through
70
+ * `getCloudBase()` rather than hardcoding `VITE_CLOUD_URL` or sniffing
71
+ * `window.location.hostname`. When the runtime *is* the cloud,
72
+ * `getCloudBase()` returns `''` and we fall back to same-origin.
73
+ */
60
74
  export interface InstallResponse {
61
75
  installation?: {
62
76
  id: string;
@@ -12,6 +12,7 @@
12
12
  * cloud/packages/service-cloud/src/routes/marketplace.ts
13
13
  * framework/packages/runtime/src/cloud/marketplace-proxy-plugin.ts
14
14
  */
15
+ import { getCloudBase } from '../../runtime-config';
15
16
  const SERVER_URL = (import.meta.env.VITE_SERVER_URL || '').replace(/\/$/, '');
16
17
  const API_BASE = `${SERVER_URL}/api/v1/marketplace`;
17
18
  async function call(path, init) {
@@ -52,21 +53,8 @@ export async function listMarketplacePackages(params = {}) {
52
53
  export async function getMarketplacePackage(id) {
53
54
  return call(`/packages/${encodeURIComponent(id)}`);
54
55
  }
55
- /**
56
- * Install a package into an environment by calling the cloud's
57
- * `install_package` action **directly** (not via the proxy). Requires the
58
- * caller's browser to have a valid cloud session cookie — typically
59
- * because the user has signed into cloud at least once and the cookie
60
- * domain covers this origin. When that's not the case the call fails
61
- * with 401; the UI can then surface a "Sign in on cloud" link.
62
- *
63
- * `cloudBaseUrl` is read from `VITE_CLOUD_URL` if set; otherwise we fall
64
- * back to relative (assumes same-origin, which is true for cloud-hosted
65
- * consoles but generally not for tenant runtimes).
66
- */
67
- const CLOUD_BASE = (import.meta.env.VITE_CLOUD_URL || '').replace(/\/$/, '');
68
56
  export async function installPackage(input) {
69
- const base = CLOUD_BASE || SERVER_URL;
57
+ const base = getCloudBase() || SERVER_URL;
70
58
  const res = await fetch(`${base}/api/v1/actions/sys_package/install_package`, {
71
59
  method: 'POST',
72
60
  credentials: 'include',
@@ -95,7 +83,7 @@ export async function installPackage(input) {
95
83
  return (payload?.data ?? payload);
96
84
  }
97
85
  export async function listCloudEnvironments() {
98
- const base = CLOUD_BASE || SERVER_URL;
86
+ const base = getCloudBase() || SERVER_URL;
99
87
  const res = await fetch(`${base}/api/v1/data/sys_environment?limit=200`, {
100
88
  credentials: 'include',
101
89
  headers: { 'Accept': 'application/json' },
@@ -119,7 +107,7 @@ export async function listCloudEnvironments() {
119
107
  * can render a clean "no installable environments" state.
120
108
  */
121
109
  export async function listInstallableOrgIds() {
122
- const base = CLOUD_BASE || SERVER_URL;
110
+ const base = getCloudBase() || SERVER_URL;
123
111
  // sys_member rows are scoped to the caller; better-auth-managed table.
124
112
  const url = `${base}/api/v1/data/sys_member?limit=200`;
125
113
  let payload = null;
@@ -149,7 +137,7 @@ export async function listInstallableOrgIds() {
149
137
  return ids;
150
138
  }
151
139
  export function cloudInstallDeepLink(packageId) {
152
- const base = CLOUD_BASE || 'https://cloud.objectos.app';
140
+ const base = getCloudBase() || 'https://cloud.objectos.app';
153
141
  return `${base}/apps/cloud-control/sys_package/${encodeURIComponent(packageId)}`;
154
142
  }
155
143
  export async function installLocal(input) {
@@ -14,6 +14,7 @@ import { useAuth } from '@object-ui/auth';
14
14
  import { useObjectTranslation } from '@object-ui/i18n';
15
15
  import { useNavigate } from 'react-router-dom';
16
16
  import { CreateWorkspaceDialog } from './CreateWorkspaceDialog';
17
+ import { resolveHomeUrl } from './resolveHomeUrl';
17
18
  function getOrgInitials(name) {
18
19
  return name
19
20
  .split(/[\s_-]+/)
@@ -42,9 +43,7 @@ export function OrganizationsPage() {
42
43
  if (org.id !== activeOrganization?.id) {
43
44
  await switchOrganization(org.id);
44
45
  }
45
- const base = import.meta.env.BASE_URL || '/';
46
- const normalizedBase = base.endsWith('/') ? base : `${base}/`;
47
- window.location.href = `${window.location.origin}${normalizedBase}home`;
46
+ window.location.href = resolveHomeUrl();
48
47
  }
49
48
  catch (err) {
50
49
  console.error('[OrganizationsPage] Failed to switch:', err);
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Resolve the absolute URL of the console "home" route used after switching
3
+ * the active organization.
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.
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).
18
+ */
19
+ export declare function resolveHomeUrl(baseURI?: string): string;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Resolve the absolute URL of the console "home" route used after switching
3
+ * the active organization.
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.
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).
18
+ */
19
+ export function resolveHomeUrl(baseURI = document.baseURI) {
20
+ return new URL('home', baseURI).toString();
21
+ }
package/dist/index.d.ts CHANGED
@@ -19,6 +19,8 @@ export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, Conn
19
19
  export type { ActivityItem } from './layout';
20
20
  export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, type ToastWithUndoOptions, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
21
21
  export { initSentry, captureError, setSentryUser, getSentry } from './observability';
22
+ export { initRuntimeConfig, getRuntimeConfig, getCloudBase, isRuntimeConfigInitialised, resetRuntimeConfigForTesting, } from './runtime-config';
23
+ export type { RuntimeConfig, RuntimeFeatures } from './runtime-config';
22
24
  export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
23
25
  export type { RecordFormPageProps } from './views';
24
26
  export { useFavorites, useMetadataService, useNavPins, useNavigationSync, NavigationSyncEffect, addNavigationItem, removeNavigationItems, renameNavigationItems, navigationEqual, generateNavId, useResponsiveSidebar, } from './hooks';
package/dist/index.js CHANGED
@@ -22,6 +22,10 @@ export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, Conn
22
22
  export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
23
23
  // Observability — Sentry integration, opt-in via VITE_SENTRY_DSN
24
24
  export { initSentry, captureError, setSentryUser, getSentry } from './observability';
25
+ // Runtime configuration pushed by the server at boot. Consumers fetch
26
+ // `/api/v1/runtime/config` via `initRuntimeConfig()` before first render
27
+ // and read upstream cloud URL + capability flags from `getRuntimeConfig()`.
28
+ export { initRuntimeConfig, getRuntimeConfig, getCloudBase, isRuntimeConfigInitialised, resetRuntimeConfigForTesting, } from './runtime-config';
25
29
  // Standard inner-SPA views
26
30
  export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
27
31
  // Hooks
@@ -0,0 +1,3 @@
1
+ import type { ConsoleFloatingChatbotProps } from './ConsoleFloatingChatbot';
2
+ export type ConsoleChatbotFabProps = ConsoleFloatingChatbotProps;
3
+ export declare function ConsoleChatbotFab(props: ConsoleChatbotFabProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ConsoleChatbotFab
4
+ *
5
+ * Lightweight FAB proxy for the console's floating AI assistant.
6
+ *
7
+ * Before any interaction this component renders a small, zero-dependency
8
+ * button (~1KB). On first hover/focus it speculatively warms the heavy
9
+ * chat chunk graph (plugin-chatbot → streamdown → shiki → mermaid →
10
+ * @ai-sdk, ~20MB); on first click it lazy-mounts `<ConsoleFloatingChatbot
11
+ * defaultOpen />` which takes over (its own FAB replaces this stub).
12
+ *
13
+ * Net effect: every console page-load no longer pays the chat-bundle
14
+ * cost just for the FAB to be visible — those bytes only download when
15
+ * the user actually opens (or hovers) the assistant.
16
+ *
17
+ * @module
18
+ */
19
+ import { Suspense, lazy, useState } from 'react';
20
+ const ConsoleFloatingChatbot = lazy(() => import('./ConsoleFloatingChatbot'));
21
+ const prefetchChatbot = () => {
22
+ void import('./ConsoleFloatingChatbot');
23
+ };
24
+ export function ConsoleChatbotFab(props) {
25
+ const [armed, setArmed] = useState(false);
26
+ if (armed) {
27
+ return (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { ...props, defaultOpen: true }) }));
28
+ }
29
+ return (_jsx("button", { type: "button", "aria-label": `Open ${props.appLabel} assistant`, onClick: () => setArmed(true), onMouseEnter: prefetchChatbot, onFocus: prefetchChatbot, className: "fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg ring-1 ring-primary/20 transition-transform hover:scale-105 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2", children: _jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M12 3v3" }), _jsx("path", { d: "M12 18v3" }), _jsx("path", { d: "M5.6 5.6l2.1 2.1" }), _jsx("path", { d: "M16.3 16.3l2.1 2.1" }), _jsx("path", { d: "M3 12h3" }), _jsx("path", { d: "M18 12h3" }), _jsx("path", { d: "M5.6 18.4l2.1-2.1" }), _jsx("path", { d: "M16.3 7.7l2.1-2.1" }), _jsx("circle", { cx: "12", cy: "12", r: "3" })] }) }));
30
+ }
@@ -12,6 +12,8 @@ export interface ConsoleFloatingChatbotProps {
12
12
  apiBase?: string;
13
13
  /** Default agent name to select on first render. */
14
14
  defaultAgent?: string;
15
+ /** Whether the floating panel should open immediately on mount. */
16
+ defaultOpen?: boolean;
15
17
  }
16
- export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, }: ConsoleFloatingChatbotProps): import("react/jsx-runtime").JSX.Element;
18
+ export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen, }: ConsoleFloatingChatbotProps): import("react/jsx-runtime").JSX.Element;
17
19
  export {};
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
12
  * @module
13
13
  */
14
14
  import React from 'react';
15
- import { FloatingChatbot, useObjectChat, useAgents, } from '@object-ui/plugin-chatbot';
15
+ import { FloatingChatbot, useObjectChat, useAgents, useHitlInChat, } from '@object-ui/plugin-chatbot';
16
16
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
17
17
  const DEFAULT_AI_PATH = '/api/v1/ai';
18
18
  function resolveApiBase(explicit) {
@@ -25,7 +25,7 @@ function resolveApiBase(explicit) {
25
25
  const serverUrl = env.VITE_SERVER_URL ?? '';
26
26
  return `${serverUrl.replace(/\/$/, '')}${DEFAULT_AI_PATH}`;
27
27
  }
28
- function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, activeAgent, onAgentChange, chatApi, }) {
28
+ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, activeAgent, onAgentChange, chatApi, apiBase, defaultOpen = false, }) {
29
29
  const objectNames = objects.map((o) => o.label || o.name).join(', ');
30
30
  const activeAgentLabel = React.useMemo(() => {
31
31
  const found = agents.find((a) => a.name === activeAgent);
@@ -59,10 +59,22 @@ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, a
59
59
  : "Thanks for your message! I'm here to help you navigate and manage your data.",
60
60
  autoResponseDelay: 600,
61
61
  });
62
+ // HITL bridge — turns the pending-approval tool result envelope from the
63
+ // framework's action-tools.ts into inline approve/reject buttons that talk
64
+ // directly to /api/v1/ai/pending-actions/:id/{approve,reject}. After a
65
+ // successful decision the hook synthesises a short follow-up user message
66
+ // so the LLM continues the conversation aware of the outcome.
67
+ const hitl = useHitlInChat({
68
+ messages: messages,
69
+ apiBase,
70
+ continueConversation: (prompt) => {
71
+ sendMessage(prompt);
72
+ },
73
+ });
62
74
  const headerExtra = agents.length > 0 ? (_jsxs(Select, { value: activeAgent, onValueChange: onAgentChange, disabled: agentsLoading, children: [_jsx(SelectTrigger, { className: "h-7 w-[180px] text-xs", "data-testid": "floating-chatbot-agent-picker", children: _jsx(SelectValue, { placeholder: "Choose agent..." }) }), _jsx(SelectContent, { align: "end", children: agents.map((agent) => (_jsxs(SelectItem, { value: agent.name, className: "text-xs", children: [_jsx("span", { className: "font-medium", children: agent.label }), agent.description ? (_jsx("span", { className: "block text-muted-foreground text-[10px] truncate max-w-[220px]", children: agent.description })) : null] }, agent.name))) })] })) : null;
63
75
  return (_jsx(FloatingChatbot, { floatingConfig: {
64
76
  position: 'bottom-right',
65
- defaultOpen: false,
77
+ defaultOpen,
66
78
  panelWidth: 420,
67
79
  panelHeight: 560,
68
80
  title: `${appLabel} Assistant`,
@@ -71,9 +83,9 @@ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, a
71
83
  ? `Ask ${activeAgentLabel}...`
72
84
  : agentsLoading
73
85
  ? 'Loading agents...'
74
- : 'Ask anything...', onSendMessage: (content) => sendMessage(content), onClear: clear, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: error, enableMarkdown: true }));
86
+ : '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" }));
75
87
  }
76
- export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, }) {
88
+ export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen = false, }) {
77
89
  const apiBase = React.useMemo(() => resolveApiBase(apiBaseProp), [apiBaseProp]);
78
90
  const env = import.meta.env ?? {};
79
91
  const envDefaultAgent = env.VITE_AI_DEFAULT_AGENT;
@@ -92,5 +104,5 @@ export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: api
92
104
  // `key` forces a clean remount whenever the active agent (and therefore the
93
105
  // chat API URL) changes — required because `useObjectChat` locks its mode
94
106
  // (api vs local) on first render.
95
- return (_jsx(ChatbotInner, { appLabel: appLabel, objects: objects, agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, onAgentChange: setActiveAgent, chatApi: chatApi }, chatApi ?? 'local'));
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'));
96
108
  }
@@ -8,12 +8,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
8
8
  * Sets navigation context to 'app' for app-specific routes.
9
9
  * @module
10
10
  */
11
- import { useEffect, Suspense, lazy } from 'react';
11
+ import { useEffect } from 'react';
12
12
  import { AppShell } from '@object-ui/layout';
13
13
  import { useDiscovery } from '@object-ui/react';
14
- // Lazy-load the chatbot so its heavy markdown deps (~150 KB) stay out of
15
- // the initial paint until the AI assistant is actually enabled.
16
- const ConsoleFloatingChatbot = lazy(() => import('./ConsoleFloatingChatbot'));
14
+ // Lightweight FAB stub the heavy chat chunk graph (plugin-chatbot,
15
+ // shiki, streamdown, mermaid, @ai-sdk, ~20MB) only downloads on first
16
+ // hover/click. See ConsoleChatbotFab.tsx.
17
+ import { ConsoleChatbotFab } from './ConsoleChatbotFab';
17
18
  import { UnifiedSidebar } from './UnifiedSidebar';
18
19
  import { AppHeader } from './AppHeader';
19
20
  import { MobileViewSwitcherProvider } from './MobileViewSwitcherContext';
@@ -50,5 +51,5 @@ export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange,
50
51
  ? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
51
52
  : undefined,
52
53
  }
53
- : undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(Suspense, { fallback: null, children: _jsx(ConsoleFloatingChatbot, { appLabel: appLabel, objects: objects }) }))] }) }));
54
+ : undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(ConsoleChatbotFab, { appLabel: appLabel, objects: objects }))] }) }));
54
55
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Runtime configuration pushed by the server at boot.
3
+ *
4
+ * The SPA fetches `GET /api/v1/runtime/config` once before first paint
5
+ * (`initRuntimeConfig()`) and exposes the response through a module-level
6
+ * singleton (`config` + `getRuntimeConfig()`). Modules that need to know
7
+ * about the upstream cloud URL or capability flags read from here —
8
+ * NEVER from `window.location.hostname` or Vite-time env vars, since
9
+ * those don't reflect the runtime the SPA is actually attached to (e.g.
10
+ * a tenant ObjectOS runtime pointing at a separate cloud control plane).
11
+ *
12
+ * Mirrors `apps/studio/src/lib/config.ts` but lives in app-shell because
13
+ * the Console SPA in `apps/console` consumes app-shell code.
14
+ *
15
+ * Server-side: see
16
+ * cloud/packages/service-cloud/src/multi-environment-plugins.ts
17
+ * cloud/packages/service-cloud/src/single-environment-plugin.ts
18
+ * framework/packages/runtime/src/cloud/runtime-config-plugin.ts
19
+ */
20
+ export interface RuntimeFeatures {
21
+ /** "Install to this runtime" button is meaningful on this runtime. */
22
+ installLocal: boolean;
23
+ /** `/api/v1/marketplace/*` is reachable from this runtime. */
24
+ marketplace: boolean;
25
+ }
26
+ export interface RuntimeConfig {
27
+ /**
28
+ * Upstream cloud base URL — the SPA dispatches install + env listing
29
+ * directly against this origin. Empty string ⇒ same-origin (i.e. the
30
+ * runtime we're attached to *is* the cloud).
31
+ */
32
+ cloudUrl: string;
33
+ /** Single-environment runtime (CLI `os serve`, etc.). */
34
+ singleEnvironment: boolean;
35
+ defaultOrgId?: string | null;
36
+ defaultEnvironmentId?: string | null;
37
+ features: RuntimeFeatures;
38
+ }
39
+ /**
40
+ * Fetch the server-pushed runtime config and merge it into the singleton.
41
+ * Must be awaited before first render so consumers see definitive values
42
+ * on first paint. Safe to call more than once (subsequent calls re-fetch
43
+ * and re-merge).
44
+ *
45
+ * `baseUrl` lets callers in dev (Vite proxy) override the fetch origin.
46
+ * In production both Console SPA and tenant runtime share an origin so
47
+ * the default (relative `/api/v1/...`) works.
48
+ */
49
+ export declare function initRuntimeConfig(baseUrl?: string): Promise<void>;
50
+ /** Read-only accessor. Returns the current snapshot. */
51
+ export declare function getRuntimeConfig(): RuntimeConfig;
52
+ /** Whether `initRuntimeConfig()` has run at least once. */
53
+ export declare function isRuntimeConfigInitialised(): boolean;
54
+ /**
55
+ * Resolve the upstream cloud base URL the SPA should target. When the
56
+ * runtime says it *is* the cloud (`cloudUrl: ''`) the SPA stays on the
57
+ * current origin. Otherwise this returns the server-supplied URL with no
58
+ * trailing slash.
59
+ */
60
+ export declare function getCloudBase(): string;
61
+ /** Test/dev helper. */
62
+ export declare function resetRuntimeConfigForTesting(): void;
@@ -0,0 +1,86 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+ const defaults = {
3
+ cloudUrl: '',
4
+ singleEnvironment: false,
5
+ defaultOrgId: null,
6
+ defaultEnvironmentId: null,
7
+ features: { installLocal: false, marketplace: true },
8
+ };
9
+ let current = { ...defaults };
10
+ let initialised = false;
11
+ /** Apply a partial update over the singleton. */
12
+ function applyUpdate(patch) {
13
+ current = {
14
+ ...current,
15
+ ...patch,
16
+ features: {
17
+ ...current.features,
18
+ ...(patch.features ?? {}),
19
+ },
20
+ };
21
+ }
22
+ /**
23
+ * Fetch the server-pushed runtime config and merge it into the singleton.
24
+ * Must be awaited before first render so consumers see definitive values
25
+ * on first paint. Safe to call more than once (subsequent calls re-fetch
26
+ * and re-merge).
27
+ *
28
+ * `baseUrl` lets callers in dev (Vite proxy) override the fetch origin.
29
+ * In production both Console SPA and tenant runtime share an origin so
30
+ * the default (relative `/api/v1/...`) works.
31
+ */
32
+ export async function initRuntimeConfig(baseUrl = '') {
33
+ const base = (baseUrl || '').replace(/\/+$/, '');
34
+ try {
35
+ const res = await fetch(`${base}/api/v1/runtime/config`, {
36
+ credentials: 'include',
37
+ headers: { Accept: 'application/json' },
38
+ });
39
+ if (!res.ok)
40
+ return;
41
+ const body = (await res.json());
42
+ if (!body || typeof body !== 'object')
43
+ return;
44
+ applyUpdate({
45
+ cloudUrl: typeof body.cloudUrl === 'string' ? body.cloudUrl.replace(/\/+$/, '') : current.cloudUrl,
46
+ singleEnvironment: !!body.singleEnvironment,
47
+ defaultOrgId: body.defaultOrgId ?? current.defaultOrgId ?? null,
48
+ defaultEnvironmentId: body.defaultEnvironmentId ?? current.defaultEnvironmentId ?? null,
49
+ features: body.features
50
+ ? {
51
+ installLocal: !!body.features.installLocal,
52
+ marketplace: body.features.marketplace !== false,
53
+ }
54
+ : current.features,
55
+ });
56
+ }
57
+ catch {
58
+ // Endpoint missing or network failure ⇒ keep defaults. Older runtimes
59
+ // pre-dating this endpoint simply behave as before.
60
+ }
61
+ finally {
62
+ initialised = true;
63
+ }
64
+ }
65
+ /** Read-only accessor. Returns the current snapshot. */
66
+ export function getRuntimeConfig() {
67
+ return current;
68
+ }
69
+ /** Whether `initRuntimeConfig()` has run at least once. */
70
+ export function isRuntimeConfigInitialised() {
71
+ return initialised;
72
+ }
73
+ /**
74
+ * Resolve the upstream cloud base URL the SPA should target. When the
75
+ * runtime says it *is* the cloud (`cloudUrl: ''`) the SPA stays on the
76
+ * current origin. Otherwise this returns the server-supplied URL with no
77
+ * trailing slash.
78
+ */
79
+ export function getCloudBase() {
80
+ return current.cloudUrl ?? '';
81
+ }
82
+ /** Test/dev helper. */
83
+ export function resetRuntimeConfigForTesting() {
84
+ current = { ...defaults, features: { ...defaults.features } };
85
+ initialised = false;
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/app-shell",
3
- "version": "5.4.2",
3
+ "version": "6.0.1",
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": "5.4.2",
32
- "@object-ui/collaboration": "5.4.2",
33
- "@object-ui/components": "5.4.2",
34
- "@object-ui/core": "5.4.2",
35
- "@object-ui/data-objectstack": "5.4.2",
36
- "@object-ui/fields": "5.4.2",
37
- "@object-ui/i18n": "5.4.2",
38
- "@object-ui/layout": "5.4.2",
39
- "@object-ui/permissions": "5.4.2",
40
- "@object-ui/providers": "5.4.2",
41
- "@object-ui/react": "5.4.2",
42
- "@object-ui/types": "5.4.2"
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"
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": "^5.4.2",
49
- "@object-ui/plugin-charts": "^5.4.2",
50
- "@object-ui/plugin-chatbot": "^5.4.2",
51
- "@object-ui/plugin-dashboard": "^5.4.2",
52
- "@object-ui/plugin-designer": "^5.4.2",
53
- "@object-ui/plugin-detail": "^5.4.2",
54
- "@object-ui/plugin-form": "^5.4.2",
55
- "@object-ui/plugin-grid": "^5.4.2",
56
- "@object-ui/plugin-kanban": "^5.4.2",
57
- "@object-ui/plugin-list": "^5.4.2",
58
- "@object-ui/plugin-report": "^5.4.2",
59
- "@object-ui/plugin-view": "^5.4.2"
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"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^25.9.0",