@object-ui/app-shell 11.0.0 → 11.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/dist/chrome/LoadingScreen.js +1 -1
  3. package/dist/console/ConsoleShell.js +2 -1
  4. package/dist/console/RemediationOverlay.d.ts +17 -0
  5. package/dist/console/RemediationOverlay.js +113 -0
  6. package/dist/console/ai/AiChatPage.d.ts +36 -0
  7. package/dist/console/ai/AiChatPage.js +104 -13
  8. package/dist/console/home/CloudOnboardingNext.d.ts +10 -0
  9. package/dist/console/home/CloudOnboardingNext.js +117 -0
  10. package/dist/console/home/HomePage.js +2 -1
  11. package/dist/console/organizations/OrganizationsPage.js +6 -2
  12. package/dist/console/organizations/resolveHomeUrl.d.ts +7 -0
  13. package/dist/console/organizations/resolveHomeUrl.js +19 -2
  14. package/dist/hooks/useChatConversation.d.ts +22 -0
  15. package/dist/hooks/useChatConversation.js +57 -10
  16. package/dist/hooks/useReconcileOnError.js +12 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.js +6 -0
  19. package/dist/layout/ConsoleFloatingChatbot.js +54 -7
  20. package/dist/layout/WorkspaceSwitcher.d.ts +4 -2
  21. package/dist/layout/WorkspaceSwitcher.js +15 -10
  22. package/dist/preview/DraftPreviewBar.js +15 -11
  23. package/dist/utils/index.d.ts +2 -24
  24. package/dist/utils/index.js +14 -101
  25. package/dist/views/DashboardView.js +2 -3
  26. package/dist/views/InterfaceListPage.js +4 -1
  27. package/dist/views/ObjectView.js +4 -2
  28. package/dist/views/PageView.js +14 -5
  29. package/dist/views/RecordDetailView.js +1 -0
  30. package/dist/views/ReportView.js +2 -3
  31. package/dist/views/metadata-admin/clientValidation.js +8 -2
  32. package/dist/views/metadata-admin/color-variant-field.d.ts +1 -12
  33. package/dist/views/metadata-admin/color-variant-field.js +11 -0
  34. package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +1 -13
  35. package/dist/views/metadata-admin/previews/OutlineStrip.js +12 -0
  36. package/dist/views/metadata-admin/previews/PagePreview.js +9 -0
  37. package/dist/views/metadata-admin/previews/SourcePageEditor.d.ts +28 -0
  38. package/dist/views/metadata-admin/previews/SourcePageEditor.js +83 -0
  39. package/dist/views/studio-design/StudioDesignSurface.d.ts +20 -0
  40. package/dist/views/studio-design/StudioDesignSurface.js +659 -0
  41. package/package.json +43 -43
package/CHANGELOG.md CHANGED
@@ -1,5 +1,149 @@
1
1
  # @object-ui/app-shell — Changelog
2
2
 
3
+ ## 11.3.0
4
+
5
+ ### Patch Changes
6
+
7
+ - ca4a795: fix(app-shell): restore admin design surface gated on the removed `user.role='admin'` overwrite
8
+
9
+ ADR-0068 (a3a5abff8) stopped the server `customSession` from overwriting
10
+ `user.role = 'admin'` for workspace owners/admins — canonical roles now arrive
11
+ in `user.roles[]` (`org_owner` / `org_admin`) with `user.isPlatformAdmin` as a
12
+ derived alias, and `useIsWorkspaceAdmin()` was introduced to read them. Four
13
+ runtime views were missed in that migration and still gated their admin design
14
+ tools on the now-defunct `user?.role === 'admin'`, so workspace owners/admins
15
+ silently lost:
16
+
17
+ - **ObjectView** — the list "+ New view" button plus rename/delete/pin/
18
+ set-default/config/manage-views and the view config panel.
19
+ - **PageView / DashboardView / ReportView** — the inline "Edit"/config entry
20
+ points for the shared page / dashboard / report definitions.
21
+
22
+ All four now call `useIsWorkspaceAdmin()` (same helper already adopted by
23
+ AppSidebar, UnifiedSidebar, HomePage, Marketplace…). No behavior change for
24
+ genuine platform admins; restores the surface for org owners/admins.
25
+
26
+ - Updated dependencies [d88c8ec]
27
+ - Updated dependencies [b7237bb]
28
+ - Updated dependencies [d23d6eb]
29
+ - @object-ui/components@11.3.0
30
+ - @object-ui/i18n@11.3.0
31
+ - @object-ui/core@11.3.0
32
+ - @object-ui/fields@11.3.0
33
+ - @object-ui/layout@11.3.0
34
+ - @object-ui/plugin-editor@11.3.0
35
+ - @object-ui/react@11.3.0
36
+ - @object-ui/data-objectstack@11.3.0
37
+ - @object-ui/types@11.3.0
38
+ - @object-ui/auth@11.3.0
39
+ - @object-ui/permissions@11.3.0
40
+ - @object-ui/collaboration@11.3.0
41
+ - @object-ui/providers@11.3.0
42
+
43
+ ## 11.2.0
44
+
45
+ ### Minor Changes
46
+
47
+ - 490ba55: feat(cloud): state-aware onboarding next-step widget for the Cloud Welcome page
48
+
49
+ The Cloud control-plane Welcome page is static SDUI, but the most useful thing it
50
+ can show — "what do I do next?" — depends on live state the metadata can't carry:
51
+ does the caller's org already have its production environment? New signups are
52
+ auto-provisioned one, so a static "Step 1: create an environment" is wrong for
53
+ most first-time users.
54
+
55
+ Add `cloud:onboarding-next`, a registered SDUI widget that resolves
56
+ `hasProductionEnv` from the same org-scoped `/cloud/environment-entitlements`
57
+ endpoint the environment list uses, and renders the right primary action:
58
+
59
+ - no production env → **Create your environment** (the real first step);
60
+ - has production env → **Open Production** (full-page nav that follows the SSO
61
+ 302 into the env) + **Manage environments**;
62
+ - loading → a neutral skeleton (no CTA flash / layout jump);
63
+ - unknown / error → degrades to the open-production actions, so the button
64
+ always works.
65
+
66
+ Routes and the SSO endpoint come from the page metadata (`properties`), so the
67
+ Cloud app owns its URLs and copy; the widget owns only the state logic.
68
+
69
+ - 32dbd6a: feat(detail): `relatedLayout: 'tabs'` — surface related tables as peer tabs via config
70
+
71
+ Record detail pages can now show each related table as its own top-level tab
72
+ instead of stacking them all inside a single **Related** tab — no custom page
73
+ required. Set `detail.relatedLayout: 'tabs'` on the object; the synthesized
74
+ record page then emits one tab per related list (label = the related list's
75
+ `title`, falling back to its `objectName`, carrying its `icon`), slotted between
76
+ the **Details** tab and **Activity** / **History**.
77
+
78
+ - `buildDefaultPageSchema` (`@object-ui/plugin-detail`): new
79
+ `BuildPageOptions.relatedLayout?: 'stack' | 'tabs'` threaded through
80
+ `buildDefaultTabs` (the single choke point for the related-tab emission).
81
+ `'tabs'` fans the related children out into peer tabs; `'stack'` (default)
82
+ keeps the legacy single **Related** tab — **zero regression** when omitted.
83
+ Still honours `hideRelatedTab` (no related tabs emitted) in both modes.
84
+ - `RecordDetailView` (`@object-ui/app-shell`): reads
85
+ `objectDef.detail.relatedLayout` per object and forwards it to the synth.
86
+
87
+ ### Patch Changes
88
+
89
+ - Updated dependencies [9e7a986]
90
+ - Updated dependencies [1311749]
91
+ - @object-ui/components@11.2.0
92
+ - @object-ui/core@11.2.0
93
+ - @object-ui/fields@11.2.0
94
+ - @object-ui/layout@11.2.0
95
+ - @object-ui/plugin-editor@11.2.0
96
+ - @object-ui/data-objectstack@11.2.0
97
+ - @object-ui/react@11.2.0
98
+ - @object-ui/types@11.2.0
99
+ - @object-ui/i18n@11.2.0
100
+ - @object-ui/auth@11.2.0
101
+ - @object-ui/permissions@11.2.0
102
+ - @object-ui/collaboration@11.2.0
103
+ - @object-ui/providers@11.2.0
104
+
105
+ ## 11.1.0
106
+
107
+ ### Minor Changes
108
+
109
+ - 6fb6738: Auth: remediation overlay for the ADR-0069 session gate (enforced MFA / password expiry)
110
+
111
+ The ObjectStack backend now blocks logged-in users from protected resources with `403 { error: { code: 'MFA_REQUIRED' | 'PASSWORD_EXPIRED' } }`. The Console now detects this on every API response and raises a full-screen, guided remediation flow instead of leaving the user on failing requests.
112
+
113
+ - `@object-ui/auth`: the authenticated fetch wrapper detects the gate and broadcasts it via a tiny module-level emitter; `AuthProvider` exposes `remediationRequired` + `setRemediationRequired`; the `twoFactorClient` plugin is enabled and `enrollTotp` / `verifyTotp` are added to the auth client (`changePassword` already existed).
114
+ - `@object-ui/app-shell`: a `RemediationOverlay` (mounted in `ConsoleShell`) renders the guided flow — change an expired password, or enrol an authenticator (password confirm → QR + backup codes → verify TOTP) — then reloads so the app re-fetches cleanly. Auth + metadata + `me/*` reads stay reachable (server allow-list), so the overlay renders above a normally-loading shell.
115
+
116
+ ### Patch Changes
117
+
118
+ - e2c9b0d: fix(first-run): two first-time-user friction fixes found via a full ObjectOS Cloud signup walkthrough.
119
+
120
+ - **Page-load race**: an app whose landing is a `type:'page'` (SDUI page) flashed a false "page not found" / blank body on the very first render — `PageView` treated the lazily-loading (empty) `pages` array as "page doesn't exist". It now shows a loading state until the `page` metadata type is actually resolved (`getTypeStatus('page')`), then trusts the not-found. This is exactly the post-signup landing, where the app's home page is the first thing rendered.
121
+ - **Redundant launcher hop**: after creating/switching a workspace, the user was hard-reloaded to `/home` (the workspace launcher) even when the workspace has a single app — an extra, contentless layer. `OrganizationsPage` and `WorkspaceSwitcher` now reload to the console ROOT (`resolveRootUrl`), so `RootLandingRedirect` resolves the right landing: a single-app workspace lands straight IN that app; multi-app workspaces still fall back to `/home`.
122
+
123
+ - 6726a2b: First-run UX polish (objectstack-ai/objectui#2038) — copy improvements found via the ObjectOS Cloud signup walkthrough:
124
+
125
+ - **"Organization" → "Workspace"** across the org picker (`organizations.*` strings, en + zh). The create flow + WorkspaceSwitcher already say "workspace"; the picker ("Your Organizations / No organizations yet") was the lone holdout. Now consistent.
126
+ - **Non-admin empty state** — "There are no applications available to you yet. Please contact your workspace administrator." → "Your workspace is being set up — apps your admin shares with you will show up here." (less dead-end, en + zh).
127
+ - **Cold-start reassurance** — new `console.loadingHint` line under the LoadingScreen steps: "Setting up a new environment can take a few moments." (en + zh).
128
+ - **Signup value-prop** — register subtitle "Enter your information to get started" → "Create your account to start building." (en + zh).
129
+
130
+ - 8e2223c: fix(home): the workspace empty-state title hardcoded "Welcome to ObjectUI" — a stale brand a first-time user sees on their empty `/home`. Read the product name from the runtime-config branding (`getRuntimeConfig().branding.productName`, server-pushed, fallback "ObjectOS") like LoadingScreen does, so it shows the deployment's real product (e.g. "Welcome to ObjectOS Cloud").
131
+ - Updated dependencies [6fb6738]
132
+ - Updated dependencies [6726a2b]
133
+ - @object-ui/auth@11.1.0
134
+ - @object-ui/i18n@11.1.0
135
+ - @object-ui/components@11.1.0
136
+ - @object-ui/fields@11.1.0
137
+ - @object-ui/react@11.1.0
138
+ - @object-ui/layout@11.1.0
139
+ - @object-ui/plugin-editor@11.1.0
140
+ - @object-ui/types@11.1.0
141
+ - @object-ui/core@11.1.0
142
+ - @object-ui/data-objectstack@11.1.0
143
+ - @object-ui/permissions@11.1.0
144
+ - @object-ui/collaboration@11.1.0
145
+ - @object-ui/providers@11.1.0
146
+
3
147
  ## 7.3.0
4
148
 
5
149
  ### Patch Changes
@@ -36,5 +36,5 @@ export function LoadingScreen({ message, error, onRetry, retrying }) {
36
36
  }, 1200);
37
37
  return () => clearInterval(timer);
38
38
  }, [message, error, loadingSteps.length]);
39
- return (_jsx("div", { className: "flex flex-col items-center justify-center h-screen bg-background", children: _jsxs("div", { className: "flex flex-col items-center gap-6", children: [_jsxs("div", { className: "relative", children: [_jsx("div", { className: "absolute inset-0 bg-primary/20 rounded-2xl blur-xl animate-pulse" }), _jsx("div", { className: "relative bg-linear-to-br from-primary to-primary/80 p-4 rounded-2xl shadow-lg", children: _jsx(Database, { className: "h-10 w-10 text-primary-foreground" }) })] }), _jsxs("div", { className: "text-center space-y-2", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: getProductName() }), _jsx("p", { className: "text-sm text-muted-foreground", children: strings.initializing })] }), error ? (_jsxs("div", { className: "flex flex-col items-center gap-4 max-w-md w-full px-6", children: [_jsxs("div", { className: "flex flex-col items-center gap-3 p-5 rounded-lg border border-destructive/30 bg-destructive/5 w-full", children: [_jsxs("div", { className: "flex items-center gap-2 text-destructive", children: [_jsx(AlertCircle, { className: "h-5 w-5 shrink-0" }), _jsx("span", { className: "text-sm font-semibold", children: strings.error.connectionFailed })] }), _jsx("p", { className: "text-xs text-muted-foreground text-center break-words", children: error }), _jsx("p", { className: "text-xs text-muted-foreground text-center", children: strings.error.checkServer })] }), onRetry && (_jsx(Button, { onClick: onRetry, disabled: retrying, variant: "default", size: "sm", className: "gap-2", children: retrying ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), strings.actions.retrying] })) : (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { className: "h-4 w-4" }), strings.actions.retry] })) }))] })) : message ? (_jsxs("div", { className: "flex items-center gap-3 px-4 py-2 bg-muted/50 rounded-full", children: [_jsx(Spinner, { className: "h-4 w-4 text-primary" }), _jsx("span", { className: "text-sm text-muted-foreground", children: message })] })) : (_jsx("div", { className: "flex flex-col gap-2 w-64", children: loadingSteps.map((step, index) => (_jsxs("div", { className: "flex items-center gap-2.5 text-sm transition-opacity duration-300", style: { opacity: index <= currentStep ? 1 : 0.3 }, children: [index < currentStep ? (_jsx(CheckCircle2, { className: "h-4 w-4 text-primary shrink-0" })) : index === currentStep ? (_jsx(Loader2, { className: "h-4 w-4 text-primary shrink-0 animate-spin" })) : (_jsx("div", { className: "h-4 w-4 rounded-full border border-muted-foreground/30 shrink-0" })), _jsx("span", { className: index <= currentStep ? 'text-foreground' : 'text-muted-foreground', children: step })] }, step))) }))] }) }));
39
+ return (_jsx("div", { className: "flex flex-col items-center justify-center h-screen bg-background", children: _jsxs("div", { className: "flex flex-col items-center gap-6", children: [_jsxs("div", { className: "relative", children: [_jsx("div", { className: "absolute inset-0 bg-primary/20 rounded-2xl blur-xl animate-pulse" }), _jsx("div", { className: "relative bg-linear-to-br from-primary to-primary/80 p-4 rounded-2xl shadow-lg", children: _jsx(Database, { className: "h-10 w-10 text-primary-foreground" }) })] }), _jsxs("div", { className: "text-center space-y-2", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: getProductName() }), _jsx("p", { className: "text-sm text-muted-foreground", children: strings.initializing })] }), error ? (_jsxs("div", { className: "flex flex-col items-center gap-4 max-w-md w-full px-6", children: [_jsxs("div", { className: "flex flex-col items-center gap-3 p-5 rounded-lg border border-destructive/30 bg-destructive/5 w-full", children: [_jsxs("div", { className: "flex items-center gap-2 text-destructive", children: [_jsx(AlertCircle, { className: "h-5 w-5 shrink-0" }), _jsx("span", { className: "text-sm font-semibold", children: strings.error.connectionFailed })] }), _jsx("p", { className: "text-xs text-muted-foreground text-center break-words", children: error }), _jsx("p", { className: "text-xs text-muted-foreground text-center", children: strings.error.checkServer })] }), onRetry && (_jsx(Button, { onClick: onRetry, disabled: retrying, variant: "default", size: "sm", className: "gap-2", children: retrying ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), strings.actions.retrying] })) : (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { className: "h-4 w-4" }), strings.actions.retry] })) }))] })) : message ? (_jsxs("div", { className: "flex items-center gap-3 px-4 py-2 bg-muted/50 rounded-full", children: [_jsx(Spinner, { className: "h-4 w-4 text-primary" }), _jsx("span", { className: "text-sm text-muted-foreground", children: message })] })) : (_jsxs("div", { className: "flex flex-col gap-2 w-64", children: [loadingSteps.map((step, index) => (_jsxs("div", { className: "flex items-center gap-2.5 text-sm transition-opacity duration-300", style: { opacity: index <= currentStep ? 1 : 0.3 }, children: [index < currentStep ? (_jsx(CheckCircle2, { className: "h-4 w-4 text-primary shrink-0" })) : index === currentStep ? (_jsx(Loader2, { className: "h-4 w-4 text-primary shrink-0 animate-spin" })) : (_jsx("div", { className: "h-4 w-4 rounded-full border border-muted-foreground/30 shrink-0" })), _jsx("span", { className: index <= currentStep ? 'text-foreground' : 'text-muted-foreground', children: step })] }, step))), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: strings.loadingHint })] }))] }) }));
40
40
  }
@@ -25,6 +25,7 @@ import { FavoritesProvider } from '../context/FavoritesProvider';
25
25
  import { RecentItemsProvider } from '../context/RecentItemsProvider';
26
26
  import { UserStateAdaptersProvider, useAttachUserStateAdapters, } from '../context/UserStateAdapters';
27
27
  import { ThemeProvider } from '../chrome/ThemeProvider';
28
+ import { RemediationOverlay } from './RemediationOverlay';
28
29
  export function LoadingFallback() {
29
30
  return (_jsx("div", { className: "h-screen flex items-center justify-center text-sm text-muted-foreground", children: "Loading\u2026" }));
30
31
  }
@@ -43,7 +44,7 @@ export function LoadingFallback() {
43
44
  * </BrowserRouter>
44
45
  */
45
46
  export function ConsoleShell({ children }) {
46
- return (_jsx(ThemeProvider, { defaultTheme: "system", storageKey: "object-ui-theme", children: _jsx(NavigationProvider, { children: _jsx(UserStateAdaptersProvider, { children: _jsx(FavoritesProvider, { children: _jsx(RecentItemsProvider, { children: _jsx(Suspense, { fallback: _jsx(LoadingFallback, {}), children: children }) }) }) }) }) }));
47
+ return (_jsx(ThemeProvider, { defaultTheme: "system", storageKey: "object-ui-theme", children: _jsx(NavigationProvider, { children: _jsx(UserStateAdaptersProvider, { children: _jsx(FavoritesProvider, { children: _jsxs(RecentItemsProvider, { children: [_jsx(Suspense, { fallback: _jsx(LoadingFallback, {}), children: children }), _jsx(RemediationOverlay, {})] }) }) }) }) }));
47
48
  }
48
49
  /**
49
50
  * ConnectedShell — mounts the data layer (AdapterProvider + MetadataProvider).
@@ -0,0 +1,17 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * ADR-0069 — full-screen remediation overlay.
10
+ *
11
+ * When the backend blocks a logged-in user with `403 PASSWORD_EXPIRED` /
12
+ * `MFA_REQUIRED`, the API fetch interceptor raises `remediationRequired` on the
13
+ * auth context. This overlay guides the user through the fix (change the
14
+ * expired password, or enrol an authenticator) instead of leaving them staring
15
+ * at failing requests. On success it reloads so the app re-fetches cleanly.
16
+ */
17
+ export declare function RemediationOverlay(): import("react").JSX.Element | null;
@@ -0,0 +1,113 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ import { useState } from 'react';
10
+ import { useAuth } from '@object-ui/auth';
11
+ import { Button, Input, Label } from '@object-ui/components';
12
+ import { toCanvas } from 'qrcode';
13
+ /**
14
+ * ADR-0069 — full-screen remediation overlay.
15
+ *
16
+ * When the backend blocks a logged-in user with `403 PASSWORD_EXPIRED` /
17
+ * `MFA_REQUIRED`, the API fetch interceptor raises `remediationRequired` on the
18
+ * auth context. This overlay guides the user through the fix (change the
19
+ * expired password, or enrol an authenticator) instead of leaving them staring
20
+ * at failing requests. On success it reloads so the app re-fetches cleanly.
21
+ */
22
+ export function RemediationOverlay() {
23
+ const { remediationRequired } = useAuth();
24
+ if (!remediationRequired)
25
+ return null;
26
+ return (_jsx("div", { className: "fixed inset-0 z-[200] flex items-center justify-center bg-background/95 p-4 backdrop-blur-sm", children: _jsx("div", { className: "w-full max-w-md rounded-xl border bg-card p-6 shadow-xl", children: remediationRequired.code === 'PASSWORD_EXPIRED' ? (_jsx(ExpiredPasswordForm, { message: remediationRequired.message })) : (_jsx(MfaEnrollForm, { message: remediationRequired.message })) }) }));
27
+ }
28
+ function SignOutLink() {
29
+ const { signOut } = useAuth();
30
+ return (_jsx("button", { type: "button", onClick: () => { void signOut(); }, className: "mt-4 w-full text-center text-xs text-muted-foreground hover:text-foreground hover:underline", children: "Sign out instead" }));
31
+ }
32
+ function ExpiredPasswordForm({ message }) {
33
+ const { changePassword, setRemediationRequired } = useAuth();
34
+ const [current, setCurrent] = useState('');
35
+ const [next, setNext] = useState('');
36
+ const [confirm, setConfirm] = useState('');
37
+ const [busy, setBusy] = useState(false);
38
+ const [error, setError] = useState(null);
39
+ const submit = async (e) => {
40
+ e.preventDefault();
41
+ setError(null);
42
+ if (next !== confirm) {
43
+ setError('New passwords do not match.');
44
+ return;
45
+ }
46
+ setBusy(true);
47
+ try {
48
+ await changePassword(current, next);
49
+ setRemediationRequired(null);
50
+ window.location.reload();
51
+ }
52
+ catch (err) {
53
+ setError(err instanceof Error ? err.message : 'Could not change password.');
54
+ setBusy(false);
55
+ }
56
+ };
57
+ return (_jsxs("form", { onSubmit: submit, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold", children: "Your password has expired" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: message || 'Please set a new password to continue.' })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rem-cur", children: "Current password" }), _jsx(Input, { id: "rem-cur", type: "password", autoComplete: "current-password", value: current, onChange: (e) => setCurrent(e.target.value), required: true })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rem-new", children: "New password" }), _jsx(Input, { id: "rem-new", type: "password", autoComplete: "new-password", value: next, onChange: (e) => setNext(e.target.value), required: true })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rem-conf", children: "Confirm new password" }), _jsx(Input, { id: "rem-conf", type: "password", autoComplete: "new-password", value: confirm, onChange: (e) => setConfirm(e.target.value), required: true })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null, _jsx(Button, { type: "submit", className: "w-full", disabled: busy, children: busy ? 'Updating…' : 'Change password & continue' }), _jsx(SignOutLink, {})] }));
58
+ }
59
+ function TotpQr({ uri }) {
60
+ const [error, setError] = useState(null);
61
+ const ref = (node) => {
62
+ if (!node || !uri)
63
+ return;
64
+ toCanvas(node, uri, { width: 184, margin: 1 }, (err) => { if (err)
65
+ setError(err.message); });
66
+ };
67
+ return (_jsx("div", { className: "flex justify-center rounded-md border bg-white p-3", children: error ? _jsx("span", { className: "text-sm text-destructive", children: error }) : _jsx("canvas", { ref: ref }) }));
68
+ }
69
+ function MfaEnrollForm({ message }) {
70
+ const { enrollTotp, verifyTotp, setRemediationRequired } = useAuth();
71
+ const [step, setStep] = useState('password');
72
+ const [password, setPassword] = useState('');
73
+ const [code, setCode] = useState('');
74
+ const [totpUri, setTotpUri] = useState('');
75
+ const [backupCodes, setBackupCodes] = useState([]);
76
+ const [busy, setBusy] = useState(false);
77
+ const [error, setError] = useState(null);
78
+ const start = async (e) => {
79
+ e.preventDefault();
80
+ setError(null);
81
+ setBusy(true);
82
+ try {
83
+ const { totpURI, backupCodes: codes } = await enrollTotp(password);
84
+ setTotpUri(totpURI);
85
+ setBackupCodes(codes ?? []);
86
+ setStep('verify');
87
+ }
88
+ catch (err) {
89
+ setError(err instanceof Error ? err.message : 'Could not start enrollment.');
90
+ }
91
+ finally {
92
+ setBusy(false);
93
+ }
94
+ };
95
+ const verify = async (e) => {
96
+ e.preventDefault();
97
+ setError(null);
98
+ setBusy(true);
99
+ try {
100
+ await verifyTotp(code.trim());
101
+ setRemediationRequired(null);
102
+ window.location.reload();
103
+ }
104
+ catch (err) {
105
+ setError(err instanceof Error ? err.message : 'Invalid code. Try again.');
106
+ setBusy(false);
107
+ }
108
+ };
109
+ if (step === 'password') {
110
+ return (_jsxs("form", { onSubmit: start, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold", children: "Set up two-factor authentication" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: message || 'Your organization requires an authenticator app to continue.' })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rem-pw", children: "Confirm your password" }), _jsx(Input, { id: "rem-pw", type: "password", autoComplete: "current-password", value: password, onChange: (e) => setPassword(e.target.value), required: true })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null, _jsx(Button, { type: "submit", className: "w-full", disabled: busy, children: busy ? 'Preparing…' : 'Continue' }), _jsx(SignOutLink, {})] }));
111
+ }
112
+ return (_jsxs("form", { onSubmit: verify, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold", children: "Scan with your authenticator" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Scan this QR code with Google Authenticator, 1Password, Authy, etc., then enter the 6-digit code." })] }), totpUri ? _jsx(TotpQr, { uri: totpUri }) : null, backupCodes.length > 0 ? (_jsxs("details", { className: "rounded-md border bg-muted/40 p-3 text-xs", children: [_jsx("summary", { className: "cursor-pointer font-medium", children: "Save your backup codes" }), _jsx("p", { className: "mt-1 text-muted-foreground", children: "Store these somewhere safe \u2014 each can be used once if you lose your device." }), _jsx("div", { className: "mt-2 grid grid-cols-2 gap-1 font-mono", children: backupCodes.map((c) => _jsx("span", { children: c }, c)) })] })) : null, _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rem-code", children: "6-digit code" }), _jsx(Input, { id: "rem-code", inputMode: "numeric", autoComplete: "one-time-code", maxLength: 8, placeholder: "123456", value: code, onChange: (e) => setCode(e.target.value), required: true })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null, _jsx(Button, { type: "submit", className: "w-full", disabled: busy, children: busy ? 'Verifying…' : 'Verify & continue' }), _jsx(SignOutLink, {})] }));
113
+ }
@@ -91,5 +91,41 @@ export declare function matchAiChatShortcut(e: {
91
91
  shiftKey: boolean;
92
92
  altKey: boolean;
93
93
  }): AiChatShortcut | null;
94
+ /** A composer submission held until the conversation id that will carry it exists. */
95
+ export interface PendingFirstMessage {
96
+ content: string;
97
+ files?: File[];
98
+ }
99
+ /**
100
+ * Guards the empty-state FIRST send against the conversation-id remount race.
101
+ *
102
+ * On a fresh `/ai/:agent` the composer (and its suggestion chips) go live the
103
+ * instant the agent resolves — BEFORE `POST /conversations` has minted the id.
104
+ * `<ChatPane>` is keyed on that id (`…:pending` → `…:<id>`), so the moment it
105
+ * resolves React UNMOUNTS the pane and mounts a fresh one. A first message
106
+ * submitted in that window lives inside the doomed pane: its optimistic bubble
107
+ * is discarded and the just-started `…/chat` request is aborted before it
108
+ * reaches the wire — so it vanishes silently (input cleared, no bubble, no
109
+ * error). Distinct from the #2047 path, where `…/chat` WAS sent and failed.
110
+ *
111
+ * The fix: when we have a server endpoint but no id yet, stash the send in a ref
112
+ * the PAGE owns (so it outlives the pane remount) and replay it the moment an id
113
+ * exists — in the freshly-mounted pane (or in place if no remount happened). A
114
+ * send made once the id is present (the normal path, and every send after the
115
+ * first) goes straight through. Local/echo mode (no `chatApi`) also sends
116
+ * immediately, so the offline-demo bot keeps responding.
117
+ *
118
+ * Exported for unit testing.
119
+ */
120
+ export declare function useDeferredFirstSend(opts: {
121
+ /** The chat endpoint — defined once an agent is resolved (server-backed mode). */
122
+ chatApi: string | undefined;
123
+ /** The conversation id; undefined while `POST /conversations` is in flight. */
124
+ conversationId: string | undefined;
125
+ /** Page-owned stash that OUTLIVES the keyed `<ChatPane>` remount. */
126
+ pendingRef: React.MutableRefObject<PendingFirstMessage | null>;
127
+ /** The real send (resetSuppression + sendMessage + onSent). */
128
+ doSend: (content: string, files?: File[]) => void;
129
+ }): (content: string, files?: File[]) => void;
94
130
  export declare function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentProp }?: AiChatPageProps): React.JSX.Element;
95
131
  export default AiChatPage;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
3
3
  /**
4
4
  * AiChatPage — full-page ChatGPT-style AI surface.
@@ -17,7 +17,7 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
17
17
  import { useAuth } from '@object-ui/auth';
18
18
  import { useObjectTranslation } from '@object-ui/i18n';
19
19
  import { toast } from 'sonner';
20
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button, ShareDialog, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Empty, EmptyTitle, EmptyDescription, cn, } from '@object-ui/components';
20
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tabs, TabsList, TabsTrigger, Button, ShareDialog, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Empty, EmptyTitle, EmptyDescription, cn, } from '@object-ui/components';
21
21
  import { Bug, PanelLeft, PanelLeftClose, PanelLeftOpen, Share2 } from 'lucide-react';
22
22
  import { ChatbotEnhanced, useAgents, useObjectChat, useAiModels, useHitlInChat, resolveDefaultAgentName, PLATFORM_DEFAULT_AGENT, agentRouteName, resolveAgentParam, isBuiltinAgentName, isBuildAgent, isAskAgent, publishHealthFromResponse, detectDraftResult, detectProposedPlan, buildProgressFromDraftReview, } from '@object-ui/plugin-chatbot';
23
23
  import { AppHeader } from '../../layout/AppHeader';
@@ -347,6 +347,55 @@ export function matchAiChatShortcut(e) {
347
347
  return null;
348
348
  }
349
349
  }
350
+ /**
351
+ * Guards the empty-state FIRST send against the conversation-id remount race.
352
+ *
353
+ * On a fresh `/ai/:agent` the composer (and its suggestion chips) go live the
354
+ * instant the agent resolves — BEFORE `POST /conversations` has minted the id.
355
+ * `<ChatPane>` is keyed on that id (`…:pending` → `…:<id>`), so the moment it
356
+ * resolves React UNMOUNTS the pane and mounts a fresh one. A first message
357
+ * submitted in that window lives inside the doomed pane: its optimistic bubble
358
+ * is discarded and the just-started `…/chat` request is aborted before it
359
+ * reaches the wire — so it vanishes silently (input cleared, no bubble, no
360
+ * error). Distinct from the #2047 path, where `…/chat` WAS sent and failed.
361
+ *
362
+ * The fix: when we have a server endpoint but no id yet, stash the send in a ref
363
+ * the PAGE owns (so it outlives the pane remount) and replay it the moment an id
364
+ * exists — in the freshly-mounted pane (or in place if no remount happened). A
365
+ * send made once the id is present (the normal path, and every send after the
366
+ * first) goes straight through. Local/echo mode (no `chatApi`) also sends
367
+ * immediately, so the offline-demo bot keeps responding.
368
+ *
369
+ * Exported for unit testing.
370
+ */
371
+ export function useDeferredFirstSend(opts) {
372
+ const { chatApi, conversationId, pendingRef, doSend } = opts;
373
+ const apiMode = Boolean(chatApi);
374
+ // Replay a deferred first message the instant a conversation id exists — in
375
+ // the freshly-mounted pane after the remount, or in place if none happened.
376
+ // Clearing the ref before sending makes the replay fire at most once even
377
+ // though `doSend` (and StrictMode's double-invoke) re-run this effect.
378
+ useEffect(() => {
379
+ if (!apiMode || !conversationId)
380
+ return;
381
+ const pending = pendingRef.current;
382
+ if (!pending)
383
+ return;
384
+ pendingRef.current = null;
385
+ doSend(pending.content, pending.files);
386
+ }, [apiMode, conversationId, pendingRef, doSend]);
387
+ return useCallback((content, files) => {
388
+ // Server-backed, but the id is still being minted: sending now would be
389
+ // lost — a convId-less `/chat` the server won't persist, or a request torn
390
+ // down when the pane remounts as the id resolves. Stash it; the effect
391
+ // above replays it once the id lands.
392
+ if (apiMode && !conversationId) {
393
+ pendingRef.current = { content, files };
394
+ return;
395
+ }
396
+ doSend(content, files);
397
+ }, [apiMode, conversationId, pendingRef, doSend]);
398
+ }
350
399
  export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentProp } = {}) {
351
400
  const { user } = useAuth();
352
401
  const { t } = useObjectTranslation();
@@ -587,6 +636,11 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro
587
636
  return;
588
637
  setTitleHints((current) => current[conversationId] === hint ? current : { ...current, [conversationId]: hint });
589
638
  }, [conversationId, initialMessages]);
639
+ // Holds an empty-state first message submitted before the conversation id was
640
+ // minted. Owned by the PAGE (not the keyed <ChatPane>) so it survives the
641
+ // remount that the id-resolution triggers; the freshly-mounted pane replays it
642
+ // via useDeferredFirstSend. See that hook for the full race.
643
+ const pendingFirstMessageRef = useRef(null);
590
644
  const handleSent = useCallback((firstUserMessage) => {
591
645
  // New user turn → bump sidebar list so the row's preview/timestamp refreshes.
592
646
  setRefreshKey((k) => k + 1);
@@ -612,11 +666,11 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro
612
666
  void t1;
613
667
  void t2;
614
668
  }, [conversationId]);
615
- return (_jsxs("div", { className: "flex h-svh w-full flex-col bg-background", "data-testid": "ai-chat-page", children: [_jsxs("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background/95 px-2 backdrop-blur sm:px-4", children: [!noAgents && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 md:hidden", onClick: () => setMobileChatsOpen(true), "aria-label": t('console.ai.openChats'), "data-testid": "ai-chat-mobile-sidebar-trigger", children: _jsx(PanelLeft, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "icon", className: "hidden h-8 w-8 shrink-0 md:inline-flex", onClick: toggleChatsCollapsed, "aria-label": chatsCollapsed
616
- ? t('console.ai.showChats', { defaultValue: 'Show chats' })
617
- : t('console.ai.hideChats', { defaultValue: 'Hide chats' }), title: chatsCollapsed
618
- ? t('console.ai.showChats', { defaultValue: 'Show chats' })
619
- : t('console.ai.hideChats', { defaultValue: 'Hide chats' }), "data-testid": "ai-chat-collapse-sidebar-trigger", "aria-pressed": chatsCollapsed, children: chatsCollapsed ? _jsx(PanelLeftOpen, { className: "h-4 w-4" }) : _jsx(PanelLeftClose, { className: "h-4 w-4" }) })] })), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(AppHeader, { variant: "home" }) })] }), noAgents ? (_jsx(AiUnavailable, { hasError: Boolean(agentsError), onRetry: refetchAgents, onHome: () => navigate('/home'), t: t })) : (_jsxs(_Fragment, { children: [_jsx(Sheet, { open: mobileChatsOpen, onOpenChange: setMobileChatsOpen, children: _jsxs(SheetContent, { side: "left", className: "w-[320px] p-0 sm:max-w-[360px]", "data-testid": "ai-chat-mobile-sidebar", children: [_jsxs(SheetHeader, { className: "sr-only", children: [_jsx(SheetTitle, { children: t('console.ai.chats') }), _jsx(SheetDescription, { children: t('console.ai.chatsDescription') })] }), _jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, activeAgent: activeAgent, refreshKey: refreshKey, titleHints: titleHints, className: "h-full border-r-0", onNavigate: () => setMobileChatsOpen(false) })] }) }), conversationId && (_jsx(ShareDialog, { open: shareOpen, onOpenChange: setShareOpen, objectName: "ai_conversations", recordId: conversationId, recordLabel: "this conversation", apiBase: restApiBase, publicBaseUrl: publicShareBase })), conversationId && isBuildAgent(activeAgent) && (_jsx(BuildDebugDrawer, { apiBase: apiBase, conversationId: conversationId, open: debugOpen, onOpenChange: setDebugOpen })), _jsxs("div", { className: "flex min-h-0 flex-1 w-full bg-muted/20", children: [!chatsCollapsed && (_jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, activeAgent: activeAgent, refreshKey: refreshKey, titleHints: titleHints, className: "hidden w-72 shrink-0 border-r md:flex" })), _jsx("main", { className: "flex min-w-0 flex-1 flex-col", children: _jsx(ChatPane, { agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, chatApi: chatApi, apiBase: apiBase, conversationId: conversationId, editPackageId: editPackageId, initialMessages: initialMessages, onSent: handleSent, onShare: () => setShareOpen(true), onDebug: () => setDebugOpen(true), showDebug: isBuildAgent(activeAgent), onCanvasOpenChange: handleCanvasOpenChange }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`) })] })] }))] }));
669
+ return (_jsxs("div", { className: "flex h-svh w-full flex-col bg-background", "data-testid": "ai-chat-page", children: [_jsxs("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background/95 px-2 backdrop-blur sm:px-4", children: [!noAgents && (_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 md:hidden", onClick: () => setMobileChatsOpen(true), "aria-label": t('console.ai.openChats'), "data-testid": "ai-chat-mobile-sidebar-trigger", children: _jsx(PanelLeft, { className: "h-4 w-4" }) })), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(AppHeader, { variant: "home" }) })] }), noAgents ? (_jsx(AiUnavailable, { hasError: Boolean(agentsError), onRetry: refetchAgents, onHome: () => navigate('/home'), t: t })) : (_jsxs(_Fragment, { children: [_jsx(Sheet, { open: mobileChatsOpen, onOpenChange: setMobileChatsOpen, children: _jsxs(SheetContent, { side: "left", className: "w-[320px] p-0 sm:max-w-[360px]", "data-testid": "ai-chat-mobile-sidebar", children: [_jsxs(SheetHeader, { className: "sr-only", children: [_jsx(SheetTitle, { children: t('console.ai.chats') }), _jsx(SheetDescription, { children: t('console.ai.chatsDescription') })] }), _jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, activeAgent: activeAgent, refreshKey: refreshKey, titleHints: titleHints, className: "h-full border-r-0", onNavigate: () => setMobileChatsOpen(false) })] }) }), conversationId && (_jsx(ShareDialog, { open: shareOpen, onOpenChange: setShareOpen, objectName: "ai_conversations", recordId: conversationId, recordLabel: "this conversation", apiBase: restApiBase, publicBaseUrl: publicShareBase })), conversationId && isBuildAgent(activeAgent) && (_jsx(BuildDebugDrawer, { apiBase: apiBase, conversationId: conversationId, open: debugOpen, onOpenChange: setDebugOpen })), _jsxs("div", { className: "flex min-h-0 flex-1 w-full bg-background", children: [_jsxs("div", { className: "hidden shrink-0 flex-col border-r md:flex", children: [!chatsCollapsed && (_jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, activeAgent: activeAgent, refreshKey: refreshKey, titleHints: titleHints, className: "w-72 min-h-0 flex-1 border-r-0" })), _jsx("div", { className: cn('mt-auto p-2', chatsCollapsed ? 'w-12' : 'w-72 border-t'), children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8", onClick: toggleChatsCollapsed, "aria-label": chatsCollapsed
670
+ ? t('console.ai.showChats', { defaultValue: 'Show chats' })
671
+ : t('console.ai.hideChats', { defaultValue: 'Hide chats' }), title: chatsCollapsed
672
+ ? t('console.ai.showChats', { defaultValue: 'Show chats' })
673
+ : t('console.ai.hideChats', { defaultValue: 'Hide chats' }), "data-testid": "ai-chat-collapse-sidebar-trigger", "aria-pressed": chatsCollapsed, children: chatsCollapsed ? _jsx(PanelLeftOpen, { className: "h-4 w-4" }) : _jsx(PanelLeftClose, { className: "h-4 w-4" }) }) })] }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col", children: _jsx(ChatPane, { agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, chatApi: chatApi, apiBase: apiBase, conversationId: conversationId, editPackageId: editPackageId, initialMessages: initialMessages, pendingFirstMessageRef: pendingFirstMessageRef, onSent: handleSent, onShare: () => setShareOpen(true), onDebug: () => setDebugOpen(true), showDebug: isBuildAgent(activeAgent), onCanvasOpenChange: handleCanvasOpenChange }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`) })] })] }))] }));
620
674
  }
621
675
  /**
622
676
  * Graceful state for `/ai` when the agent catalog resolved empty — shown
@@ -634,7 +688,7 @@ function AiUnavailable({ hasError, onRetry, onHome, t, }) {
634
688
  defaultValue: "This deployment doesn't have an AI assistant enabled. Everything else works as usual.",
635
689
  }) }), _jsxs("div", { className: "mt-6 flex flex-col items-center gap-3 sm:flex-row", children: [hasError && (_jsx(Button, { variant: "outline", onClick: onRetry, "data-testid": "ai-unavailable-retry", children: t('console.ai.unavailableRetry', { defaultValue: 'Try again' }) })), _jsx(Button, { onClick: onHome, "data-testid": "ai-unavailable-home", children: t('console.ai.unavailableHome', { defaultValue: 'Back to home' }) })] })] }) }));
636
690
  }
637
- function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, apiBase, conversationId, editPackageId, initialMessages, onSent, onShare, onDebug, showDebug, onCanvasOpenChange, }) {
691
+ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, apiBase, conversationId, editPackageId, initialMessages, pendingFirstMessageRef, onSent, onShare, onDebug, showDebug, onCanvasOpenChange, }) {
638
692
  const { t } = useObjectTranslation();
639
693
  const navigate = useNavigate();
640
694
  // The agent dropdown is a LAUNCHER now (not an in-surface mode toggle): it
@@ -755,12 +809,24 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
755
809
  sendMessage(prompt);
756
810
  },
757
811
  });
758
- const handleSend = useCallback((content, files) => {
812
+ // The real send, shared by a normal submit and by the deferred-first-message
813
+ // replay below (resetSuppression → send → onSent, in that order).
814
+ const doSend = useCallback((content, files) => {
759
815
  resetSuppression();
760
816
  sendMessage(content, files);
761
817
  onSent(content);
762
- }, [sendMessage, onSent]);
763
- const headerSlot = (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 border-b border-border/50 px-4 pb-2 pt-3 sm:px-6", children: [_jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: showAgentLauncher ? (_jsxs(Select, { value: activeAgent, onValueChange: (name) => navigate(`/ai/${agentRouteName(name)}`), disabled: agentsLoading, children: [_jsx(SelectTrigger, { className: "h-7 w-auto min-w-0 border-0 bg-transparent px-1.5 text-xs shadow-none hover:bg-accent focus:ring-0 focus:ring-offset-0 focus-visible:ring-1 focus-visible:ring-border/80 focus-visible:ring-offset-0 sm:min-w-[160px]", "data-testid": "ai-chat-agent-picker", "aria-label": t('console.ai.switchAssistant', { defaultValue: 'Switch assistant' }), children: _jsx(SelectValue, { placeholder: t('console.ai.chooseAgent', { defaultValue: 'Choose assistant…' }) }) }), _jsx(SelectContent, { align: "start", children: agents.map((agent) => (_jsxs(SelectItem, { value: agent.name, className: "text-xs", children: [_jsx("span", { className: "font-medium", children: localizeAgentLabel(t, agent.name, agent.label) }), agent.description ? (_jsx("span", { className: "block text-muted-foreground text-[10px] truncate max-w-[260px]", children: agent.description })) : null] }, agent.name))) })] })) : (_jsx("span", { className: "truncate text-xs font-medium text-foreground/85", children: activeAgentLabel })) }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [showDebug && onDebug ? (_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7 text-muted-foreground hover:text-foreground", onClick: onDebug, disabled: !conversationId, "aria-label": "Build Doctor", "data-testid": "ai-chat-debug-button", title: conversationId ? 'Build Doctor — what actually landed?' : 'Send a message first', children: _jsx(Bug, { className: "h-3.5 w-3.5" }) })) : null, _jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7 text-muted-foreground hover:text-foreground", onClick: onShare, disabled: !conversationId, "aria-label": t('console.ai.share'), "data-testid": "ai-chat-share-button", title: conversationId ? t('console.ai.shareTitle') : t('console.ai.shareDisabledTitle'), children: _jsx(Share2, { className: "h-3.5 w-3.5" }) })] }), agentsError ? (_jsx("span", { className: "basis-full text-[10px] text-amber-700 dark:text-amber-400", title: agentsError.message, children: t('console.ai.offlineDemoMode') })) : null] }));
818
+ }, [resetSuppression, sendMessage, onSent]);
819
+ // Guards the empty-state first send against the conversation-id remount race:
820
+ // a send made before the id is minted is stashed in the page-owned ref and
821
+ // replayed once the id lands, so the magic-moment first message reliably
822
+ // reaches `…/chat` instead of being dropped. See useDeferredFirstSend.
823
+ const handleSend = useDeferredFirstSend({
824
+ chatApi,
825
+ conversationId,
826
+ pendingRef: pendingFirstMessageRef,
827
+ doSend,
828
+ });
829
+ const headerSlot = (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 border-b border-border/50 px-4 pb-2 pt-3 sm:px-6", children: [_jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: showAgentLauncher ? (agents.length <= 3 ? (_jsx(Tabs, { value: activeAgent, onValueChange: (name) => navigate(`/ai/${agentRouteName(name)}`), children: _jsx(TabsList, { className: "h-7 gap-0.5 p-0.5", "data-testid": "ai-chat-agent-picker", "aria-label": t('console.ai.switchAssistant', { defaultValue: 'Switch assistant' }), children: agents.map((agent) => (_jsx(TabsTrigger, { value: agent.name, disabled: agentsLoading, title: agent.description || undefined, className: "h-6 px-2.5 text-xs", children: localizeAgentLabel(t, agent.name, agent.label) }, agent.name))) }) })) : (_jsxs(Select, { value: activeAgent, onValueChange: (name) => navigate(`/ai/${agentRouteName(name)}`), disabled: agentsLoading, children: [_jsx(SelectTrigger, { className: "h-7 w-auto min-w-0 border-0 bg-transparent px-1.5 text-xs shadow-none hover:bg-accent focus:ring-0 focus:ring-offset-0 focus-visible:ring-1 focus-visible:ring-border/80 focus-visible:ring-offset-0 sm:min-w-[160px]", "data-testid": "ai-chat-agent-picker", "aria-label": t('console.ai.switchAssistant', { defaultValue: 'Switch assistant' }), children: _jsx(SelectValue, { placeholder: t('console.ai.chooseAgent', { defaultValue: 'Choose assistant…' }) }) }), _jsx(SelectContent, { align: "start", children: agents.map((agent) => (_jsxs(SelectItem, { value: agent.name, className: "text-xs", children: [_jsx("span", { className: "font-medium", children: localizeAgentLabel(t, agent.name, agent.label) }), agent.description ? (_jsx("span", { className: "block text-muted-foreground text-[10px] truncate max-w-[260px]", children: agent.description })) : null] }, agent.name))) })] }))) : (_jsx("span", { className: "truncate text-xs font-medium text-foreground/85", children: activeAgentLabel })) }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [showDebug && onDebug ? (_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7 text-muted-foreground hover:text-foreground", onClick: onDebug, disabled: !conversationId, "aria-label": "Build Doctor", "data-testid": "ai-chat-debug-button", title: conversationId ? 'Build Doctor — what actually landed?' : 'Send a message first', children: _jsx(Bug, { className: "h-3.5 w-3.5" }) })) : null, _jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7 text-muted-foreground hover:text-foreground", onClick: onShare, disabled: !conversationId, "aria-label": t('console.ai.share'), "data-testid": "ai-chat-share-button", title: conversationId ? t('console.ai.shareTitle') : t('console.ai.shareDisabledTitle'), children: _jsx(Share2, { className: "h-3.5 w-3.5" }) })] }), agentsError ? (_jsx("span", { className: "basis-full text-[10px] text-amber-700 dark:text-amber-400", title: agentsError.message, children: t('console.ai.offlineDemoMode') })) : null] }));
764
830
  return (_jsxs("div", { ref: split.containerRef, className: "relative flex min-h-0 flex-1 px-0", children: [_jsx("div", { "data-chat-column": true, className: canvasApp
765
831
  ? 'flex min-h-0 shrink-0 justify-center'
766
832
  : 'flex min-h-0 flex-1 justify-center', style: canvasApp ? { width: split.width } : undefined, children: _jsx(ChatbotEnhanced, { className: "min-h-0 flex-1 bg-background md:max-w-5xl", onUpgrade: () => window.open(cloudPricingDeepLink(), '_blank', 'noopener,noreferrer'), surface: "plain", maxHeight: "100%", headerSlot: headerSlot, messages: messages, placeholder: activeAgent
@@ -784,6 +850,23 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
784
850
  connectionWaiting: t('console.ai.connectionWaiting', { defaultValue: 'Waiting for server…' }),
785
851
  connectionStalledLabel: t('console.ai.connectionStalled', { defaultValue: 'Still working…' }),
786
852
  connectionOfflineLabel: t('console.ai.connectionOffline', { defaultValue: 'Connection lost — reconnecting…' }),
853
+ // Friendly in-progress feedback for the long, atomic propose_blueprint
854
+ // call — a lead-in plus rotating hints so the wait reads as deliberate
855
+ // design work, not a hang. Each hint is translated individually (the
856
+ // established per-string pattern; avoids an i18n returnObjects array).
857
+ designingPlanLabel: t('console.ai.designingPlan', { defaultValue: 'Designing your app…' }),
858
+ designingPlanHints: [
859
+ t('console.ai.designingPlanHint.data', { defaultValue: 'Mapping out the data you’ll track…' }),
860
+ t('console.ai.designingPlanHint.objects', { defaultValue: 'Shaping objects and their fields…' }),
861
+ t('console.ai.designingPlanHint.relations', { defaultValue: 'Connecting related records…' }),
862
+ t('console.ai.designingPlanHint.lookups', { defaultValue: 'Setting up relationships and lookups…' }),
863
+ t('console.ai.designingPlanHint.views', { defaultValue: 'Planning the screens and views…' }),
864
+ t('console.ai.designingPlanHint.forms', { defaultValue: 'Laying out forms and lists…' }),
865
+ t('console.ai.designingPlanHint.defaults', { defaultValue: 'Adding sensible defaults and validations…' }),
866
+ t('console.ai.designingPlanHint.dashboard', { defaultValue: 'Sketching a dashboard to track it…' }),
867
+ t('console.ai.designingPlanHint.review', { defaultValue: 'Double-checking the structure hangs together…' }),
868
+ t('console.ai.designingPlanHint.finalize', { defaultValue: 'Pulling the plan together…' }),
869
+ ],
787
870
  toolDetailsHidden: t('console.ai.toolDetailsHidden'),
788
871
  copy: t('console.ai.copy'),
789
872
  copied: t('console.ai.copied'),
@@ -792,6 +875,12 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
792
875
  submit: t('console.ai.submit'),
793
876
  uploadFiles: t('console.ai.uploadFiles'),
794
877
  stopResponse: t('console.ai.stopResponse'),
878
+ sendFailedRateLimited: t('console.ai.sendFailedRateLimited', {
879
+ defaultValue: "You're sending messages too quickly. Your message is kept below — wait a moment and try again.",
880
+ }),
881
+ sendFailedGeneric: t('console.ai.sendFailedGeneric', {
882
+ defaultValue: "Couldn't send your message. It's kept below — please try again.",
883
+ }),
795
884
  trace: t('console.ai.trace'),
796
885
  viewTrace: t('console.ai.viewTrace'),
797
886
  },
@@ -851,9 +940,11 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
851
940
  });
852
941
  return false;
853
942
  }
854
- }, publishDraftsLabel: t('console.ai.publishDrafts', { defaultValue: 'Publish' }), publishedLabel: t('console.ai.published', { defaultValue: 'Published' }), nextStepsLabel: t('console.ai.nextSteps', { defaultValue: "What's next" }), planTitleLabel: t('console.ai.planTitle', { defaultValue: 'Proposed plan' }), planQuestionsLabel: t('console.ai.planQuestions', { defaultValue: 'Confirm before building' }), planAssumptionsLabel: t('console.ai.planAssumptions', { defaultValue: 'Assumptions' }), planApproveHintLabel: t('console.ai.planApproveHint', {
943
+ }, publishDraftsLabel: t('console.ai.publishDrafts', { defaultValue: 'Publish' }), publishedLabel: t('console.ai.published', { defaultValue: 'Published' }), nextStepsLabel: t('console.ai.nextSteps', { defaultValue: "What's next" }), planTitleLabel: t('console.ai.planTitle', { defaultValue: 'Proposed plan' }), planQuestionsLabel: t('console.ai.planQuestions', { defaultValue: 'Confirm before building' }), planAssumptionsLabel: t('console.ai.planAssumptions', { defaultValue: 'Assumptions' }), planDeferredLabel: t('console.ai.planDeferred', { defaultValue: 'Not yet built' }), planApproveHintLabel: t('console.ai.planApproveHint', {
855
944
  defaultValue: 'Reply to approve or adjust this plan.',
856
- }), planApproveLabel: t('console.ai.planApprove', { defaultValue: 'Build it' }), planAdjustLabel: t('console.ai.planAdjust', { defaultValue: 'Adjust' }), planBuiltLabel: t('console.ai.planBuilt', { defaultValue: 'Built' }), planApproveMessage: t('console.ai.planApproveMessage', {
945
+ }), planApproveLabel: t('console.ai.planApprove', { defaultValue: 'Build it' }), planAdjustLabel: t('console.ai.planAdjust', { defaultValue: 'Adjust' }), planBuiltLabel: t('console.ai.planBuilt', { defaultValue: 'Built' }), planReadyLabel: t('console.ai.planReady', {
946
+ defaultValue: 'The plan is ready. Build it now, or tell me what to adjust.',
947
+ }), planApproveMessage: t('console.ai.planApproveMessage', {
857
948
  defaultValue: 'Looks good — build it as proposed.',
858
949
  }), planApproveDefaultsMessage: t('console.ai.planApproveDefaultsMessage', {
859
950
  defaultValue: 'Build it with your best assumptions; use sensible defaults for the open questions.',
@@ -0,0 +1,10 @@
1
+ interface CloudOnboardingNextProps {
2
+ properties?: {
3
+ /** Backend SSO-open endpoint (full-page nav that 302s into the prod env). */
4
+ openProductionUrl?: string;
5
+ /** SPA route to the environments list (create / open / manage). */
6
+ environmentsRoute?: string;
7
+ };
8
+ }
9
+ export declare function CloudOnboardingNext({ properties }: CloudOnboardingNextProps): import("react").JSX.Element;
10
+ export {};