@object-ui/app-shell 7.0.0 → 7.2.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 (141) hide show
  1. package/CHANGELOG.md +560 -0
  2. package/dist/console/AppContent.js +23 -17
  3. package/dist/console/ConsoleShell.d.ts +16 -0
  4. package/dist/console/ConsoleShell.js +43 -2
  5. package/dist/console/ai/AiChatPage.js +47 -16
  6. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  7. package/dist/console/ai/LiveCanvas.js +6 -4
  8. package/dist/console/home/HomeLayout.js +5 -7
  9. package/dist/console/home/HomePage.js +1 -9
  10. package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
  11. package/dist/console/organizations/OrganizationsPage.js +22 -3
  12. package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
  13. package/dist/console/organizations/provisionEnvironment.js +64 -0
  14. package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
  15. package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
  16. package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
  17. package/dist/environment/EnvironmentListToolbar.js +59 -0
  18. package/dist/environment/entitlements.d.ts +90 -0
  19. package/dist/environment/entitlements.js +91 -0
  20. package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
  21. package/dist/environment/useEnvironmentEntitlements.js +108 -0
  22. package/dist/hooks/useActionModal.js +15 -1
  23. package/dist/hooks/useAiSurface.d.ts +59 -0
  24. package/dist/hooks/useAiSurface.js +78 -0
  25. package/dist/hooks/useChatConversation.d.ts +30 -0
  26. package/dist/hooks/useChatConversation.js +63 -0
  27. package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
  28. package/dist/hooks/useConsoleActionRuntime.js +42 -10
  29. package/dist/index.d.ts +5 -2
  30. package/dist/index.js +10 -2
  31. package/dist/layout/AppHeader.js +28 -4
  32. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  33. package/dist/layout/ConsoleFloatingChatbot.js +41 -10
  34. package/dist/layout/ConsoleLayout.js +5 -6
  35. package/dist/layout/ContextSelectors.js +59 -35
  36. package/dist/layout/agentPicker.d.ts +56 -0
  37. package/dist/layout/agentPicker.js +40 -0
  38. package/dist/preview/CommitTimeline.d.ts +15 -0
  39. package/dist/preview/CommitTimeline.js +82 -0
  40. package/dist/preview/DraftPreviewBar.js +20 -7
  41. package/dist/preview/UnpublishedAppBar.js +11 -7
  42. package/dist/preview/commitHistory.d.ts +28 -0
  43. package/dist/preview/commitHistory.js +48 -0
  44. package/dist/providers/ExpressionProvider.js +9 -3
  45. package/dist/providers/MetadataProvider.js +9 -0
  46. package/dist/utils/index.d.ts +2 -2
  47. package/dist/utils/index.js +1 -1
  48. package/dist/utils/recordFormNavigation.d.ts +60 -0
  49. package/dist/utils/recordFormNavigation.js +35 -0
  50. package/dist/utils/resolvePageVarTokens.d.ts +31 -0
  51. package/dist/utils/resolvePageVarTokens.js +72 -0
  52. package/dist/views/CreateViewDialog.js +14 -1
  53. package/dist/views/FlowRunner.d.ts +2 -30
  54. package/dist/views/FlowRunner.js +18 -50
  55. package/dist/views/ObjectView.js +26 -12
  56. package/dist/views/ScreenView.d.ts +70 -0
  57. package/dist/views/ScreenView.js +73 -0
  58. package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
  59. package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
  60. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  61. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  62. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  63. package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
  64. package/dist/views/metadata-admin/PackagesPage.js +58 -5
  65. package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
  66. package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
  67. package/dist/views/metadata-admin/ResourceListPage.js +28 -19
  68. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  69. package/dist/views/metadata-admin/anchors.js +20 -2
  70. package/dist/views/metadata-admin/createBody.d.ts +26 -0
  71. package/dist/views/metadata-admin/createBody.js +30 -0
  72. package/dist/views/metadata-admin/i18n.js +108 -2
  73. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
  74. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
  75. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
  76. package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
  77. package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
  78. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
  79. package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
  80. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
  81. package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
  82. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
  83. package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
  84. package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
  85. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  86. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  87. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  88. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  89. package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
  90. package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
  91. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  92. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  93. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  94. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
  95. package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
  96. package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
  97. package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
  98. package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
  99. package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
  100. package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
  101. package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
  102. package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
  103. package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
  104. package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
  105. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  106. package/dist/views/metadata-admin/issuePath.js +65 -0
  107. package/dist/views/metadata-admin/package-scope.d.ts +41 -0
  108. package/dist/views/metadata-admin/package-scope.js +59 -0
  109. package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
  110. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  111. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
  112. package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
  113. package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
  114. package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
  115. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  116. package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
  117. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  118. package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
  119. package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
  120. package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
  121. package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
  122. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  123. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  124. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  125. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  126. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
  127. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
  128. package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
  129. package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
  130. package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
  131. package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
  132. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  133. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  134. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  135. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  136. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
  137. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  138. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
  139. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  140. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  141. package/package.json +38 -38
@@ -136,6 +136,36 @@ interface ServerConversation {
136
136
  messages?: ServerMessage[];
137
137
  }
138
138
  export declare function toUIMessages(rows: ServerMessage[] | undefined): HydratedUIMessage[];
139
+ /**
140
+ * A FLAT `ai_messages` row as returned by the public share endpoint
141
+ * (`GET /api/v1/share-links/:token/messages`), which streams the raw object
142
+ * rows rather than the reconstructed ModelMessage history that the
143
+ * authenticated `GET /conversations/:id` returns.
144
+ */
145
+ export interface RawAiMessageRow {
146
+ id?: string;
147
+ role: string;
148
+ /** Persisted text (assistant/user/system) OR a JSON-stringified tool-result array (tool role). */
149
+ content?: unknown;
150
+ /** Assistant tool CALLS, JSON-stringified (`tool-call` parts). */
151
+ tool_calls?: string | null;
152
+ /** Tool RESULT's owning call id (legacy plain-string tool rows). */
153
+ tool_call_id?: string | null;
154
+ }
155
+ /**
156
+ * Reconstruct the ModelMessage-shaped `content` that {@link toUIMessages}
157
+ * expects from the FLAT columns the public share endpoint returns raw.
158
+ *
159
+ * The authenticated path gets this reconstruction server-side
160
+ * (`ObjqlConversationService.toMessage`): an assistant turn's tool CALLS live
161
+ * in the separate `tool_calls` column, and a `tool` row's RESULTS are a
162
+ * JSON-stringified array in `content`. The share endpoint skips that step and
163
+ * dumps the rows verbatim — so the shared transcript previously rendered the
164
+ * raw `{"type":"tool-result",…}` envelope as text instead of a card. Mirroring
165
+ * `toMessage` here lets the share page reuse the exact same hydrate → render
166
+ * pipeline as the live chat. Keep this in lockstep with `toMessage`.
167
+ */
168
+ export declare function aiMessageRowsToServerMessages(rows: RawAiMessageRow[] | undefined): ServerMessage[];
139
169
  export declare function fetchConversation(apiBase: string, id: string): Promise<ServerConversation | null>;
140
170
  export declare function useChatConversation(options: UseChatConversationOptions): UseChatConversationReturn;
141
171
  export {};
@@ -267,6 +267,69 @@ export function toUIMessages(rows) {
267
267
  });
268
268
  return out;
269
269
  }
270
+ function safeParseArray(value) {
271
+ if (typeof value !== 'string' || !value)
272
+ return undefined;
273
+ try {
274
+ const parsed = JSON.parse(value);
275
+ return Array.isArray(parsed) ? parsed : undefined;
276
+ }
277
+ catch {
278
+ return undefined;
279
+ }
280
+ }
281
+ /**
282
+ * Reconstruct the ModelMessage-shaped `content` that {@link toUIMessages}
283
+ * expects from the FLAT columns the public share endpoint returns raw.
284
+ *
285
+ * The authenticated path gets this reconstruction server-side
286
+ * (`ObjqlConversationService.toMessage`): an assistant turn's tool CALLS live
287
+ * in the separate `tool_calls` column, and a `tool` row's RESULTS are a
288
+ * JSON-stringified array in `content`. The share endpoint skips that step and
289
+ * dumps the rows verbatim — so the shared transcript previously rendered the
290
+ * raw `{"type":"tool-result",…}` envelope as text instead of a card. Mirroring
291
+ * `toMessage` here lets the share page reuse the exact same hydrate → render
292
+ * pipeline as the live chat. Keep this in lockstep with `toMessage`.
293
+ */
294
+ export function aiMessageRowsToServerMessages(rows) {
295
+ if (!rows)
296
+ return [];
297
+ return rows.map((row) => {
298
+ const id = row.id;
299
+ const text = typeof row.content === 'string' ? row.content : '';
300
+ if (row.role === 'assistant') {
301
+ const toolCalls = safeParseArray(row.tool_calls);
302
+ if (toolCalls && toolCalls.length > 0) {
303
+ const content = [];
304
+ if (text)
305
+ content.push({ type: 'text', text });
306
+ content.push(...toolCalls);
307
+ return { id, role: 'assistant', content };
308
+ }
309
+ return { id, role: 'assistant', content: text };
310
+ }
311
+ if (row.role === 'tool') {
312
+ const results = safeParseArray(row.content);
313
+ if (results && results.length > 0 && results[0]?.type === 'tool-result') {
314
+ return { id, role: 'tool', content: results };
315
+ }
316
+ // Back-compat: pre-array tool rows persisted a plain string.
317
+ return {
318
+ id,
319
+ role: 'tool',
320
+ content: [
321
+ {
322
+ type: 'tool-result',
323
+ toolCallId: row.tool_call_id ?? '',
324
+ toolName: 'unknown',
325
+ output: { type: 'text', value: text },
326
+ },
327
+ ],
328
+ };
329
+ }
330
+ return { id, role: row.role, content: text };
331
+ });
332
+ }
270
333
  export async function fetchConversation(apiBase, id) {
271
334
  const res = await fetch(`${apiBase}/conversations/${encodeURIComponent(id)}`, {
272
335
  credentials: 'include',
@@ -23,6 +23,7 @@
23
23
  import React from 'react';
24
24
  import { createAuthenticatedFetch } from '@object-ui/auth';
25
25
  import type { ActionContext, ActionDef, ActionResult, ConfirmationHandler, NavigationHandler, ParamCollectionHandler, ResultDialogHandler, ToastHandler } from '@object-ui/core';
26
+ import { type EntitlementDialogSpec } from '../environment/entitlements';
26
27
  export interface ConsoleActionRuntimeOptions {
27
28
  /** Adapter for generic CRUD / execute calls. */
28
29
  dataSource: any;
@@ -45,6 +46,8 @@ export interface ConsoleActionRuntime {
45
46
  serverActionHandler: (action: ActionDef, context?: ActionContext) => Promise<ActionResult>;
46
47
  /** Authenticated fetch wrapper (Bearer + tenant + cookies). */
47
48
  authFetch: ReturnType<typeof createAuthenticatedFetch>;
49
+ /** Open the shared environment entitlement (upgrade / limit) dialog. */
50
+ openEntitlementDialog: (spec: EntitlementDialogSpec) => void;
48
51
  /** Props to spread onto `<ActionProvider>`. */
49
52
  actionProviderProps: {
50
53
  context: Record<string, any>;
@@ -24,6 +24,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
24
24
  import { useCallback, useMemo, useRef, useState } from 'react';
25
25
  import { useNavigate } from 'react-router-dom';
26
26
  import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
27
+ import { usePermissions } from '@object-ui/permissions';
27
28
  import { useObjectLabel } from '@object-ui/i18n';
28
29
  import { ActionProvider, useGlobalUndo } from '@object-ui/react';
29
30
  import { toast } from 'sonner';
@@ -32,11 +33,17 @@ import { ActionParamDialog } from '../views/ActionParamDialog';
32
33
  import { ActionResultDialog } from '../views/ActionResultDialog';
33
34
  import { FlowRunner } from '../views/FlowRunner';
34
35
  import { resolveActionParams } from '../utils/resolveActionParams';
36
+ import { EnvironmentEntitlementDialog } from '../environment/EnvironmentEntitlementDialog';
37
+ import { entitlementDialogFromError } from '../environment/entitlements';
38
+ import { resolvePageVarTokens } from '../utils/resolvePageVarTokens';
35
39
  const FALLBACK_USER = { id: 'current-user', name: 'Demo User', isPlatformAdmin: false };
36
40
  export function useConsoleActionRuntime(opts) {
37
41
  const { dataSource, objects, objectName, onRefresh } = opts;
38
42
  const navigate = useNavigate();
39
43
  const { user, activeOrganization } = useAuth();
44
+ // [ADR-0066 D4] System capabilities for the action capability gate (fail-open
45
+ // when no PermissionProvider is mounted — usePermissions returns []).
46
+ const { systemPermissions } = usePermissions();
40
47
  const { fieldLabel, fieldOptionLabel, actionParamText, actionParamOptionLabel, actionDescription } = useObjectLabel();
41
48
  const objectDef = useMemo(() => (objectName ? objects?.find((o) => o.name === objectName) : undefined), [objects, objectName]);
42
49
  // Object name used for API paths / generic CRUD. Falls back to the action's
@@ -56,6 +63,9 @@ export function useConsoleActionRuntime(opts) {
56
63
  const [resultDialogState, setResultDialogState] = useState({ open: false });
57
64
  // A paused `screen`-node flow awaiting user input.
58
65
  const [screenFlow, setScreenFlow] = useState(null);
66
+ // Plan/capacity gate dialog (upgrade / limit), shared by the env-list toolbar
67
+ // (proactive) and the api-action error path below (reactive safety net).
68
+ const [entitlementDialog, setEntitlementDialog] = useState({ open: false });
59
69
  // Guards against double-firing a server action (slow SSO handoff, etc.).
60
70
  const serverActionInFlight = useRef(new Set());
61
71
  const resultDialogHandler = useCallback((spec, data) => new Promise((resolve) => {
@@ -103,8 +113,8 @@ export function useConsoleActionRuntime(opts) {
103
113
  });
104
114
  }, [objectName, objectDef, objects, fieldLabel, fieldOptionLabel, actionParamText, actionParamOptionLabel]);
105
115
  const currentUser = user
106
- ? { id: user.id, name: user.name, avatar: user.image, isPlatformAdmin: user?.isPlatformAdmin ?? false }
107
- : FALLBACK_USER;
116
+ ? { id: user.id, name: user.name, avatar: user.image, isPlatformAdmin: user?.isPlatformAdmin ?? false, systemPermissions: systemPermissions ?? [] }
117
+ : { ...FALLBACK_USER, systemPermissions: systemPermissions ?? [] };
108
118
  const toastHandler = useCallback((message, options) => {
109
119
  if (options?.type === 'error') {
110
120
  toast.error(message);
@@ -129,7 +139,10 @@ export function useConsoleActionRuntime(opts) {
129
139
  }, [navigate]);
130
140
  // Authenticated fetch for direct backend calls. Declared before apiHandler.
131
141
  const authFetch = useMemo(() => createAuthenticatedFetch(), []);
132
- const apiHandler = useCallback(async (action) => {
142
+ const openEntitlementDialog = useCallback((spec) => {
143
+ setEntitlementDialog({ open: true, spec });
144
+ }, []);
145
+ const apiHandler = useCallback(async (action, context) => {
133
146
  try {
134
147
  const target = action.target || action.name;
135
148
  const params = action.params || {};
@@ -144,6 +157,12 @@ export function useConsoleActionRuntime(opts) {
144
157
  const rawParams = { ...params };
145
158
  const rowRecord = rawParams._rowRecord;
146
159
  delete rawParams._rowRecord;
160
+ // Resolve `{{page.<var>}}` tokens against the live page-variable snapshot
161
+ // (published into the action context by PageVariableActionBridge). This is
162
+ // what lets a pure-SDUI form submit the values its inputs wrote into page
163
+ // variables; whole-value tokens preserve type. See resolvePageVarTokens.
164
+ const pageVars = (context?.pageVariables ?? undefined);
165
+ const resolvedParams = resolvePageVarTokens(rawParams, pageVars);
147
166
  // Interpolate `{field}` tokens in the target URL from the row record.
148
167
  let resolvedTarget = targetStr;
149
168
  if (rowRecord && /\{[a-z_][a-z0-9_]*\}/i.test(resolvedTarget)) {
@@ -156,7 +175,7 @@ export function useConsoleActionRuntime(opts) {
156
175
  const wrap = action.bodyShape && typeof action.bodyShape === 'object' && action.bodyShape.wrap
157
176
  ? action.bodyShape.wrap
158
177
  : undefined;
159
- const body = wrap ? { [wrap]: rawParams } : { ...rawParams };
178
+ const body = wrap ? { [wrap]: resolvedParams } : { ...resolvedParams };
160
179
  if (rowRecord && action.recordIdParam) {
161
180
  const rowField = action.recordIdField || 'id';
162
181
  const rowValue = rowRecord[rowField];
@@ -168,7 +187,7 @@ export function useConsoleActionRuntime(opts) {
168
187
  body.organizationId = activeOrganization.id;
169
188
  }
170
189
  if (action.bodyExtra && typeof action.bodyExtra === 'object') {
171
- Object.assign(body, action.bodyExtra);
190
+ Object.assign(body, resolvePageVarTokens(action.bodyExtra, pageVars));
172
191
  }
173
192
  const method = (action.method || 'POST').toUpperCase();
174
193
  const init = {
@@ -181,12 +200,23 @@ export function useConsoleActionRuntime(opts) {
181
200
  }
182
201
  const res = await authFetch(url, init);
183
202
  if (!res.ok) {
184
- let detail = `HTTP ${res.status}`;
203
+ let body = null;
185
204
  try {
186
- const j = await res.json();
187
- detail = j?.error || j?.message || detail;
205
+ body = await res.json();
188
206
  }
189
207
  catch { /* response body not JSON */ }
208
+ // Plan/capacity gates (e.g. creating an environment the org's plan
209
+ // doesn't include) come back as coded 403s. Surface them as a friendly
210
+ // upgrade/limit DIALOG with a CTA — never a generic red error toast.
211
+ // Returning success:false WITHOUT an `error` suppresses the runner's
212
+ // error toast (ActionRunner.handlePostExecution); the dialog owns the
213
+ // messaging.
214
+ const entitlementSpec = entitlementDialogFromError(body);
215
+ if (entitlementSpec) {
216
+ openEntitlementDialog(entitlementSpec);
217
+ return { success: false };
218
+ }
219
+ const detail = body?.error || body?.message || `HTTP ${res.status}`;
190
220
  return { success: false, error: detail };
191
221
  }
192
222
  const data = await res.json().catch(() => ({}));
@@ -247,7 +277,7 @@ export function useConsoleActionRuntime(opts) {
247
277
  catch (error) {
248
278
  return { success: false, error: error.message };
249
279
  }
250
- }, [dataSource, objApiName, authFetch, activeOrganization, refresh]);
280
+ }, [dataSource, objApiName, authFetch, activeOrganization, refresh, openEntitlementDialog]);
251
281
  // Flow action handler — POST to /api/v1/automation/{name}/trigger.
252
282
  // `context` is the shared ActionRunner context (registered handlers are
253
283
  // invoked as `handler(action, runnerContext)`).
@@ -533,7 +563,8 @@ export function useConsoleActionRuntime(opts) {
533
563
  } }), _jsx(ActionResultDialog, { state: resultDialogState, onAcknowledge: () => {
534
564
  resultDialogState.resolve?.();
535
565
  setResultDialogState({ open: false });
536
- } }), _jsx(FlowRunner, { state: screenFlow, authFetch: authFetch, baseUrl: import.meta.env.VITE_SERVER_URL || '', dataSource: dataSource, objects: objects, onClose: () => setScreenFlow(null), onComplete: () => { setScreenFlow(null); refresh(); } })] }));
566
+ } }), _jsx(FlowRunner, { state: screenFlow, authFetch: authFetch, baseUrl: import.meta.env.VITE_SERVER_URL || '', dataSource: dataSource, objects: objects, onClose: () => setScreenFlow(null), onComplete: () => { setScreenFlow(null); refresh(); } }), _jsx(EnvironmentEntitlementDialog, { state: entitlementDialog, apiBase: import.meta.env.VITE_SERVER_URL || '', onOpenChange: (open) => { if (!open)
567
+ setEntitlementDialog({ open: false }); } })] }));
537
568
  return {
538
569
  confirmHandler,
539
570
  toastHandler,
@@ -544,6 +575,7 @@ export function useConsoleActionRuntime(opts) {
544
575
  flowHandler,
545
576
  serverActionHandler,
546
577
  authFetch,
578
+ openEntitlementDialog,
547
579
  actionProviderProps,
548
580
  dialogs,
549
581
  };
package/dist/index.d.ts CHANGED
@@ -19,7 +19,9 @@ export type { AppShellProps, } from './types';
19
19
  export type { MetadataState, MetadataContextValue, MetadataTypeStatus, } from './providers/MetadataProvider';
20
20
  export type { ExpressionContextValue, } from './providers/ExpressionProvider';
21
21
  export type { RecentItem, } from './hooks/useRecentItems';
22
- export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
22
+ export { ConsoleShell, ConnectedShell, RequireOrganization, RequireAiSurface, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
23
+ export { useAiSurfaceEnabled } from './hooks/useAiSurface';
24
+ export type { AiSurfaceState } from './hooks/useAiSurface';
23
25
  export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
24
26
  export type { ActivityItem } from './layout';
25
27
  export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, type ToastWithUndoOptions, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
@@ -48,8 +50,9 @@ export { MembersPage as DefaultMembersPage } from './console/organizations/manag
48
50
  export { InvitationsPage as DefaultInvitationsPage } from './console/organizations/manage/InvitationsPage';
49
51
  export { SettingsPage as DefaultSettingsPage } from './console/organizations/manage/SettingsPage';
50
52
  export { AcceptInvitationPage as DefaultAcceptInvitationPage } from './console/organizations/manage/AcceptInvitationPage';
51
- export { AiChatPage as DefaultAiChatPage, AiChatPage } from './console/ai/AiChatPage';
53
+ export { AiChatPage as DefaultAiChatPage, AiChatPage, hydratedMessagesToChatMessages, } from './console/ai/AiChatPage';
52
54
  export { ConversationsSidebar } from './console/ai/ConversationsSidebar';
55
+ export { toUIMessages, aiMessageRowsToServerMessages, type HydratedUIMessage, type RawAiMessageRow, } from './hooks/useChatConversation';
53
56
  export { registerAppComponent, getAppComponent, listAppComponents, componentRefToUrlSegments, urlSegmentsToComponentRef, } from './services/componentRegistry';
54
57
  export type { AppComponentRegistryEntry } from './services/componentRegistry';
55
58
  import './services/builtinComponents';
package/dist/index.js CHANGED
@@ -18,7 +18,11 @@ export { getPendingRequests, isIdle, subscribeSettle, whenIdle, withSettleSignal
18
18
  export { useRecentItems } from './hooks/useRecentItems';
19
19
  // Console building blocks — compose these in your App.tsx to build the console
20
20
  // routing tree. See examples/console-starter/src/App.tsx for a minimal example.
21
- export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
21
+ export { ConsoleShell, ConnectedShell, RequireOrganization, RequireAiSurface, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
22
+ // Runtime AI-availability signal — the single source of truth every AI entry
23
+ // point gates on (FAB, /ai routes, designer "Ask AI"). Server-pushed, no
24
+ // build-time edition flag. See ./hooks/useAiSurface.
25
+ export { useAiSurfaceEnabled } from './hooks/useAiSurface';
22
26
  // Layout chrome
23
27
  export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
24
28
  // Top-level chrome (dialogs, providers, error boundaries)
@@ -52,8 +56,12 @@ export { MembersPage as DefaultMembersPage } from './console/organizations/manag
52
56
  export { InvitationsPage as DefaultInvitationsPage } from './console/organizations/manage/InvitationsPage';
53
57
  export { SettingsPage as DefaultSettingsPage } from './console/organizations/manage/SettingsPage';
54
58
  export { AcceptInvitationPage as DefaultAcceptInvitationPage } from './console/organizations/manage/AcceptInvitationPage';
55
- export { AiChatPage as DefaultAiChatPage, AiChatPage } from './console/ai/AiChatPage';
59
+ export { AiChatPage as DefaultAiChatPage, AiChatPage, hydratedMessagesToChatMessages, } from './console/ai/AiChatPage';
56
60
  export { ConversationsSidebar } from './console/ai/ConversationsSidebar';
61
+ // Conversation-history hydration helpers — reused by the public read-only
62
+ // share page (`/s/:token`) so a shared transcript renders identically to the
63
+ // live chat (tool cards included) instead of dumping raw envelopes.
64
+ export { toUIMessages, aiMessageRowsToServerMessages, } from './hooks/useChatConversation';
57
65
  // Phase 3b: Component nav registry — plugins use this to register
58
66
  // admin/setup UI surfaces that are addressable from App metadata via
59
67
  // `{ type: 'component', componentRef: 'ns:name' }` nav items.
@@ -20,7 +20,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
20
20
  */
21
21
  import { useLocation, useParams, Link, useNavigate } from 'react-router-dom';
22
22
  import { Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, cn, } from '@object-ui/components';
23
- import { Search, HelpCircle, ChevronDown, Check, Lock, LogOut, Boxes, Layers, Bot, User, BookOpen, ExternalLink, Keyboard, } from 'lucide-react';
23
+ import { Search, HelpCircle, ChevronDown, Check, Lock, LogOut, Boxes, Plus, Layers, Bot, User, BookOpen, ExternalLink, Keyboard, } from 'lucide-react';
24
24
  import { useState, useEffect, useCallback, useRef } from 'react';
25
25
  import { useOffline } from '@object-ui/react';
26
26
  import { PresenceAvatars, useTenantPresence } from '@object-ui/collaboration';
@@ -39,6 +39,7 @@ import { useMobileViewSwitcher } from './MobileViewSwitcherContext';
39
39
  import { useNavigationContext } from '../context/NavigationContext';
40
40
  import { useCommandPalette } from '../context/CommandPaletteProvider';
41
41
  import { useUrlOverlay } from '../hooks/useUrlOverlay';
42
+ import { useAiSurfaceEnabled } from '../hooks/useAiSurface';
42
43
  import { getProductName } from '../runtime-config';
43
44
  import { LocalizedSidebarTrigger } from './LocalizedSidebarTrigger';
44
45
  function humanizeSlug(slug) {
@@ -64,8 +65,12 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
64
65
  // Click-reachable entry for the keyboard-shortcuts dialog (was `?`-key only).
65
66
  // Shares the `?shortcuts=1` URL param with KeyboardShortcutsDialog (C2/C3).
66
67
  const { openOverlay: openShortcuts } = useUrlOverlay('shortcuts');
67
- const { user, signOut, isAuthEnabled, organizations, activeOrganization, isOrganizationsLoading, } = useAuth();
68
+ const { user, signOut, isAuthEnabled, organizations, activeOrganization, isOrganizationsLoading, getAuthConfig, } = useAuth();
68
69
  const dataSource = useAdapter();
70
+ // Runtime AI gating: hide the top-bar AI entry point when the server serves
71
+ // no AI (Community Edition) so it can't dead-end on a chat with no agent.
72
+ // Same signal as the FAB and the `/ai` route guard.
73
+ const { enabled: aiEnabled } = useAiSurfaceEnabled();
69
74
  const { t } = useObjectTranslation();
70
75
  const { objectLabel, dashboardLabel, pageLabel, reportLabel, viewLabel, appLabel } = useObjectLabel();
71
76
  const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
@@ -432,6 +437,25 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
432
437
  const activeActivities = activities ?? apiActivities ?? [];
433
438
  const orgList = organizations ?? [];
434
439
  const hasOrgSection = isOrganizationsLoading || orgList.length > 0 || !!activeOrganization;
440
+ // Mirror the server's `beforeCreateOrganization` gate so the "Create
441
+ // workspace" entry only shows where multi-org self-service is enabled.
442
+ // Default to allowed until the config resolves (avoids hiding it on slow
443
+ // networks); the server still enforces.
444
+ const [multiOrgDisabled, setMultiOrgDisabled] = useState(false);
445
+ useEffect(() => {
446
+ let cancelled = false;
447
+ getAuthConfig?.()
448
+ .then((cfg) => {
449
+ if (!cancelled)
450
+ setMultiOrgDisabled(cfg?.features?.multiOrgEnabled === false);
451
+ })
452
+ .catch(() => {
453
+ /* leave default — server still enforces */
454
+ });
455
+ return () => {
456
+ cancelled = true;
457
+ };
458
+ }, [getAuthConfig]);
435
459
  // Build path segments (only used in `app` variant)
436
460
  const pathParts = location.pathname.split('/').filter(Boolean);
437
461
  const appNameFromRoute = params.appName || pathParts[1];
@@ -545,10 +569,10 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
545
569
  if (!isActive)
546
570
  mobileSwitcher.onChange(v.id);
547
571
  }, className: "gap-2", children: [v.icon ? (_jsx("span", { className: "shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", children: v.icon })) : null, _jsx("span", { className: "flex-1 truncate", children: v.label }), v.locked ? (_jsx(Lock, { className: "h-3 w-3 shrink-0 text-muted-foreground", "aria-hidden": true })) : null, isActive ? (_jsx(Check, { className: "h-4 w-4 shrink-0 text-foreground", "aria-hidden": true })) : null] }, v.id));
548
- }) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), t('topbar.offline', { defaultValue: 'Offline' })] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: t('topbar.usersOnline', { defaultValue: 'Users currently online' }), children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { type: "button", "data-testid": "action:command-palette:open", "aria-label": t('console.search', { defaultValue: 'Search…' }), "aria-keyshortcuts": "Meta+K Control+K", onClick: openCommandPalette, className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", "data-testid": "action:command-palette:open-mobile", onClick: openCommandPalette, "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", asChild: true, "aria-label": t('topbar.aiAssistant', { defaultValue: 'AI Assistant' }), children: _jsx(Link, { to: "/ai", children: _jsx(Bot, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenu, { onOpenChange: (open) => { if (open)
572
+ }) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), t('topbar.offline', { defaultValue: 'Offline' })] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: t('topbar.usersOnline', { defaultValue: 'Users currently online' }), children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { type: "button", "data-testid": "action:command-palette:open", "aria-label": t('console.search', { defaultValue: 'Search…' }), "aria-keyshortcuts": "Meta+K Control+K", onClick: openCommandPalette, className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", "data-testid": "action:command-palette:open-mobile", onClick: openCommandPalette, "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), aiEnabled && (_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", asChild: true, "aria-label": t('topbar.aiAssistant', { defaultValue: 'AI Assistant' }), children: _jsx(Link, { to: "/ai", children: _jsx(Bot, { className: "h-4 w-4" }) }) })), _jsxs(DropdownMenu, { onOpenChange: (open) => { if (open)
549
573
  void loadHelpDocs(); }, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-56 rounded-lg", sideOffset: 4, children: [currentAppDocs.length > 0 && currentAppPackageId ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate(currentAppDocs.length === 1
550
574
  ? `/apps/${currentAppPackageId}/docs/${currentAppDocs[0].name}`
551
- : `/apps/${currentAppPackageId}/docs`), children: [_jsx(BookOpen, { className: "mr-2 h-4 w-4" }), t('help.appDocs', { defaultValue: "This app's docs" })] })) : null, _jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate('/docs'), children: [_jsx(Layers, { className: "mr-2 h-4 w-4" }), t('help.allDocs', { defaultValue: 'All documentation' })] }), isApp ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", "data-testid": "action:keyboard-shortcuts:open", onClick: openShortcuts, children: [_jsx(Keyboard, { className: "mr-2 h-4 w-4" }), t('help.keyboardShortcuts', { defaultValue: 'Keyboard shortcuts' })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { asChild: true, className: "cursor-pointer", children: _jsxs("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), t('help.onlineDocs', { defaultValue: 'Online documentation' })] }) })] })] })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/account/component/account/profile_card'), className: "cursor-pointer", children: [_jsx(User, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), (metadataApps || [])
575
+ : `/apps/${currentAppPackageId}/docs`), children: [_jsx(BookOpen, { className: "mr-2 h-4 w-4" }), t('help.appDocs', { defaultValue: "This app's docs" })] })) : null, _jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate('/docs'), children: [_jsx(Layers, { className: "mr-2 h-4 w-4" }), t('help.allDocs', { defaultValue: 'All documentation' })] }), isApp ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", "data-testid": "action:keyboard-shortcuts:open", onClick: openShortcuts, children: [_jsx(Keyboard, { className: "mr-2 h-4 w-4" }), t('help.keyboardShortcuts', { defaultValue: 'Keyboard shortcuts' })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { asChild: true, className: "cursor-pointer", children: _jsxs("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), t('help.onlineDocs', { defaultValue: 'Online documentation' })] }) })] })] })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/account/component/account/profile_card'), className: "cursor-pointer", children: [_jsx(User, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations?manage=1'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), hasOrgSection && !multiOrgDisabled && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations?create=1'), className: "cursor-pointer", "data-testid": "header-create-workspace", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t('organizations.create', { defaultValue: 'Create workspace' })] })), (metadataApps || [])
552
576
  .filter((a) => a.active !== false && a.hidden === true && a.name !== 'account')
553
577
  .map((app) => {
554
578
  const AppIcon = getIcon(app.icon);
@@ -36,10 +36,12 @@ export interface ConsoleFloatingChatbotProps {
36
36
  */
37
37
  defaultAgent?: string;
38
38
  /**
39
- * Show the in-header agent switcher. Off by default: end users get the
40
- * single agent bound to their app and never have to choose. Enable for
41
- * power users / admins (or via `VITE_AI_SHOW_AGENT_PICKER`) when a
42
- * surface genuinely exposes multiple agents.
39
+ * Force the in-header agent switcher on (`true`) or off (`false`),
40
+ * overriding the default. When left undefined the switcher auto-reveals
41
+ * only when AI development is unlocked for the viewer — the live catalog
42
+ * serves BOTH an `ask` and a `build` agent and `aiStudio` isn't disabled —
43
+ * so pure end-user apps (only `ask`) stay clean while builders can flip
44
+ * Ask↔Build inline. `VITE_AI_SHOW_AGENT_PICKER=true` also forces it on.
43
45
  */
44
46
  showAgentPicker?: boolean;
45
47
  /** Whether the floating panel should open immediately on mount. */
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  */
14
14
  import React from 'react';
15
15
  import { useReconcileOnError } from '../hooks/useReconcileOnError';
16
- import { FloatingChatbot, useObjectChat, useAgents, useHitlInChat, resolveDefaultAgentName, uiMessagesToChatMessages, publishHealthFromResponse, agentRouteName, } from '@object-ui/plugin-chatbot';
16
+ import { FloatingChatbot, useObjectChat, useAgents, useAiModels, useHitlInChat, resolveDefaultAgentName, uiMessagesToChatMessages, publishHealthFromResponse, agentRouteName, } from '@object-ui/plugin-chatbot';
17
17
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button, ShareDialog, } from '@object-ui/components';
18
18
  import { Share2, SquarePen } from 'lucide-react';
19
19
  import { useObjectTranslation } from '@object-ui/i18n';
@@ -24,6 +24,7 @@ import { useAssistant, requestAssistantReview, emitCanvasInvalidate } from '../a
24
24
  import { fetchPendingDraftCount } from '../preview/draftStatus';
25
25
  import { getRuntimeConfig } from '../runtime-config';
26
26
  import { cloudPricingDeepLink } from '../console/marketplace/marketplaceApi';
27
+ import { shouldShowAgentPicker } from './agentPicker';
27
28
  /**
28
29
  * Display names for the two built-in platform agents (ADR-0063: `ask` / `build`,
29
30
  * bound by surface). The backend ships English labels ("Assistant" / "Builder");
@@ -73,6 +74,9 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
73
74
  toolRunning: '运行中',
74
75
  toolAwaitingApproval: '等待确认',
75
76
  toolFailed: '失败',
77
+ connectionWaiting: '正在等待服务器响应…',
78
+ connectionStalledLabel: '仍在处理中…',
79
+ connectionOfflineLabel: '网络已断开,正在重连…',
76
80
  toolDetailsHidden: '已隐藏工具参数和原始结果,仅保留过程摘要。',
77
81
  copy: '复制',
78
82
  copied: '已复制',
@@ -99,6 +103,7 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
99
103
  planApproveHint: '回复以确认或调整该方案。',
100
104
  planApprove: '开始搭建',
101
105
  planAdjust: '调整方案',
106
+ planBuilt: '已搭建',
102
107
  planApproveMessage: '就按这个方案搭建吧。',
103
108
  planApproveDefaultsMessage: '就按你的合理假设直接搭建,未决问题用默认即可。',
104
109
  planAnswer: (question, option) => `关于「${question}」,我选择「${option}」。`,
@@ -128,6 +133,9 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
128
133
  toolRunning: 'Running',
129
134
  toolAwaitingApproval: 'Awaiting approval',
130
135
  toolFailed: 'Failed',
136
+ connectionWaiting: 'Waiting for server…',
137
+ connectionStalledLabel: 'Still working…',
138
+ connectionOfflineLabel: 'Connection lost — reconnecting…',
131
139
  toolDetailsHidden: 'Tool inputs and raw results are hidden in this view.',
132
140
  copy: 'Copy',
133
141
  copied: 'Copied',
@@ -154,6 +162,7 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
154
162
  planApproveHint: 'Reply to approve or adjust this plan.',
155
163
  planApprove: 'Build it',
156
164
  planAdjust: 'Adjust',
165
+ planBuilt: 'Built',
157
166
  planApproveMessage: 'Looks good — build it as proposed.',
158
167
  planApproveDefaultsMessage: 'Build it with your best assumptions; use sensible defaults for the open questions.',
159
168
  planAnswer: (question, option) => `For "${question}", go with: ${option}.`,
@@ -248,6 +257,14 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
248
257
  // the agent context so "add a priority field" acts on the open object,
249
258
  // and drives context-aware starter suggestions.
250
259
  const { editor } = useAssistant();
260
+ // ADR-0028: the plan-filtered AI-model allowlist this env offers in the
261
+ // picker (free / single-model envs return one entry → the footer picker
262
+ // hides itself). The selected id is sent with each turn; the backend
263
+ // validates it against the same allowlist and weights the quota by the
264
+ // chosen model's cost_weight.
265
+ const { models: aiModels, defaultModelId } = useAiModels({ apiBase });
266
+ const [selectedModelId, setSelectedModelId] = React.useState(undefined);
267
+ const effectiveModelId = selectedModelId ?? defaultModelId;
251
268
  // Replay persisted history when present.
252
269
  const hydratedHistory = React.useMemo(() => {
253
270
  if (!persistedMessages || persistedMessages.length === 0)
@@ -268,6 +285,9 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
268
285
  const { messages, isLoading, error, sendMessage, stop, reload, clear, setMessages, } = useObjectChat({
269
286
  api: chatApi,
270
287
  conversationId,
288
+ // ADR-0028: the user's picked model (or the env default) — sent in each
289
+ // request body; the agent route validates it + routes the turn to it.
290
+ model: effectiveModelId,
271
291
  onError: handleChatError,
272
292
  body: {
273
293
  context: {
@@ -321,10 +341,10 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
321
341
  sendMessage(prompt);
322
342
  },
323
343
  });
324
- // Agent switcher — deliberately hidden by default. End users get the
325
- // single agent bound to their app (Studio metadata_assistant, others
326
- // data_chat) and are never asked to choose. Only surfaces when the
327
- // host explicitly opts in AND there is more than one agent to pick.
344
+ // Agent switcher — Ask Build (plus any custom agents). Restrained by
345
+ // design: end users bound to a single agent never see it. `showAgentPicker`
346
+ // is true when AI development is unlocked (catalog serves both ask & build)
347
+ // or forced on; it still needs more than one agent to be a real choice.
328
348
  const headerExtra = showAgentPicker && agents.length > 1 ? (_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;
329
349
  // Share-link control. Sits to the left of the panel's built-in
330
350
  // fullscreen / close buttons so users can mint a public link without
@@ -339,7 +359,10 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
339
359
  panelHeight: 560,
340
360
  title: locale.title,
341
361
  triggerSize: 56,
342
- }, headerExtra: headerExtra, headerActions: headerActions, messages: messages, labels: locale.labels, showAvatars: true, hideClearBar: true, assistantAvatarFallback: locale.agentLabel, suggestions: messages.length === 0 ? (editorSuggestions ?? locale.suggestions) : undefined, placeholder: activeAgent
362
+ }, headerExtra: headerExtra, headerActions: headerActions, messages: messages, labels: locale.labels,
363
+ // ADR-0028: model picker — ChatbotEnhanced renders the footer <select>
364
+ // only when 2+ models are offered, so free / single-model envs see none.
365
+ models: aiModels, selectedModelId: effectiveModelId, onModelChange: setSelectedModelId, showAvatars: true, hideClearBar: true, assistantAvatarFallback: locale.agentLabel, suggestions: messages.length === 0 ? (editorSuggestions ?? locale.suggestions) : undefined, placeholder: activeAgent
343
366
  ? locale.placeholder
344
367
  : agentsLoading
345
368
  ? locale.loadingPlaceholder
@@ -423,7 +446,7 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
423
446
  });
424
447
  return false;
425
448
  }
426
- }, publishDraftsLabel: locale.publishDrafts, nextStepsLabel: locale.nextSteps, planTitleLabel: locale.planTitle, planQuestionsLabel: locale.planQuestions, planAssumptionsLabel: locale.planAssumptions, planApproveHintLabel: locale.planApproveHint, planApproveLabel: locale.planApprove, planAdjustLabel: locale.planAdjust, planApproveMessage: locale.planApproveMessage, planApproveDefaultsMessage: locale.planApproveDefaultsMessage, planAnswerMessage: locale.planAnswer, publishedLabel: locale.published,
449
+ }, publishDraftsLabel: locale.publishDrafts, nextStepsLabel: locale.nextSteps, planTitleLabel: locale.planTitle, planQuestionsLabel: locale.planQuestions, planAssumptionsLabel: locale.planAssumptions, planApproveHintLabel: locale.planApproveHint, planApproveLabel: locale.planApprove, planAdjustLabel: locale.planAdjust, planBuiltLabel: locale.planBuilt, planApproveMessage: locale.planApproveMessage, planApproveDefaultsMessage: locale.planApproveDefaultsMessage, planAnswerMessage: locale.planAnswer, publishedLabel: locale.published,
427
450
  // Self-use "magic moment": when the plan enables it, auto-publish the
428
451
  // drafted app the instant the agent finishes — the success path above
429
452
  // then navigates straight to the live app, so "build" lands the user on
@@ -434,10 +457,18 @@ export default function ConsoleFloatingChatbot({ appLabel, appName, objects, api
434
457
  const apiBase = React.useMemo(() => resolveApiBase(apiBaseProp), [apiBaseProp]);
435
458
  const env = import.meta.env ?? {};
436
459
  const envDefaultAgent = env.VITE_AI_DEFAULT_AGENT;
437
- // Power-user / admin escape hatch: force the picker on globally without
438
- // touching app metadata.
439
- const showAgentPicker = showAgentPickerProp ?? env.VITE_AI_SHOW_AGENT_PICKER === 'true';
440
460
  const { agents, isLoading: agentsLoading, error: agentsError } = useAgents({ apiBase });
461
+ // Reveal the Build/Ask switcher only when AI development is unlocked for this
462
+ // viewer — the live catalog serves BOTH an `ask` and a `build` agent and
463
+ // authoring isn't deployment-disabled. Pure end-user apps (only `ask`) stay
464
+ // clean; builders can flip "ask about my data" ↔ "extend my app" inline. An
465
+ // explicit prop or `VITE_AI_SHOW_AGENT_PICKER` still forces it. See agentPicker.
466
+ const showAgentPicker = shouldShowAgentPicker({
467
+ agents,
468
+ showAgentPickerProp,
469
+ envOptIn: env.VITE_AI_SHOW_AGENT_PICKER === 'true',
470
+ aiStudioEnabled: getRuntimeConfig().features.aiStudio !== false,
471
+ });
441
472
  const [activeAgent, setActiveAgent] = React.useState(undefined);
442
473
  React.useEffect(() => {
443
474
  if (!activeAgent && agents.length > 0) {
@@ -10,7 +10,6 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
10
10
  */
11
11
  import { useEffect } from 'react';
12
12
  import { AppShell } from '@object-ui/layout';
13
- import { useDiscovery } from '@object-ui/react';
14
13
  import { isBuildAgent } from '@object-ui/plugin-chatbot';
15
14
  // Lightweight FAB stub — the heavy chat chunk graph (plugin-chatbot,
16
15
  // shiki, streamdown, mermaid, @ai-sdk, ~20MB) only downloads on first
@@ -22,6 +21,7 @@ import { UnifiedSidebar } from './UnifiedSidebar';
22
21
  import { AppHeader } from './AppHeader';
23
22
  import { MobileViewSwitcherProvider } from './MobileViewSwitcherContext';
24
23
  import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
24
+ import { useAiSurfaceEnabled } from '../hooks/useAiSurface';
25
25
  import { useNavigationContext } from '../context/NavigationContext';
26
26
  import { CommandPaletteProvider } from '../context/CommandPaletteProvider';
27
27
  import { resolveI18nLabel } from '../utils';
@@ -35,11 +35,10 @@ function ConsoleLayoutInner({ children }) {
35
35
  // (moved to ./ConsoleFloatingChatbot.tsx for code-splitting)
36
36
  export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState, userId, }) {
37
37
  const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
38
- const { isAiEnabled } = useDiscovery();
39
- // Trust an explicit `VITE_AI_BASE_URL` opt-in even when discovery reports
40
- // AI as disabled (e.g. framework started without `--preset full`).
41
- const aiBaseUrlConfigured = Boolean(import.meta.env?.VITE_AI_BASE_URL);
42
- const showChatbot = isAiEnabled || aiBaseUrlConfigured;
38
+ // Runtime, server-pushed AI gating (shared with the `/ai` route guard and the
39
+ // top-bar AI link): show the chatbot only when the server actually serves AI,
40
+ // or when an explicit `VITE_AI_BASE_URL` opt-in points at an external server.
41
+ const { enabled: showChatbot } = useAiSurfaceEnabled();
43
42
  // AI Studio (AI-driven metadata authoring / "online development") can be
44
43
  // turned off per deployment. When off, suppress the metadata-authoring
45
44
  // assistant so the chatbot falls back to the generic data assistant — the