@growthub/cli 0.10.0 → 0.12.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 (28) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +307 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +372 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/receipts/route.js +47 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +664 -82
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +1371 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1383 -24
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +7 -21
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/ownership/ownership-panel.jsx +222 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/ownership/page.jsx +19 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +2 -1
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +116 -24
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +497 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json +20 -4
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +19 -4
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +23 -5
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +473 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +583 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +34 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +3 -1
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/export-training-traces.mjs +144 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/grade-raw-pairs.mjs +279 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/harvest-cursor-traces.mjs +288 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/helpers/upload-graded-traces.mjs +128 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +19 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/alignment-loop.config.json +264 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/workers/custom-workspace-operator/CLAUDE.md +38 -0
  27. package/dist/index.js +1416 -2627
  28. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { readAdapterConfig } from "@/lib/adapters/env";
3
3
  import { describeIntegrationAdapter, listGovernedWorkspaceIntegrations } from "@/lib/adapters/integrations";
4
4
  import { groupIntegrationsByLane } from "@/lib/domain/integrations";
5
5
  import { readWorkspaceConfig } from "@/lib/workspace-config";
6
+ import { WorkspaceRail } from "../../workspace-rail.jsx";
6
7
 
7
8
  function countConnected(rows) {
8
9
  return rows.filter((item) => item.isConnected || item.status === "connected").length;
@@ -57,28 +58,13 @@ async function IntegrationsSettingsPage() {
57
58
  const allRows = [...grouped.dataSources, ...grouped.workspaceIntegrations];
58
59
 
59
60
  return <main className="workspace-builder workspace-settings-page">
60
- <aside className="workspace-rail" aria-label="Workspace navigation">
61
- <div className="workspace-brand">
62
- <span className="workspace-mark" style={{
63
- background: branding.logoUrl ? undefined : branding.accent || undefined,
64
- color: branding.logoUrl ? undefined : textColorForAccent(branding.accent)
65
- }}>
66
- {branding.logoUrl ? <img src={branding.logoUrl} alt="" /> : workspaceName.slice(0, 1).toUpperCase()}
67
- </span>
68
- <span>{workspaceName}</span>
69
- </div>
70
- <nav className="workspace-nav">
71
- <Link href="/">Dashboards</Link>
72
- <Link href="/data-model">Data Model</Link>
61
+ <WorkspaceRail
62
+ workspaceConfig={workspaceConfig}
63
+ authority={adapter.authority}
64
+ managementSlot={(
73
65
  <Link className="active" href="/settings/integrations">Integrations</Link>
74
- <span className="workspace-nav-static">Management</span>
75
- <Link className="workspace-nav-bottom" href="/settings/general">Workspace Settings</Link>
76
- </nav>
77
- <div className="workspace-rail-status">
78
- <span className="status-dot" />
79
- {adapter.authority}
80
- </div>
81
- </aside>
66
+ )}
67
+ />
82
68
 
83
69
  <section className="workspace-surface">
84
70
  <header className="workspace-toolbar">
@@ -0,0 +1,222 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Ownership settings panel — the inspect-only "Management" surface that
5
+ * previously lived behind a modal triggered from the nav rail. Moved here
6
+ * as the 4th Workspace Settings tab so the rail can collapse its
7
+ * Management item into the renamed "Management" link (formerly Data
8
+ * Model) without losing the readiness/diagnostics surface.
9
+ *
10
+ * Pure presentational + one fetch effect for the live resolver list.
11
+ * Workflow execution stays in `growthub workflow` / `growthub bridge`;
12
+ * this panel never executes, never calls hosted endpoints, and never
13
+ * exposes tokens.
14
+ */
15
+
16
+ import { useEffect, useMemo, useRef, useState } from "react";
17
+
18
+ const DEFAULT_PERSISTENCE = {
19
+ mode: "unknown",
20
+ canSave: false,
21
+ reason: "Persistence mode could not be resolved.",
22
+ guidance: "",
23
+ };
24
+
25
+ function ResolverManagementSection({ canSave, config }) {
26
+ const [resolverData, setResolverData] = useState(null);
27
+ const [uploading, setUploading] = useState(false);
28
+ const [uploadResult, setUploadResult] = useState(null);
29
+ const fileInputRef = useRef(null);
30
+
31
+ useEffect(() => {
32
+ fetch("/api/workspace/resolvers")
33
+ .then((r) => (r.ok ? r.json() : { files: [], registeredIds: [], resolvers: [], canUpload: false }))
34
+ .then(setResolverData)
35
+ .catch(() => setResolverData({ files: [], registeredIds: [], resolvers: [], canUpload: false }));
36
+ }, [uploadResult]);
37
+
38
+ const dataModelObjects = Array.isArray(config?.dataModel?.objects) ? config.dataModel.objects : [];
39
+
40
+ const linkedObjectsByResolver = useMemo(() => {
41
+ const map = {};
42
+ dataModelObjects.forEach((obj) => {
43
+ const intId = obj.binding?.integrationId;
44
+ if (!intId) return;
45
+ if (!map[intId]) map[intId] = [];
46
+ map[intId].push(obj);
47
+ });
48
+ return map;
49
+ }, [dataModelObjects]);
50
+
51
+ async function handleFileChange(event) {
52
+ const file = event.target.files?.[0];
53
+ if (!file) return;
54
+ setUploading(true);
55
+ setUploadResult(null);
56
+ const form = new FormData();
57
+ form.append("file", file);
58
+ try {
59
+ const res = await fetch("/api/workspace/register-resolver", { method: "POST", body: form });
60
+ const data = await res.json();
61
+ setUploadResult(res.ok ? { ok: true, ...data } : { ok: false, ...data });
62
+ } catch {
63
+ setUploadResult({ ok: false, error: "Network error" });
64
+ } finally {
65
+ setUploading(false);
66
+ if (fileInputRef.current) fileInputRef.current.value = "";
67
+ }
68
+ }
69
+
70
+ const resolvers = resolverData?.resolvers || [];
71
+
72
+ return (
73
+ <article className="workspace-readiness-section">
74
+ <h3>Source Resolvers</h3>
75
+ <div className="workspace-readiness-row">
76
+ <span>Files</span>
77
+ <code>{resolverData ? resolverData.files.length : "…"}</code>
78
+ </div>
79
+ <div className="workspace-readiness-row">
80
+ <span>Registered</span>
81
+ <code>{resolverData ? resolvers.length : "…"}</code>
82
+ </div>
83
+ <div className="workspace-readiness-row">
84
+ <span>Data Model objects</span>
85
+ <code>{dataModelObjects.length}</code>
86
+ </div>
87
+ {canSave ? (
88
+ <>
89
+ <div className="workspace-readiness-row">
90
+ <span>Upload resolver</span>
91
+ <input ref={fileInputRef} type="file" accept=".js" style={{ display: "none" }} onChange={handleFileChange} />
92
+ <button
93
+ type="button"
94
+ className="workspace-readiness-action"
95
+ disabled={uploading}
96
+ onClick={() => fileInputRef.current?.click()}
97
+ >
98
+ {uploading ? "Uploading…" : "Upload .js file"}
99
+ </button>
100
+ </div>
101
+ {uploadResult && (
102
+ <div className={`workspace-readiness-row resolver-upload-result ${uploadResult.ok ? "good" : "error"}`}>
103
+ <span>{uploadResult.ok ? "Saved" : "Error"}</span>
104
+ <em>{uploadResult.ok ? uploadResult.path : uploadResult.error}</em>
105
+ </div>
106
+ )}
107
+ <p className="workspace-panel-hint">
108
+ Upload a <code>.js</code> resolver file that calls <code>registerSourceResolver()</code>.
109
+ </p>
110
+ </>
111
+ ) : (
112
+ <div className="workspace-readiness-row">
113
+ <span>Upload</span>
114
+ <em>
115
+ Requires <code>WORKSPACE_CONFIG_ALLOW_FS_WRITE=true</code> or add resolver files manually to{" "}
116
+ <code>lib/adapters/integrations/resolvers/</code>.
117
+ </em>
118
+ </div>
119
+ )}
120
+ </article>
121
+ );
122
+ }
123
+
124
+ export function OwnershipPanel({ config, persistence, adapterConfig }) {
125
+ const persist = persistence || DEFAULT_PERSISTENCE;
126
+ const pipelines = Array.isArray(config?.pipelines) ? config.pipelines : [];
127
+ const integrations = Array.isArray(config?.integrations) ? config.integrations : [];
128
+ const capabilities = Array.isArray(config?.capabilities) ? config.capabilities : [];
129
+ return (
130
+ <section className="workspace-settings-card workspace-ownership-card">
131
+ <div className="workspace-settings-card-heading">
132
+ <div>
133
+ <h2>Ownership</h2>
134
+ <p>
135
+ Inspect-only readiness, persistence, integrations, workflows, and source resolvers for
136
+ this governed workspace. Workflow execution stays in <code>growthub workflow</code> /
137
+ <code> growthub bridge</code>; this panel never executes, never calls hosted endpoints, and
138
+ never exposes tokens.
139
+ </p>
140
+ </div>
141
+ </div>
142
+
143
+ <div className="workspace-readiness">
144
+ <article className="workspace-readiness-section">
145
+ <h3>Workspace</h3>
146
+ <div className="workspace-readiness-row"><span>ID</span><code>{config?.id || "Unknown"}</code></div>
147
+ <div className="workspace-readiness-row"><span>Name</span><strong>{config?.name || "Workspace"}</strong></div>
148
+ <div className="workspace-readiness-row">
149
+ <span>Capabilities</span>
150
+ <span>{capabilities.length ? capabilities.join(", ") : "none"}</span>
151
+ </div>
152
+ </article>
153
+
154
+ <article className="workspace-readiness-section">
155
+ <h3>API</h3>
156
+ <div className="workspace-readiness-row"><span>PATCH allowlist</span><code>dashboards | widgetTypes | canvas | dataModel</code></div>
157
+ <div className="workspace-readiness-row"><span>Unknown field</span><code>400</code></div>
158
+ <div className="workspace-readiness-row"><span>Read-only runtime</span><code>409 + guidance</code></div>
159
+ <div className="workspace-readiness-row">
160
+ <span>Can save now</span>
161
+ <span className={`workspace-readiness-badge ${persist.canSave ? "good" : "warn"}`}>
162
+ {persist.canSave ? "yes" : "no"}
163
+ </span>
164
+ </div>
165
+ </article>
166
+
167
+ <article className="workspace-readiness-section">
168
+ <h3>Workflows</h3>
169
+ {pipelines.length === 0 ? (
170
+ <div className="workspace-readiness-row workspace-readiness-empty">
171
+ <em>
172
+ No workflows declared in <code>growthub.config.json</code>. Connect via{" "}
173
+ <code>growthub workflow</code> after Bridge auth.
174
+ </em>
175
+ </div>
176
+ ) : (
177
+ pipelines.map((pipeline, index) => (
178
+ <div className="workspace-readiness-row" key={pipeline.id || index}>
179
+ <span>{pipeline.id || `pipeline-${index}`}</span>
180
+ <strong>{pipeline.name || "Untitled"}</strong>
181
+ </div>
182
+ ))
183
+ )}
184
+ </article>
185
+
186
+ <article className="workspace-readiness-section">
187
+ <h3>Integrations</h3>
188
+ <div className="workspace-readiness-row"><span>Adapter</span><code>{adapterConfig?.integrationAdapter || "—"}</code></div>
189
+ <div className="workspace-readiness-row"><span>Deploy target</span><code>{adapterConfig?.deployTarget || "—"}</code></div>
190
+ {integrations.length === 0 ? (
191
+ <div className="workspace-readiness-row workspace-readiness-empty">
192
+ <em>
193
+ No static integrations declared. Use <code>growthub bridge agents bind</code> for hosted bindings.
194
+ </em>
195
+ </div>
196
+ ) : (
197
+ integrations.map((integration, index) => (
198
+ <div className="workspace-readiness-row" key={integration.id || index}>
199
+ <span>{integration.id || `integration-${index}`}</span>
200
+ <strong>{integration.name || "Untitled"}</strong>
201
+ </div>
202
+ ))
203
+ )}
204
+ </article>
205
+
206
+ <article className="workspace-readiness-section">
207
+ <h3>Persistence</h3>
208
+ <div className="workspace-readiness-row">
209
+ <span>Mode</span>
210
+ <span className={`workspace-readiness-badge mode-${persist.mode}`}>{persist.mode}</span>
211
+ </div>
212
+ <div className="workspace-readiness-row"><span>Reason</span><em>{persist.reason}</em></div>
213
+ {persist.guidance ? (
214
+ <div className="workspace-readiness-row"><span>Guidance</span><em>{persist.guidance}</em></div>
215
+ ) : null}
216
+ </article>
217
+
218
+ <ResolverManagementSection canSave={persist.canSave} config={config} />
219
+ </div>
220
+ </section>
221
+ );
222
+ }
@@ -0,0 +1,19 @@
1
+ import { SettingsShell } from "../settings-shell.jsx";
2
+ import { readAdapterConfig } from "@/lib/adapters/env";
3
+ import { describePersistenceMode, readWorkspaceConfig } from "@/lib/workspace-config";
4
+ import { OwnershipPanel } from "./ownership-panel.jsx";
5
+
6
+ async function OwnershipSettingsPage() {
7
+ const config = await readWorkspaceConfig();
8
+ const adapterConfig = readAdapterConfig();
9
+ const persistence = describePersistenceMode();
10
+ return (
11
+ <SettingsShell active="/settings/ownership" eyebrow="Settings" title="Ownership">
12
+ <OwnershipPanel config={config} persistence={persistence} adapterConfig={adapterConfig} />
13
+ </SettingsShell>
14
+ );
15
+ }
16
+
17
+ export {
18
+ OwnershipSettingsPage as default,
19
+ };
@@ -4,7 +4,8 @@ import { X } from "lucide-react";
4
4
  const SETTINGS_TABS = [
5
5
  { href: "/settings/general", label: "General" },
6
6
  { href: "/settings/apis-webhooks", label: "APIs & Webhooks" },
7
- { href: "/settings/apps", label: "Apps" }
7
+ { href: "/settings/apps", label: "Apps" },
8
+ { href: "/settings/ownership", label: "Ownership" }
8
9
  ];
9
10
 
10
11
  function SettingsShell({ active, eyebrow, title, children, aside }) {
@@ -76,6 +76,8 @@ import {
76
76
  } from "@/lib/workspace-schema";
77
77
  import { governedWorkspaceIntegrationCatalog } from "@/lib/domain/integrations";
78
78
  import { OBJECT_TYPE_PRESETS, listWorkspaceDataModelTables } from "@/lib/workspace-data-model";
79
+ import { HelperSidecar } from "./data-model/components/HelperSidecar.jsx";
80
+ import { WorkspaceRail } from "./workspace-rail.jsx";
79
81
 
80
82
  const DEFAULT_CHART_TYPE = "bar-vertical";
81
83
  const DEFAULT_FILTER_OP = "and";
@@ -266,7 +268,7 @@ const GRID_COLUMNS = 12;
266
268
  const GRID_ROWS = 16;
267
269
  const GRID_CELL_COUNT = GRID_COLUMNS * GRID_ROWS;
268
270
  const DEFAULT_TAB_ID = "tab-default";
269
- const COLLAPSED_GRID_COLUMNS = "220px minmax(0, 1fr)";
271
+ const COLLAPSED_GRID_COLUMNS = "264px minmax(0, 1fr)";
270
272
 
271
273
  function generateId(prefix) {
272
274
  if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) {
@@ -1003,7 +1005,7 @@ function widgetKindFill(kind) {
1003
1005
  switch (kind) {
1004
1006
  case "chart": return "#dbeafe";
1005
1007
  case "view": return "#fef3c7";
1006
- case "iframe": return "#ede9fe";
1008
+ case "iframe": return "#e0f2fe";
1007
1009
  case "rich-text": return "#dcfce7";
1008
1010
  default: return "#e5e7eb";
1009
1011
  }
@@ -2213,7 +2215,7 @@ function FieldsSubPanel({ widget, dataModelTable, onChange, onBack }) {
2213
2215
  <SubPanelHeader title="Fields" breadcrumb={widget.title} onBack={onBack} />
2214
2216
  {isBound && (
2215
2217
  <p className="workspace-panel-hint">
2216
- Fields come from the bound object. Manage them on the <a href="/data-model" style={{ color: "#6366f1" }}>Data Model page</a>.
2218
+ Fields come from the bound object. Manage them on the <a href="/data-model" style={{ color: "#3f68ff" }}>Data Model page</a>.
2217
2219
  </p>
2218
2220
  )}
2219
2221
 
@@ -3352,6 +3354,10 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
3352
3354
  const [configMessage, setConfigMessage] = useState("");
3353
3355
  const [inspectorPath, setInspectorPath] = useState(SUB_PANEL_ROOT);
3354
3356
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
3357
+ const [helperOpen, setHelperOpen] = useState(false);
3358
+ const [helperIntent, setHelperIntent] = useState("build_dashboard");
3359
+ const [helperInitialPrompt, setHelperInitialPrompt] = useState("");
3360
+ const [helperInitialThread, setHelperInitialThread] = useState(null);
3355
3361
  const [templateFilter, setTemplateFilter] = useState({ category: "all", tag: "all", query: "" });
3356
3362
  const [expandedIframeWidget, setExpandedIframeWidget] = useState(null);
3357
3363
  const [refreshing, setRefreshing] = useState(false);
@@ -4158,6 +4164,29 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
4158
4164
 
4159
4165
  const paletteCommands = useMemo(() => {
4160
4166
  const list = [];
4167
+ const openHelperWith = (i, p) => {
4168
+ setHelperIntent(i);
4169
+ setHelperInitialPrompt(p);
4170
+ setHelperInitialThread(null);
4171
+ setHelperOpen(true);
4172
+ };
4173
+ list.push({
4174
+ id: "helper.build_dashboard", group: "Ask helper", icon: Zap, label: "Ask helper — build a dashboard",
4175
+ run: () => openHelperWith("build_dashboard", "Draft a dashboard for a local agency: pipeline overview, weekly revenue, and a leaderboard widget.")
4176
+ });
4177
+ list.push({
4178
+ id: "helper.edit_view", group: "Ask helper", icon: Zap, label: "Ask helper — improve this dashboard",
4179
+ disabled: !activeDashboard,
4180
+ run: () => openHelperWith("edit_view", `Improve the "${activeDashboard?.name || "current"}" dashboard. Suggest widget placements that match the data already in the workspace.`)
4181
+ });
4182
+ list.push({
4183
+ id: "helper.create_widget", group: "Ask helper", icon: Zap, label: "Ask helper — suggest widgets",
4184
+ run: () => openHelperWith("create_widget", "Suggest widgets that fit the data already in this workspace.")
4185
+ });
4186
+ list.push({
4187
+ id: "helper.repair", group: "Ask helper", icon: Zap, label: "Ask helper — repair workspace",
4188
+ run: () => openHelperWith("repair", "Inspect this workspace for missing references, broken bindings, or incomplete views. Propose the smallest fix for each issue.")
4189
+ });
4161
4190
  list.push({
4162
4191
  id: "dashboard.new", group: "Dashboard", icon: Plus, label: "Create dashboard", shortcut: "N",
4163
4192
  run: () => addDashboard()
@@ -4292,27 +4321,45 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
4292
4321
  ]);
4293
4322
 
4294
4323
  return <main className="workspace-builder" onPointerDownCapture={resetWidgetSelectionOnOutsidePointer} style={builderStyle}>
4295
- <aside className="workspace-rail" aria-label="Workspace navigation">
4296
- <div className="workspace-brand">
4297
- <span className="workspace-mark" style={{
4298
- background: branding.logoUrl ? undefined : branding.accent || undefined,
4299
- color: branding.logoUrl ? undefined : textColorForAccent(branding.accent)
4300
- }}>
4301
- {branding.logoUrl ? <img src={branding.logoUrl} alt="" /> : (branding.name || config.name || "Growthub Workspace").slice(0, 1).toUpperCase()}
4302
- </span>
4303
- <span>{branding.name || config.name || "Growthub Workspace"}</span>
4304
- </div>
4305
- <nav className="workspace-nav">
4306
- <button type="button" className={workspaceView === "dashboards" ? "active workspace-nav-button" : "workspace-nav-button"} onClick={showDashboardHome}>Dashboards</button>
4307
- <Link href="/data-model">Data Model</Link>
4308
- <button type="button" className="workspace-nav-button" onClick={() => setManagementOpen(true)}>Management</button>
4309
- <Link className="workspace-nav-bottom" href="/settings/general">Workspace Settings</Link>
4310
- </nav>
4311
- <div className="workspace-rail-status">
4312
- <span className="status-dot" />
4313
- {integrationAdapter.authority}
4314
- </div>
4315
- </aside>
4324
+ <WorkspaceRail
4325
+ workspaceConfig={config}
4326
+ authority={integrationAdapter.authority}
4327
+ helperOpen={helperOpen}
4328
+ onOpenHelper={() => {
4329
+ if (helperOpen) { setHelperOpen(false); return; }
4330
+ // Rail pill ALWAYS lands on a fresh thread (chip stack +
4331
+ // empty composer). Reopen specific threads via the Chat tab.
4332
+ setHelperIntent("build_dashboard");
4333
+ setHelperInitialPrompt("");
4334
+ setHelperInitialThread(null);
4335
+ setHelperOpen(true);
4336
+ }}
4337
+ onOpenThread={(row) => {
4338
+ setHelperInitialThread(row);
4339
+ setHelperOpen(true);
4340
+ }}
4341
+ onConfigChange={(nextConfig) => {
4342
+ if (typeof setConfig === "function") setConfig(nextConfig);
4343
+ }}
4344
+ dashboardsSlot={(
4345
+ <button
4346
+ type="button"
4347
+ className={workspaceView === "dashboards" ? "active workspace-nav-button" : "workspace-nav-button"}
4348
+ onClick={showDashboardHome}
4349
+ >
4350
+ Dashboards
4351
+ </button>
4352
+ )}
4353
+ managementSlot={(
4354
+ <button
4355
+ type="button"
4356
+ className="workspace-nav-button"
4357
+ onClick={() => setManagementOpen(true)}
4358
+ >
4359
+ Management
4360
+ </button>
4361
+ )}
4362
+ />
4316
4363
 
4317
4364
  <section className="workspace-surface">
4318
4365
  <header className="workspace-toolbar">
@@ -4537,6 +4584,51 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
4537
4584
  onClose={closeManagement}
4538
4585
  /> : null}
4539
4586
 
4587
+ <HelperSidecar
4588
+ open={helperOpen}
4589
+ onClose={() => setHelperOpen(false)}
4590
+ workspaceConfig={config}
4591
+ initialIntent={helperIntent}
4592
+ initialPrompt={helperInitialPrompt}
4593
+ initialThread={helperInitialThread}
4594
+ onOpenArtifact={(target) => {
4595
+ if (!target) return;
4596
+ // dashboards surface — focus the created dashboard inline.
4597
+ if (target.surface === "dashboard" && target.dashboardId) {
4598
+ setActiveDashboardId?.(target.dashboardId);
4599
+ setWorkspaceView?.("builder");
4600
+ setHelperOpen(false);
4601
+ return;
4602
+ }
4603
+ // a data-model artifact was applied — navigate to that surface.
4604
+ if (target.surface === "data-model" && target.source) {
4605
+ setHelperOpen(false);
4606
+ if (typeof window !== "undefined") {
4607
+ window.location.href = `/data-model?source=${encodeURIComponent(target.source)}`;
4608
+ }
4609
+ }
4610
+ }}
4611
+ onApplied={(updatedConfig) => {
4612
+ if (!updatedConfig) return;
4613
+ // Re-seat canvas from the dashboard the user is currently viewing.
4614
+ // If the helper created a new dashboard we still keep the user
4615
+ // anchored where they were unless they had no active dashboard yet.
4616
+ setConfig((current) => {
4617
+ const nextDashboards = Array.isArray(updatedConfig.dashboards) && updatedConfig.dashboards.length
4618
+ ? updatedConfig.dashboards
4619
+ : current.dashboards;
4620
+ const stillActive = nextDashboards.find((d) => d?.id === resolvedActiveDashboardId);
4621
+ const anchor = stillActive || nextDashboards[0];
4622
+ return {
4623
+ ...current,
4624
+ ...updatedConfig,
4625
+ dashboards: nextDashboards,
4626
+ canvas: dashboardCanvasFrom(anchor, updatedConfig.canvas || current.canvas)
4627
+ };
4628
+ });
4629
+ }}
4630
+ />
4631
+
4540
4632
  {workspaceView === "builder" && panelOpen ? <aside className="workspace-widget-panel" id="widgets" aria-label="Widget configuration">
4541
4633
  <div className="workspace-panel-title">
4542
4634
  <button type="button" aria-label="Close widget panel" onClick={closePanel}>x</button>