@object-ui/app-shell 7.0.0 → 7.1.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 (73) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/dist/console/AppContent.js +14 -2
  3. package/dist/console/ai/AiChatPage.js +11 -7
  4. package/dist/console/ai/LiveCanvas.d.ts +8 -2
  5. package/dist/console/ai/LiveCanvas.js +6 -4
  6. package/dist/hooks/useChatConversation.d.ts +30 -0
  7. package/dist/hooks/useChatConversation.js +63 -0
  8. package/dist/hooks/useConsoleActionRuntime.js +6 -2
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +5 -1
  11. package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
  12. package/dist/layout/ConsoleFloatingChatbot.js +25 -8
  13. package/dist/layout/ContextSelectors.js +59 -35
  14. package/dist/layout/agentPicker.d.ts +56 -0
  15. package/dist/layout/agentPicker.js +40 -0
  16. package/dist/preview/CommitTimeline.d.ts +15 -0
  17. package/dist/preview/CommitTimeline.js +82 -0
  18. package/dist/preview/UnpublishedAppBar.js +11 -7
  19. package/dist/preview/commitHistory.d.ts +28 -0
  20. package/dist/preview/commitHistory.js +48 -0
  21. package/dist/providers/MetadataProvider.js +9 -0
  22. package/dist/views/FlowRunner.d.ts +2 -30
  23. package/dist/views/FlowRunner.js +18 -50
  24. package/dist/views/ScreenView.d.ts +70 -0
  25. package/dist/views/ScreenView.js +73 -0
  26. package/dist/views/metadata-admin/DirectoryPage.js +2 -14
  27. package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
  28. package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
  29. package/dist/views/metadata-admin/PackagesPage.js +9 -1
  30. package/dist/views/metadata-admin/ResourceEditPage.js +47 -20
  31. package/dist/views/metadata-admin/ResourceListPage.js +8 -16
  32. package/dist/views/metadata-admin/StudioHomePage.js +6 -14
  33. package/dist/views/metadata-admin/anchors.js +20 -2
  34. package/dist/views/metadata-admin/i18n.js +88 -2
  35. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +2 -2
  36. package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +122 -8
  37. package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +84 -3
  38. package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +67 -2
  39. package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
  40. package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
  41. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
  42. package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
  43. package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
  44. package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
  45. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
  46. package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +97 -0
  47. package/dist/views/metadata-admin/inspectors/flow-node-config.js +46 -1
  48. package/dist/views/metadata-admin/issuePath.d.ts +22 -0
  49. package/dist/views/metadata-admin/issuePath.js +65 -0
  50. package/dist/views/metadata-admin/package-scope.d.ts +26 -0
  51. package/dist/views/metadata-admin/package-scope.js +43 -0
  52. package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
  53. package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +7 -1
  54. package/dist/views/metadata-admin/previews/FlowCanvas.js +104 -16
  55. package/dist/views/metadata-admin/previews/FlowPreview.js +31 -3
  56. package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
  57. package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
  58. package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
  59. package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
  60. package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
  61. package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
  62. package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +9 -1
  63. package/dist/views/metadata-admin/previews/flow-canvas-parts.js +21 -6
  64. package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
  65. package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
  66. package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
  67. package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
  68. package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +11 -0
  69. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
  70. package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +72 -0
  71. package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
  72. package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
  73. package/package.json +38 -38
@@ -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}.`,
@@ -321,10 +330,10 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
321
330
  sendMessage(prompt);
322
331
  },
323
332
  });
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.
333
+ // Agent switcher — Ask Build (plus any custom agents). Restrained by
334
+ // design: end users bound to a single agent never see it. `showAgentPicker`
335
+ // is true when AI development is unlocked (catalog serves both ask & build)
336
+ // or forced on; it still needs more than one agent to be a real choice.
328
337
  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
338
  // Share-link control. Sits to the left of the panel's built-in
330
339
  // fullscreen / close buttons so users can mint a public link without
@@ -423,7 +432,7 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
423
432
  });
424
433
  return false;
425
434
  }
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,
435
+ }, 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
436
  // Self-use "magic moment": when the plan enables it, auto-publish the
428
437
  // drafted app the instant the agent finishes — the success path above
429
438
  // then navigates straight to the live app, so "build" lands the user on
@@ -434,10 +443,18 @@ export default function ConsoleFloatingChatbot({ appLabel, appName, objects, api
434
443
  const apiBase = React.useMemo(() => resolveApiBase(apiBaseProp), [apiBaseProp]);
435
444
  const env = import.meta.env ?? {};
436
445
  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
446
  const { agents, isLoading: agentsLoading, error: agentsError } = useAgents({ apiBase });
447
+ // Reveal the Build/Ask switcher only when AI development is unlocked for this
448
+ // viewer — the live catalog serves BOTH an `ask` and a `build` agent and
449
+ // authoring isn't deployment-disabled. Pure end-user apps (only `ask`) stay
450
+ // clean; builders can flip "ask about my data" ↔ "extend my app" inline. An
451
+ // explicit prop or `VITE_AI_SHOW_AGENT_PICKER` still forces it. See agentPicker.
452
+ const showAgentPicker = shouldShowAgentPicker({
453
+ agents,
454
+ showAgentPickerProp,
455
+ envOptIn: env.VITE_AI_SHOW_AGENT_PICKER === 'true',
456
+ aiStudioEnabled: getRuntimeConfig().features.aiStudio !== false,
457
+ });
441
458
  const [activeAgent, setActiveAgent] = React.useState(undefined);
442
459
  React.useEffect(() => {
443
460
  if (!activeAgent && agents.length > 0) {
@@ -19,6 +19,16 @@ import { useLocation, useNavigate } from 'react-router-dom';
19
19
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
20
20
  import { getIcon } from '../utils/getIcon';
21
21
  import { resolveI18nLabel } from '../utils';
22
+ // Local/Custom scope sentinel — kept inline (not imported from metadata-admin)
23
+ // so this layout module never forms an import cycle with the metadata-admin
24
+ // views. Mirrors `LOCAL_PACKAGE_ID` in views/metadata-admin/package-scope.ts.
25
+ const LOCAL_SCOPE_ID = 'sys_metadata';
26
+ function localScopeLabel() {
27
+ const lang = (typeof document !== 'undefined' && document.documentElement?.lang) ||
28
+ (typeof navigator !== 'undefined' && navigator.language) ||
29
+ '';
30
+ return /^zh/i.test(lang) ? '本地 / 自定义(本环境)' : 'Local / Custom (this env)';
31
+ }
22
32
  const ALL_SENTINEL = '__all__';
23
33
  /** Read a (possibly dotted) property path off a row, e.g. `manifest.id`. */
24
34
  function getByPath(row, key) {
@@ -80,43 +90,56 @@ function useSelectorOptions(def) {
80
90
  const labelKey = def.optionsSource.labelKey || 'name';
81
91
  const filters = def.optionsSource.filter;
82
92
  const filterKey = JSON.stringify(filters ?? []);
83
- React.useEffect(() => {
84
- let cancelled = false;
85
- (async () => {
86
- try {
87
- const res = await fetch(endpoint, {
88
- credentials: 'include',
89
- headers: { Accept: 'application/json' },
90
- });
91
- if (!res.ok)
92
- return;
93
- const json = await res.json();
94
- if (cancelled)
95
- return;
96
- const rows = extractRows(json);
97
- const opts = [];
98
- const seen = new Set();
99
- for (const row of rows) {
100
- if (!rowPasses(row, filters))
101
- continue;
102
- const value = getByPath(row, valueKey);
103
- if (value == null || typeof value !== 'string' || seen.has(value))
104
- continue;
105
- seen.add(value);
106
- const labelRaw = getByPath(row, labelKey);
107
- opts.push({ value, label: typeof labelRaw === 'string' && labelRaw ? labelRaw : value });
108
- }
109
- opts.sort((a, b) => a.label.localeCompare(b.label));
110
- setOptions(opts);
93
+ // Stable, re-runnable fetch. Without an explicit refetch the option list is
94
+ // read once on mount, so a package created elsewhere (PackagesPage) stays
95
+ // invisible in this dropdown until a full page reload. We refetch on
96
+ // dropdown-open and on a global `objectui:packages-changed` signal.
97
+ const load = React.useCallback(async () => {
98
+ try {
99
+ const res = await fetch(endpoint, {
100
+ credentials: 'include',
101
+ headers: { Accept: 'application/json' },
102
+ });
103
+ if (!res.ok)
104
+ return;
105
+ const json = await res.json();
106
+ const rows = extractRows(json);
107
+ const opts = [];
108
+ const seen = new Set();
109
+ for (const row of rows) {
110
+ if (!rowPasses(row, filters))
111
+ continue;
112
+ const value = getByPath(row, valueKey);
113
+ if (value == null || typeof value !== 'string' || seen.has(value))
114
+ continue;
115
+ seen.add(value);
116
+ const labelRaw = getByPath(row, labelKey);
117
+ opts.push({ value, label: typeof labelRaw === 'string' && labelRaw ? labelRaw : value });
111
118
  }
112
- catch {
113
- /* offline / unauthorized render with no options */
119
+ opts.sort((a, b) => a.label.localeCompare(b.label));
120
+ // The package-scope selector gets a stable "Local / Custom (this env)"
121
+ // entry for this environment's runtime, DB-authored metadata — it is
122
+ // never a real package row (`package_id = null` / `sys_metadata`
123
+ // provenance) yet must always be selectable so org-authored items are
124
+ // discoverable and editable. The metadata list/get API already treats
125
+ // `?package=sys_metadata` as exactly this local scope.
126
+ if (/package/i.test(endpoint) && !opts.some((o) => o.value === LOCAL_SCOPE_ID)) {
127
+ opts.push({ value: LOCAL_SCOPE_ID, label: localScopeLabel() });
114
128
  }
115
- })();
116
- return () => { cancelled = true; };
129
+ setOptions(opts);
130
+ }
131
+ catch {
132
+ /* offline / unauthorized — render with no options */
133
+ }
117
134
  // eslint-disable-next-line react-hooks/exhaustive-deps
118
135
  }, [endpoint, valueKey, labelKey, filterKey]);
119
- return options;
136
+ React.useEffect(() => {
137
+ void load();
138
+ const onChanged = () => { void load(); };
139
+ window.addEventListener('objectui:packages-changed', onChanged);
140
+ return () => window.removeEventListener('objectui:packages-changed', onChanged);
141
+ }, [load]);
142
+ return { options, refetch: load };
120
143
  }
121
144
  /**
122
145
  * Hook: resolves the active values for an app's context selectors and
@@ -189,7 +212,7 @@ export function useAppContextSelectors(appName, selectors, t) {
189
212
  return { contextValues, element };
190
213
  }
191
214
  function SelectorControl({ def, value, onChange, t, }) {
192
- const options = useSelectorOptions(def);
215
+ const { options, refetch } = useSelectorOptions(def);
193
216
  const Icon = getIcon(def.icon);
194
217
  const rawLabel = resolveI18nLabel(def.label, t) || def.id;
195
218
  const label = rawLabel === 'Package'
@@ -214,5 +237,6 @@ function SelectorControl({ def, value, onChange, t, }) {
214
237
  }
215
238
  }, [hasConcrete, onChange, options]);
216
239
  const current = hasConcrete ? value : '';
217
- return (_jsxs(Select, { value: current, onValueChange: onChange, children: [_jsx(SelectTrigger, { "aria-label": label, className: "h-9 w-full gap-2 rounded-md border-sidebar-border/70 bg-sidebar/80 px-2 text-xs font-medium text-sidebar-foreground shadow-none transition-colors hover:bg-sidebar-accent focus:ring-1 focus:ring-sidebar-ring data-[state=open]:bg-sidebar-accent [&>svg]:h-3.5 [&>svg]:w-3.5 [&>svg]:shrink-0", children: _jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2 overflow-hidden", children: [_jsx("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center rounded border border-sidebar-border/70 bg-sidebar-accent text-sidebar-foreground/70", children: _jsx(Icon, { className: "h-3 w-3" }) }), _jsx(SelectValue, { placeholder: placeholder, className: "truncate" })] }) }), _jsx(SelectContent, { className: "w-[var(--radix-select-trigger-width)]", children: options.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
240
+ return (_jsxs(Select, { value: current, onValueChange: onChange, onOpenChange: (open) => { if (open)
241
+ refetch(); }, children: [_jsx(SelectTrigger, { "aria-label": label, "data-testid": "package-switcher", className: "h-9 w-full gap-2 rounded-md border-sidebar-border/70 bg-sidebar/80 px-2 text-xs font-medium text-sidebar-foreground shadow-none transition-colors hover:bg-sidebar-accent focus:ring-1 focus:ring-sidebar-ring data-[state=open]:bg-sidebar-accent [&>svg]:h-3.5 [&>svg]:w-3.5 [&>svg]:shrink-0", children: _jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2 overflow-hidden", children: [_jsx("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center rounded border border-sidebar-border/70 bg-sidebar-accent text-sidebar-foreground/70", children: _jsx(Icon, { className: "h-3 w-3" }) }), _jsx(SelectValue, { placeholder: placeholder, className: "truncate" })] }) }), _jsx(SelectContent, { className: "w-[var(--radix-select-trigger-width)]", children: options.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
218
242
  }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * agentPicker
3
+ *
4
+ * Pure decision logic for the floating assistant's Build/Ask switcher. Kept in
5
+ * its own module (no React, no chat deps) so it can be unit-tested without
6
+ * dragging in the heavy chat component graph (FloatingChatbot → streamdown →
7
+ * shiki → @ai-sdk, ~20MB) that `ConsoleFloatingChatbot` pulls in.
8
+ * @module
9
+ */
10
+ import { type AgentDescriptor } from '@object-ui/plugin-chatbot';
11
+ /** Minimal catalog shape the decision needs — just the agent name. */
12
+ type AgentLike = Pick<AgentDescriptor, 'name'>;
13
+ export interface AgentPickerDecisionInput {
14
+ /** Live agent catalog from `useAgents` (the single source of truth). */
15
+ agents: AgentLike[];
16
+ /**
17
+ * Explicit host override. When defined it wins outright — `true` forces the
18
+ * switcher on, `false` forces it off — regardless of catalog or env.
19
+ */
20
+ showAgentPickerProp?: boolean;
21
+ /**
22
+ * `VITE_AI_SHOW_AGENT_PICKER === 'true'` — the power-user / admin global
23
+ * escape hatch that forces the switcher on without touching app metadata.
24
+ */
25
+ envOptIn?: boolean;
26
+ /**
27
+ * Whether AI Studio (authoring / "online development") is enabled for this
28
+ * deployment. When off, the build agent must not be reachable from the panel,
29
+ * so the auto-reveal is suppressed even if the catalog still serves `build`.
30
+ * Mirrors `ConsoleLayout`'s `aiStudioEnabled` gate. Defaults to true.
31
+ */
32
+ aiStudioEnabled?: boolean;
33
+ }
34
+ /**
35
+ * True when the live catalog exposes BOTH a data/query (`ask`) and an authoring
36
+ * (`build`) agent — alias-aware via {@link isAskAgent}/{@link isBuildAgent}, so
37
+ * a catalog still serving the legacy `data_chat`/`metadata_assistant` ids counts
38
+ * too. This is the "AI development is unlocked for this viewer" signal, the same
39
+ * `askAvailable && buildAvailable` notion HomePage uses to surface "Build with AI".
40
+ */
41
+ export declare function isAiDevUnlocked(agents: AgentLike[]): boolean;
42
+ /**
43
+ * Decide whether the floating assistant should reveal its Build/Ask switcher.
44
+ *
45
+ * Restrained by design (the original "end users shouldn't have to choose" rule):
46
+ * a pure end-user surface bound to a single `ask` agent never sees it. Precedence:
47
+ * 1. `showAgentPickerProp` — explicit host override wins (`true`/`false`).
48
+ * 2. `envOptIn` — `VITE_AI_SHOW_AGENT_PICKER` forces it on globally.
49
+ * 3. Auto-reveal — AI development is unlocked ({@link isAiDevUnlocked}) AND
50
+ * authoring isn't deployment-disabled (`aiStudioEnabled`).
51
+ *
52
+ * Returns the *intent* only: the render site still requires more than one agent
53
+ * (`agents.length > 1`) to draw an actual choice.
54
+ */
55
+ export declare function shouldShowAgentPicker({ agents, showAgentPickerProp, envOptIn, aiStudioEnabled, }: AgentPickerDecisionInput): boolean;
56
+ export {};
@@ -0,0 +1,40 @@
1
+ /**
2
+ * agentPicker
3
+ *
4
+ * Pure decision logic for the floating assistant's Build/Ask switcher. Kept in
5
+ * its own module (no React, no chat deps) so it can be unit-tested without
6
+ * dragging in the heavy chat component graph (FloatingChatbot → streamdown →
7
+ * shiki → @ai-sdk, ~20MB) that `ConsoleFloatingChatbot` pulls in.
8
+ * @module
9
+ */
10
+ import { isAskAgent, isBuildAgent } from '@object-ui/plugin-chatbot';
11
+ /**
12
+ * True when the live catalog exposes BOTH a data/query (`ask`) and an authoring
13
+ * (`build`) agent — alias-aware via {@link isAskAgent}/{@link isBuildAgent}, so
14
+ * a catalog still serving the legacy `data_chat`/`metadata_assistant` ids counts
15
+ * too. This is the "AI development is unlocked for this viewer" signal, the same
16
+ * `askAvailable && buildAvailable` notion HomePage uses to surface "Build with AI".
17
+ */
18
+ export function isAiDevUnlocked(agents) {
19
+ return (agents.some((a) => isAskAgent(a.name)) && agents.some((a) => isBuildAgent(a.name)));
20
+ }
21
+ /**
22
+ * Decide whether the floating assistant should reveal its Build/Ask switcher.
23
+ *
24
+ * Restrained by design (the original "end users shouldn't have to choose" rule):
25
+ * a pure end-user surface bound to a single `ask` agent never sees it. Precedence:
26
+ * 1. `showAgentPickerProp` — explicit host override wins (`true`/`false`).
27
+ * 2. `envOptIn` — `VITE_AI_SHOW_AGENT_PICKER` forces it on globally.
28
+ * 3. Auto-reveal — AI development is unlocked ({@link isAiDevUnlocked}) AND
29
+ * authoring isn't deployment-disabled (`aiStudioEnabled`).
30
+ *
31
+ * Returns the *intent* only: the render site still requires more than one agent
32
+ * (`agents.length > 1`) to draw an actual choice.
33
+ */
34
+ export function shouldShowAgentPicker({ agents, showAgentPickerProp, envOptIn = false, aiStudioEnabled = true, }) {
35
+ if (showAgentPickerProp !== undefined)
36
+ return showAgentPickerProp;
37
+ if (envOptIn)
38
+ return true;
39
+ return aiStudioEnabled && isAiDevUnlocked(agents);
40
+ }
@@ -0,0 +1,15 @@
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
+ export interface CommitTimelineProps {
9
+ open: boolean;
10
+ onOpenChange: (open: boolean) => void;
11
+ packageId: string;
12
+ /** Called after a successful revert so the host can refresh the app view. */
13
+ onReverted?: () => void;
14
+ }
15
+ export declare function CommitTimeline({ open, onOpenChange, packageId, onReverted }: CommitTimelineProps): import("react").JSX.Element;
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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
+ /**
10
+ * ADR-0067 — the package "Build history" timeline. Lists every commit an AI
11
+ * build (or edit) landed, newest-first, with "Revert" per apply commit: undo
12
+ * that change set (artifacts it created are soft-removed; ones it edited are
13
+ * restored) as a NEW append-only revert commit. This is the history-not-confirm
14
+ * surface — the user reviews and reverts instead of approving each publish.
15
+ *
16
+ * Sibling of DraftChangesPanel (which lists PENDING drafts before a publish);
17
+ * this lists what already LANDED, and can undo it.
18
+ */
19
+ import { useCallback, useEffect, useState } from 'react';
20
+ import { GitBranch, Undo2, Loader2 } from 'lucide-react';
21
+ import { toast } from 'sonner';
22
+ import { Badge, Button, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from '@object-ui/components';
23
+ import { useObjectTranslation } from '@object-ui/i18n';
24
+ import { fetchCommits, revertCommit } from './commitHistory';
25
+ function relativeTime(iso) {
26
+ if (!iso)
27
+ return '';
28
+ const then = Date.parse(iso);
29
+ if (Number.isNaN(then))
30
+ return '';
31
+ const secs = Math.max(0, Math.round((Date.now() - then) / 1000));
32
+ if (secs < 60)
33
+ return `${secs}s`;
34
+ const mins = Math.round(secs / 60);
35
+ if (mins < 60)
36
+ return `${mins}m`;
37
+ const hrs = Math.round(mins / 60);
38
+ if (hrs < 24)
39
+ return `${hrs}h`;
40
+ return `${Math.round(hrs / 24)}d`;
41
+ }
42
+ export function CommitTimeline({ open, onOpenChange, packageId, onReverted }) {
43
+ const { t } = useObjectTranslation();
44
+ const [commits, setCommits] = useState(null);
45
+ const [error, setError] = useState(null);
46
+ const [reverting, setReverting] = useState(null);
47
+ const load = useCallback(async () => {
48
+ setCommits(null);
49
+ setError(null);
50
+ try {
51
+ setCommits(await fetchCommits(packageId));
52
+ }
53
+ catch (e) {
54
+ setError(e.message);
55
+ }
56
+ }, [packageId]);
57
+ useEffect(() => {
58
+ if (open)
59
+ void load();
60
+ }, [open, load]);
61
+ const onRevert = async (commitId) => {
62
+ setReverting(commitId);
63
+ try {
64
+ await revertCommit(packageId, commitId);
65
+ toast.success(t('preview.history.reverted', { defaultValue: 'Reverted — the change has been undone.' }));
66
+ onReverted?.();
67
+ await load();
68
+ }
69
+ catch (e) {
70
+ toast.error(`${t('preview.history.revertFailed', { defaultValue: 'Revert failed' })}: ${e.message}`);
71
+ }
72
+ finally {
73
+ setReverting(null);
74
+ }
75
+ };
76
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", className: "w-[420px] sm:max-w-[420px]", "data-testid": "commit-timeline-panel", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: t('preview.history.title', { defaultValue: 'Build history' }) }), _jsx(SheetDescription, { children: t('preview.history.description', {
77
+ defaultValue: 'Every change to this app, newest first. Revert any step to undo it — no publish confirmation needed.',
78
+ }) })] }), _jsx("div", { className: "mt-4 flex flex-col gap-2 overflow-y-auto px-4 pb-6", children: error ? (_jsxs("p", { className: "text-sm text-destructive", children: [t('preview.history.loadFailed', { defaultValue: 'Could not load history:' }), " ", error] })) : commits === null ? (_jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), t('preview.history.loading', { defaultValue: 'Loading history…' })] })) : commits.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: t('preview.history.empty', { defaultValue: 'No history yet for this app.' }) })) : (commits.map((c) => (_jsxs("div", { className: "flex items-start gap-2.5 rounded-md border px-2.5 py-2 text-sm", "data-testid": "commit-row", children: [_jsx(GitBranch, { className: `mt-0.5 h-4 w-4 shrink-0 ${c.operation === 'revert' ? 'text-muted-foreground' : 'text-primary'}` }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate font-medium", children: c.message ??
79
+ (c.operation === 'revert'
80
+ ? t('preview.history.revertLabel', { defaultValue: 'Reverted a change' })
81
+ : t('preview.history.applyLabel', { defaultValue: 'Build change' })) }), _jsxs("p", { className: "mt-0.5 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground", children: [c.operation === 'revert' ? (_jsx(Badge, { variant: "outline", className: "border-muted-foreground/30", children: t('preview.history.revert', { defaultValue: 'revert' }) })) : null, _jsxs("span", { children: [c.itemCount, " ", t('preview.history.items', { defaultValue: 'item(s)' })] }), c.actor ? _jsxs("span", { children: ["\u00B7 ", c.actor] }) : null, c.createdAt ? _jsxs("span", { children: ["\u00B7 ", relativeTime(c.createdAt)] }) : null] })] }), c.operation === 'apply' ? (_jsx(Button, { size: "sm", variant: "ghost", className: "shrink-0", disabled: reverting !== null, onClick: () => onRevert(c.id), "data-testid": "commit-revert", children: reverting === c.id ? (_jsx(Loader2, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsxs(_Fragment, { children: [_jsx(Undo2, { className: "mr-1 h-3.5 w-3.5" }), t('preview.history.revertAction', { defaultValue: 'Revert' })] })) })) : null] }, c.id)))) })] }) }));
82
+ }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
3
  * ObjectUI
4
4
  * Copyright (c) 2024-present ObjectStack Inc.
@@ -20,12 +20,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
20
  */
21
21
  import { useState } from 'react';
22
22
  import { useLocation, useParams } from 'react-router-dom';
23
- import { EyeOff, Rocket } from 'lucide-react';
23
+ import { EyeOff, History, Rocket } from 'lucide-react';
24
24
  import { toast } from 'sonner';
25
25
  import { Button } from '@object-ui/components';
26
26
  import { useObjectTranslation } from '@object-ui/i18n';
27
27
  import { useMetadata } from '../providers/MetadataProvider';
28
28
  import { matchAppBySegment } from '../utils/appRoute';
29
+ import { CommitTimeline } from './CommitTimeline';
29
30
  import { usePreviewDrafts } from './PreviewModeContext';
30
31
  export function UnpublishedAppBar() {
31
32
  const preview = usePreviewDrafts();
@@ -34,6 +35,7 @@ export function UnpublishedAppBar() {
34
35
  const { apps, refresh } = useMetadata();
35
36
  const { t } = useObjectTranslation();
36
37
  const [publishing, setPublishing] = useState(false);
38
+ const [historyOpen, setHistoryOpen] = useState(false);
37
39
  // The draft-preview watermark owns the preview tree; never stack both bars.
38
40
  if (preview)
39
41
  return null;
@@ -43,6 +45,8 @@ export function UnpublishedAppBar() {
43
45
  const app = matchAppBySegment(apps ?? [], routeApp);
44
46
  if (!app || app.hidden !== true)
45
47
  return null;
48
+ // ADR-0067 — the package this app belongs to keys its commit timeline.
49
+ const packageId = app?.packageId ?? app?._packageId ?? null;
46
50
  const publish = async () => {
47
51
  setPublishing(true);
48
52
  try {
@@ -71,9 +75,9 @@ export function UnpublishedAppBar() {
71
75
  setPublishing(false);
72
76
  }
73
77
  };
74
- return (_jsxs("div", { className: "sticky top-0 z-40 flex items-center gap-3 border-b border-amber-300/70 bg-amber-50 px-4 py-2 text-sm text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-200", "data-testid": "unpublished-app-bar", children: [_jsx(EyeOff, { className: "h-4 w-4 shrink-0" }), _jsx("p", { className: "min-w-0 flex-1 truncate", children: t('preview.unpublishedBar.message', {
75
- defaultValue: 'Unpublished app — fully functional, but only builders can see it. Publish to make it visible to your users.',
76
- }) }), _jsxs(Button, { size: "sm", onClick: publish, disabled: publishing, "data-testid": "unpublished-app-publish", children: [_jsx(Rocket, { className: "mr-1 h-3.5 w-3.5" }), publishing
77
- ? t('preview.unpublishedBar.publishing', { defaultValue: 'Publishing…' })
78
- : t('preview.unpublishedBar.publish', { defaultValue: 'Publish' })] })] }));
78
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sticky top-0 z-40 flex items-center gap-3 border-b border-amber-300/70 bg-amber-50 px-4 py-2 text-sm text-amber-900 dark:border-amber-700/60 dark:bg-amber-950/40 dark:text-amber-200", "data-testid": "unpublished-app-bar", children: [_jsx(EyeOff, { className: "h-4 w-4 shrink-0" }), _jsx("p", { className: "min-w-0 flex-1 truncate", children: t('preview.unpublishedBar.message', {
79
+ defaultValue: 'Unpublished app — fully functional, but only builders can see it. Publish to make it visible to your users.',
80
+ }) }), packageId ? (_jsxs(Button, { size: "sm", variant: "outline", onClick: () => setHistoryOpen(true), "data-testid": "unpublished-app-history", children: [_jsx(History, { className: "mr-1 h-3.5 w-3.5" }), t('preview.history.button', { defaultValue: 'History' })] })) : null, _jsxs(Button, { size: "sm", onClick: publish, disabled: publishing, "data-testid": "unpublished-app-publish", children: [_jsx(Rocket, { className: "mr-1 h-3.5 w-3.5" }), publishing
81
+ ? t('preview.unpublishedBar.publishing', { defaultValue: 'Publishing…' })
82
+ : t('preview.unpublishedBar.publish', { defaultValue: 'Publish' })] })] }), packageId ? (_jsx(CommitTimeline, { open: historyOpen, onOpenChange: setHistoryOpen, packageId: packageId, onReverted: refresh })) : null] }));
79
83
  }
@@ -0,0 +1,28 @@
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-0067 — package-scoped commit history reads/writes for the timeline.
10
+ *
11
+ * Each AI build (and Studio batch) lands as one revertible COMMIT on top of
12
+ * the metadata history. The history-not-confirm model: you don't approve each
13
+ * publish — you review this timeline and revert if a change was wrong. These
14
+ * helpers read the timeline (`GET /packages/:id/commits`) and revert one
15
+ * commit (`POST /packages/:id/commits/:cid/revert`). Cookie-authenticated like
16
+ * every console call; tolerant of the `{ data: ... }` / bare envelope shapes.
17
+ */
18
+ export interface CommitEntry {
19
+ id: string;
20
+ operation: 'apply' | 'revert';
21
+ message?: string;
22
+ actor?: string;
23
+ aiModel?: string;
24
+ itemCount: number;
25
+ createdAt?: string;
26
+ }
27
+ export declare function fetchCommits(packageId: string): Promise<CommitEntry[]>;
28
+ export declare function revertCommit(packageId: string, commitId: string): Promise<void>;
@@ -0,0 +1,48 @@
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
+ export async function fetchCommits(packageId) {
9
+ const res = await fetch(`/api/v1/packages/${encodeURIComponent(packageId)}/commits`, {
10
+ credentials: 'include',
11
+ headers: { Accept: 'application/json' },
12
+ cache: 'no-store',
13
+ });
14
+ if (!res.ok)
15
+ throw new Error(`commits HTTP ${res.status}`);
16
+ const data = (await res.json());
17
+ const list = Array.isArray(data)
18
+ ? data
19
+ : data?.commits ??
20
+ data?.data?.commits ??
21
+ [];
22
+ return (Array.isArray(list) ? list : []).map((raw) => {
23
+ const c = raw;
24
+ return {
25
+ id: String(c.id),
26
+ operation: c.operation === 'revert' ? 'revert' : 'apply',
27
+ message: typeof c.message === 'string' ? c.message : undefined,
28
+ actor: typeof c.actor === 'string' ? c.actor : undefined,
29
+ aiModel: typeof c.aiModel === 'string' ? c.aiModel : undefined,
30
+ itemCount: typeof c.itemCount === 'number' ? c.itemCount : 0,
31
+ createdAt: typeof c.createdAt === 'string' ? c.createdAt : undefined,
32
+ };
33
+ });
34
+ }
35
+ export async function revertCommit(packageId, commitId) {
36
+ const res = await fetch(`/api/v1/packages/${encodeURIComponent(packageId)}/commits/${encodeURIComponent(commitId)}/revert`, {
37
+ method: 'POST',
38
+ credentials: 'include',
39
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
40
+ body: '{}',
41
+ });
42
+ const payload = (await res.json().catch(() => null));
43
+ const inner = payload?.data ?? payload ?? undefined;
44
+ if (!res.ok || inner?.success === false) {
45
+ const err = payload?.error;
46
+ throw new Error((typeof err === 'object' ? err?.message : err) ?? `HTTP ${res.status}`);
47
+ }
48
+ }
@@ -459,6 +459,15 @@ export function MetadataProvider({ children, adapter, ttlMs = DEFAULT_TTL_MS })
459
459
  // NOT gated to preview-drafts mode: the live world every tree reads changed.
460
460
  useEffect(() => {
461
461
  return subscribeMetadataRefresh(() => {
462
+ // The adapter keeps its OWN object-schema cache (getObjectSchema) that the
463
+ // context refresh below does not touch. A publish/install just changed the
464
+ // live schema, so drop it too — otherwise create/edit forms (which read
465
+ // the adapter's getObjectSchema, NOT this context) keep showing the
466
+ // pre-publish field set until the adapter cache's 5-minute TTL lapses.
467
+ try {
468
+ adapterRef.current?.clearCache?.();
469
+ }
470
+ catch { /* adapter mid-swap */ }
462
471
  void refresh();
463
472
  });
464
473
  }, [refresh]);
@@ -1,33 +1,5 @@
1
- export interface ScreenFieldSpec {
2
- name: string;
3
- label?: string;
4
- type?: string;
5
- required?: boolean;
6
- options?: Array<{
7
- value: unknown;
8
- label: string;
9
- }>;
10
- defaultValue?: unknown;
11
- placeholder?: string;
12
- }
13
- export interface ScreenSpec {
14
- nodeId: string;
15
- title?: string;
16
- description?: string;
17
- fields: ScreenFieldSpec[];
18
- /**
19
- * `'object-form'` renders the named object's FULL create/edit form — incl.
20
- * inline master-detail child grids — as a wizard step (vs. the flat `fields`
21
- * list). The form persists the record (and its children, atomically) itself,
22
- * then resumes the run with the saved id bound to `idVariable`.
23
- */
24
- kind?: 'fields' | 'object-form';
25
- objectName?: string;
26
- mode?: 'create' | 'edit';
27
- recordId?: string;
28
- defaults?: Record<string, unknown>;
29
- idVariable?: string;
30
- }
1
+ import { type ScreenSpec } from './ScreenView';
2
+ export type { ScreenSpec, ScreenFieldSpec } from './ScreenView';
31
3
  export interface ScreenFlowState {
32
4
  flowName: string;
33
5
  runId: string;