@growthub/cli 0.13.4 → 0.13.6

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 (52) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/QUICKSTART.md +19 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/action/execute/route.js +60 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/actions/route.js +50 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connect-session/route.js +68 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connection-status/route.js +56 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/proxy/route.js +67 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/status/route.js +50 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +184 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +25 -2
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +161 -50
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +496 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +88 -1
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +41 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +120 -17
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/nango/page.jsx +167 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +1 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +67 -11
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +31 -10
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +16 -14
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +7 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/index.js +38 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-adapter.js +552 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-config-loader.js +202 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-schema.js +303 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +49 -10
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +1 -1
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/nango.js +49 -0
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +4 -2
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +14 -1
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +218 -7
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +28 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +43 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +3 -1
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +36 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +53 -0
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -1
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +102 -3
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/bundles/growthub-custom-workspace-starter-v1.json +1 -0
  49. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +7 -0
  50. package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +276 -0
  51. package/dist/index.js +127 -44
  52. package/package.json +1 -1
@@ -15,7 +15,9 @@ import {
15
15
  ChevronDown,
16
16
  ChevronRight,
17
17
  Code2,
18
+ Copy,
18
19
  Database,
20
+ Download,
19
21
  EyeOff,
20
22
  FileText,
21
23
  Filter,
@@ -28,6 +30,7 @@ import {
28
30
  Mail,
29
31
  Maximize2,
30
32
  MoreHorizontal,
33
+ MoreVertical,
31
34
  Play,
32
35
  Plus,
33
36
  Pencil,
@@ -38,6 +41,7 @@ import {
38
41
  ToggleLeft,
39
42
  Trash2,
40
43
  Type,
44
+ Unlock,
41
45
  Users,
42
46
  X,
43
47
  Zap,
@@ -73,10 +77,8 @@ import { isSandboxLocalAgentHost } from "@/lib/sandbox-agent-auth-eligibility";
73
77
  import { StatusPill } from "./StatusPill.jsx";
74
78
  import { SegmentedToggle, ToggleField } from "./ToggleField.jsx";
75
79
  import { SourceTestPanel } from "./SourceTestPanel.jsx";
76
- import { ApiRegistryActionCard } from "./ApiRegistryActionCard.jsx";
77
80
  import { SandboxToolDraftPanel } from "./SandboxToolDraftPanel.jsx";
78
81
  import { SandboxToolConfirmModal } from "./SandboxToolConfirmModal.jsx";
79
- import { SandboxOrchestrationEditorPanel } from "./SandboxOrchestrationEditorPanel.jsx";
80
82
  import { OrchestrationRunTracePanel } from "./OrchestrationRunTracePanel.jsx";
81
83
  import {
82
84
  buildSandboxRowFromApiRegistry,
@@ -170,7 +172,7 @@ function mergeColumnOrder(order, columns) {
170
172
  }
171
173
 
172
174
  function isLockedObject(table) {
173
- return Boolean(table?.objectType && table.objectType !== "custom");
175
+ return Boolean(table?.locked);
174
176
  }
175
177
 
176
178
  function compareCellValues(left, right) {
@@ -220,14 +222,16 @@ function applyRowsView(rows, settings) {
220
222
  });
221
223
  }
222
224
 
223
- function ObjectViewPicker({ tables, selectedTable, onSelectSource }) {
225
+ function ObjectViewPicker({ tables, selectedTable, onSelectSource, onToggleLock, onDeleteObject, onExportObject, onDuplicateObject }) {
224
226
  const pickerRef = useRef(null);
225
227
  const [open, setOpen] = useState(false);
228
+ const [menuSource, setMenuSource] = useState("");
226
229
 
227
230
  useEffect(() => {
228
231
  function handlePointer(event) {
229
232
  if (!pickerRef.current?.contains(event.target)) {
230
233
  setOpen(false);
234
+ setMenuSource("");
231
235
  }
232
236
  }
233
237
  document.addEventListener("pointerdown", handlePointer);
@@ -241,6 +245,7 @@ function ObjectViewPicker({ tables, selectedTable, onSelectSource }) {
241
245
  onBlur={(event) => {
242
246
  if (!event.currentTarget.contains(event.relatedTarget)) {
243
247
  setOpen(false);
248
+ setMenuSource("");
244
249
  }
245
250
  }}
246
251
  >
@@ -262,11 +267,43 @@ function ObjectViewPicker({ tables, selectedTable, onSelectSource }) {
262
267
  <button type="button" className="dm-picker-row" onClick={() => {
263
268
  onSelectSource(table.source);
264
269
  setOpen(false);
270
+ setMenuSource("");
265
271
  }}>
266
272
  <LucideIcon name={table.icon || OBJECT_TYPE_PRESETS[table.objectType]?.icon || "Database"} size={14} />
267
273
  <span>{table.label}</span>
268
274
  {isLockedObject(table) && <Lock size={12} className="dm-picker-lock" />}
269
275
  </button>
276
+ <div className="dm-picker-actions">
277
+ <button
278
+ type="button"
279
+ className="dm-picker-icon-btn"
280
+ aria-label={`${table.label} actions`}
281
+ title="Object actions"
282
+ onClick={(event) => {
283
+ event.stopPropagation();
284
+ setMenuSource((current) => current === table.source ? "" : table.source);
285
+ }}
286
+ >
287
+ <MoreVertical size={14} aria-hidden="true" />
288
+ </button>
289
+ {menuSource === table.source && (
290
+ <div className="dm-picker-menu" onClick={(event) => event.stopPropagation()}>
291
+ <button type="button" onClick={() => { onToggleLock?.(table); setMenuSource(""); }}>
292
+ {isLockedObject(table) ? <Unlock size={13} /> : <Lock size={13} />}
293
+ {isLockedObject(table) ? "Unlock" : "Lock"}
294
+ </button>
295
+ <button type="button" onClick={() => { onExportObject?.(table); setMenuSource(""); }}>
296
+ <Download size={13} />Export
297
+ </button>
298
+ <button type="button" onClick={() => { onDuplicateObject?.(table); setMenuSource(""); }}>
299
+ <Copy size={13} />Duplicate
300
+ </button>
301
+ <button type="button" className="danger" onClick={() => { onDeleteObject?.(table); setMenuSource(""); }}>
302
+ <Trash2 size={13} />Delete
303
+ </button>
304
+ </div>
305
+ )}
306
+ </div>
270
307
  </div>
271
308
  ))}
272
309
  </div>
@@ -885,14 +922,14 @@ function SandboxRecordFields({
885
922
 
886
923
  <DrawerSection title="Orchestration">
887
924
  <div className="dm-record-field">
888
- <span>{draft.orchestrationConfig !== undefined ? "orchestrationConfig" : "orchestrationGraph"}</span>
925
+ <span>orchestrationConfig</span>
889
926
  <button
890
927
  type="button"
891
928
  className="dm-btn-outline"
892
929
  disabled={saving}
893
930
  onClick={() => onOpenGraphSidecar?.()}
894
931
  >
895
- {getOrchestrationGraphUiState(draft.orchestrationGraph ?? draft.orchestrationConfig) === "populated" ? "Edit orchestration graph" : "Start orchestration graph"}
932
+ {getOrchestrationGraphUiState(draft.orchestrationConfig ?? draft.orchestrationGraph) === "populated" ? "Open workflow" : "Create workflow"}
896
933
  </button>
897
934
  </div>
898
935
  </DrawerSection>
@@ -1013,6 +1050,7 @@ function DataModelRecordDrawer({
1013
1050
  const [traceField, setTraceField] = useState(null);
1014
1051
  const [traceRunId, setTraceRunId] = useState("");
1015
1052
  const drawerKeyRef = useRef("");
1053
+ const router = useRouter();
1016
1054
 
1017
1055
  useEffect(() => {
1018
1056
  const drawerKey = `${table.id || table.objectId || table.source}:${rowIndex}:${row?.Name || row?.id || ""}`;
@@ -1033,11 +1071,7 @@ function DataModelRecordDrawer({
1033
1071
  setCreatedSandboxMeta(null);
1034
1072
  setCreatedSandboxTestMessage("");
1035
1073
  }
1036
- if (initialSidecar?.mode === "graph") {
1037
- setSidecarMode("graph");
1038
- setTraceField(null);
1039
- setTraceRunId("");
1040
- } else if (initialSidecar?.mode === "trace") {
1074
+ if (initialSidecar?.mode === "trace") {
1041
1075
  setSidecarMode("trace");
1042
1076
  setTraceField(initialSidecar.field || "lastResponse");
1043
1077
  setTraceRunId(String(initialSidecar.runId || row?.lastRunId || "").trim());
@@ -1145,7 +1179,7 @@ function DataModelRecordDrawer({
1145
1179
  function ensureSandboxColumns(config, sandboxTable) {
1146
1180
  let next = config;
1147
1181
  let current = sandboxTable;
1148
- for (const field of ["orchestrationGraph", "description"]) {
1182
+ for (const field of ["orchestrationConfig", "description"]) {
1149
1183
  if (!current.columns.includes(field)) {
1150
1184
  next = addTableField(next, current, field);
1151
1185
  const tables = listWorkspaceDataModelTables(next);
@@ -1364,9 +1398,14 @@ function DataModelRecordDrawer({
1364
1398
  onClearInitialSidecar?.();
1365
1399
  }
1366
1400
 
1367
- function openGraphSidecar() {
1368
- setSidecarMode("graph");
1401
+ function openWorkflowView() {
1402
+ const rowId = String(draft?.Name || draft?.name || draft?.slug || draft?.id || "").trim();
1403
+ const field = draft?.orchestrationConfig !== undefined ? "orchestrationConfig" : "orchestrationGraph";
1404
+ if (!table.objectId || !rowId) return;
1369
1405
  onClearInitialSidecar?.();
1406
+ router.push(
1407
+ `/workflows?object=${encodeURIComponent(table.objectId)}&row=${encodeURIComponent(rowId)}&field=${encodeURIComponent(field)}`
1408
+ );
1370
1409
  }
1371
1410
 
1372
1411
  function openTraceSidecar({ field, runId } = {}) {
@@ -1376,15 +1415,8 @@ function DataModelRecordDrawer({
1376
1415
  onClearInitialSidecar?.();
1377
1416
  }
1378
1417
 
1379
- function saveOrchestrationGraph(serialized) {
1380
- if (rowIndex === null || rowIndex === undefined) return;
1381
- const graphField = draft.orchestrationConfig !== undefined ? "orchestrationConfig" : "orchestrationGraph";
1382
- onSave((config) => updateTableCell(config, table, rowIndex, graphField, serialized));
1383
- setDraft((current) => ({ ...current, [graphField]: serialized }));
1384
- }
1385
-
1386
- const drawerWide = sandboxToolFlow === "draft" || sidecarMode === "graph" || sidecarMode === "trace";
1387
- const hideRecordFields = isSandbox && (sidecarMode === "graph" || sidecarMode === "trace");
1418
+ const drawerWide = sandboxToolFlow === "draft" || sidecarMode === "trace";
1419
+ const hideRecordFields = isSandbox && sidecarMode === "trace";
1388
1420
 
1389
1421
  return (
1390
1422
  <>
@@ -1420,7 +1452,7 @@ function DataModelRecordDrawer({
1420
1452
  </button>
1421
1453
  </div>
1422
1454
  </header>
1423
- {(table.objectType === "api-registry" || table.objectType === "data-source") && sandboxToolFlow !== "draft" && (
1455
+ {table.objectType === "data-source" && sandboxToolFlow !== "draft" && (
1424
1456
  <SourceTestPanel
1425
1457
  status={draft.status}
1426
1458
  testing={testing}
@@ -1429,19 +1461,6 @@ function DataModelRecordDrawer({
1429
1461
  onTest={testApiRecord}
1430
1462
  />
1431
1463
  )}
1432
- {isApiRegistry && sandboxToolFlow !== "draft" && sandboxToolFlow !== "created" && (
1433
- <ApiRegistryActionCard
1434
- registryRow={draft}
1435
- workspaceConfig={workspaceConfig}
1436
- disabled={saving || sandboxToolCreating}
1437
- testing={testing}
1438
- sandboxRunning={createdSandboxTesting}
1439
- onTestConnection={testApiRecord}
1440
- onCreateSandboxTool={() => setSandboxToolFlow("draft")}
1441
- onOpenSandboxTool={openSandboxToolRow}
1442
- onRunSandboxTool={runExistingSandboxTool}
1443
- />
1444
- )}
1445
1464
  {isApiRegistry && sandboxToolFlow === "created" && createdSandboxMeta && (
1446
1465
  <section className="dm-api-action-card dm-api-action-card-success" aria-label="Sandbox tool created">
1447
1466
  <div className="dm-api-action-card-body">
@@ -1514,15 +1533,6 @@ function DataModelRecordDrawer({
1514
1533
  onPatchDraft={(patch) => setDraft((current) => ({ ...current, ...patch }))}
1515
1534
  />
1516
1535
  )}
1517
- {isSandbox && sidecarMode === "graph" && (
1518
- <SandboxOrchestrationEditorPanel
1519
- sandboxRow={draft}
1520
- workspaceConfig={workspaceConfig}
1521
- disabled={saving}
1522
- onSaveGraph={saveOrchestrationGraph}
1523
- onBack={closeSidecar}
1524
- />
1525
- )}
1526
1536
  {isSandbox && sidecarMode === "trace" && (
1527
1537
  <OrchestrationRunTracePanel
1528
1538
  row={draft}
@@ -1530,7 +1540,7 @@ function DataModelRecordDrawer({
1530
1540
  fieldName={traceField || "lastResponse"}
1531
1541
  selectedRunId={traceRunId}
1532
1542
  onBack={closeSidecar}
1533
- onOpenGraph={openGraphSidecar}
1543
+ onOpenGraph={openWorkflowView}
1534
1544
  />
1535
1545
  )}
1536
1546
  <div className="dm-record-fields">
@@ -1548,7 +1558,7 @@ function DataModelRecordDrawer({
1548
1558
  sandboxHistoryMessage={sandboxHistoryMessage}
1549
1559
  loadingSandboxHistory={loadingSandboxHistory}
1550
1560
  onLoadSandboxHistory={loadSandboxHistory}
1551
- onOpenGraphSidecar={openGraphSidecar}
1561
+ onOpenGraphSidecar={openWorkflowView}
1552
1562
  onOpenTraceSidecar={openTraceSidecar}
1553
1563
  />
1554
1564
  ) : groupRecordColumns(table.columns || []).map((section) => (
@@ -1629,7 +1639,7 @@ function DataModelRecordDrawer({
1629
1639
  const SANDBOX_SIDECAR_COLUMNS = new Set(["orchestrationGraph", "orchestrationConfig", "lastResponse", "lastRunId", "lastSourceId"]);
1630
1640
 
1631
1641
  function sandboxSidecarForColumn(column, row) {
1632
- if (column === "orchestrationGraph" || column === "orchestrationConfig") return { mode: "graph" };
1642
+ if (column === "orchestrationGraph" || column === "orchestrationConfig") return null;
1633
1643
  if (column === "lastResponse") return { mode: "trace", field: "lastResponse" };
1634
1644
  if (column === "lastRunId") return { mode: "trace", field: "lastRunId", runId: row?.lastRunId };
1635
1645
  if (column === "lastSourceId") return { mode: "trace", field: "lastSourceId" };
@@ -2178,7 +2188,7 @@ function DataModelTableSurface({
2178
2188
  }}
2179
2189
  >
2180
2190
  {column === "orchestrationGraph" || column === "orchestrationConfig"
2181
- ? (getOrchestrationGraphUiState(row?.[column]) === "populated" ? "Edit graph" : "Start graph")
2191
+ ? (getOrchestrationGraphUiState(row?.[column]) === "populated" ? "Open workflow" : "Create workflow")
2182
2192
  : (formatCellValue(row?.[column], column) || "View trace")}
2183
2193
  </button>
2184
2194
  ) : (
@@ -2784,6 +2794,102 @@ export default function DataModelShell() {
2784
2794
  setHelperOpen(true);
2785
2795
  };
2786
2796
 
2797
+ const mutateObjectById = useCallback((table, updater) => {
2798
+ const targetId = String(table?.objectId || "").trim();
2799
+ if (!targetId) return;
2800
+ save((config) => {
2801
+ const dataModel = config.dataModel && typeof config.dataModel === "object" && !Array.isArray(config.dataModel)
2802
+ ? config.dataModel
2803
+ : {};
2804
+ const objects = Array.isArray(dataModel.objects) ? dataModel.objects : [];
2805
+ return {
2806
+ ...config,
2807
+ dataModel: {
2808
+ ...dataModel,
2809
+ objects: objects.map((object) => String(object?.id || "") === targetId ? updater(object) : object)
2810
+ }
2811
+ };
2812
+ });
2813
+ }, [save]);
2814
+
2815
+ const toggleObjectLock = useCallback((table) => {
2816
+ mutateObjectById(table, (object) => ({ ...object, locked: !isLockedObject(table) }));
2817
+ }, [mutateObjectById]);
2818
+
2819
+ const exportObject = useCallback((table) => {
2820
+ const blob = new Blob([exportTableAsCsv(table)], { type: "text/csv" });
2821
+ const url = URL.createObjectURL(blob);
2822
+ const a = document.createElement("a");
2823
+ a.href = url;
2824
+ a.download = `${String(table?.source || table?.label || "data-model-object").replace(/\s+/g, "-").toLowerCase()}.csv`;
2825
+ a.click();
2826
+ URL.revokeObjectURL(url);
2827
+ }, []);
2828
+
2829
+ const duplicateObject = useCallback((table) => {
2830
+ const targetId = String(table?.objectId || "").trim();
2831
+ if (!targetId) return;
2832
+ let nextSource = "";
2833
+ save((config) => {
2834
+ const dataModel = config.dataModel && typeof config.dataModel === "object" && !Array.isArray(config.dataModel)
2835
+ ? config.dataModel
2836
+ : {};
2837
+ const objects = Array.isArray(dataModel.objects) ? dataModel.objects : [];
2838
+ const sourceObject = objects.find((object) => String(object?.id || "") === targetId);
2839
+ if (!sourceObject) return config;
2840
+ const baseLabel = `${sourceObject.label || sourceObject.name || sourceObject.source || table.label || "Object"} Copy`;
2841
+ const existingIds = new Set(objects.map((object) => String(object?.id || "")));
2842
+ const slugBase = baseLabel.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "object-copy";
2843
+ let id = slugBase;
2844
+ let index = 2;
2845
+ while (existingIds.has(id)) {
2846
+ id = `${slugBase}-${index}`;
2847
+ index += 1;
2848
+ }
2849
+ nextSource = baseLabel;
2850
+ const clone = {
2851
+ ...JSON.parse(JSON.stringify(sourceObject)),
2852
+ id,
2853
+ label: baseLabel,
2854
+ source: baseLabel,
2855
+ locked: false
2856
+ };
2857
+ return {
2858
+ ...config,
2859
+ dataModel: {
2860
+ ...dataModel,
2861
+ objects: [...objects, clone]
2862
+ }
2863
+ };
2864
+ });
2865
+ if (nextSource) setSelectedSource(nextSource);
2866
+ }, [save]);
2867
+
2868
+ const deleteObject = useCallback((table) => {
2869
+ const targetId = String(table?.objectId || "").trim();
2870
+ if (!targetId) return;
2871
+ const confirmed = window.confirm(`Delete "${table.label || table.source}" from the Data Model?`);
2872
+ if (!confirmed) return;
2873
+ let fallbackSource = "";
2874
+ save((config) => {
2875
+ const dataModel = config.dataModel && typeof config.dataModel === "object" && !Array.isArray(config.dataModel)
2876
+ ? config.dataModel
2877
+ : {};
2878
+ const objects = Array.isArray(dataModel.objects) ? dataModel.objects : [];
2879
+ const nextObjects = objects.filter((object) => String(object?.id || "") !== targetId);
2880
+ const fallback = nextObjects[0];
2881
+ fallbackSource = String(fallback?.source || fallback?.label || fallback?.name || "");
2882
+ return {
2883
+ ...config,
2884
+ dataModel: {
2885
+ ...dataModel,
2886
+ objects: nextObjects
2887
+ }
2888
+ };
2889
+ });
2890
+ if (selectedSource === table.source) setSelectedSource(fallbackSource);
2891
+ }, [save, selectedSource]);
2892
+
2787
2893
  // Reopen a helper thread row from the Helper Threads Data Model object.
2788
2894
  // The row already holds the full prior turn (intent, prompt, proposals,
2789
2895
  // warnings, receipts) — passing it through initialThread rehydrates the
@@ -2901,6 +3007,10 @@ export default function DataModelShell() {
2901
3007
  saving={saving}
2902
3008
  onSelectSource={setSelectedSource}
2903
3009
  onSave={save}
3010
+ onToggleLock={toggleObjectLock}
3011
+ onDeleteObject={deleteObject}
3012
+ onExportObject={exportObject}
3013
+ onDuplicateObject={duplicateObject}
2904
3014
  />
2905
3015
  )}
2906
3016
  <button type="button" className="dm-btn-primary" onClick={() => setAddOpen(true)}>
@@ -2982,6 +3092,7 @@ export default function DataModelShell() {
2982
3092
  <section className="dm-detail-v2 dm-detail-v3">
2983
3093
  <SourceValidationBanner table={selectedTable} />
2984
3094
  <DataModelTableSurface
3095
+ key={selectedTableKey}
2985
3096
  workspaceConfig={workspaceConfig}
2986
3097
  table={selectedTable}
2987
3098
  tables={tables}