@growthub/cli 0.13.9 → 0.14.1

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 (39) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +31 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +227 -5
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +70 -9
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +6 -3
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +61 -35
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +414 -9
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +339 -77
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +70 -85
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +229 -9
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +224 -14
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +139 -4
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +246 -4
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +69 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +411 -1
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +30 -1
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +551 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
  39. package/package.json +1 -1
@@ -42,6 +42,7 @@ import {
42
42
  Trash2,
43
43
  Type,
44
44
  Unlock,
45
+ Upload,
45
46
  Users,
46
47
  X,
47
48
  Zap,
@@ -80,9 +81,16 @@ import { SourceTestPanel } from "./SourceTestPanel.jsx";
80
81
  import { SandboxToolDraftPanel } from "./SandboxToolDraftPanel.jsx";
81
82
  import { SandboxToolConfirmModal } from "./SandboxToolConfirmModal.jsx";
82
83
  import { OrchestrationRunTracePanel } from "./OrchestrationRunTracePanel.jsx";
84
+ import { ApiRegistryCreationCockpit } from "./ApiRegistryCreationCockpit.jsx";
85
+ import { deriveApiRegistryCreationState } from "@/lib/api-registry-creation-flow";
86
+ import { deriveSandboxServerlessState } from "@/lib/sandbox-serverless-flow";
87
+ import { profileApiResponse, recommendResolver } from "@/lib/api-response-profile";
88
+ import { classifyCreationError } from "@/lib/creation-error-recovery";
83
89
  import {
84
90
  buildSandboxRowFromApiRegistry,
85
91
  findSandboxRowsForRegistry,
92
+ buildDataSourceRowFromApiRegistry,
93
+ findDataSourceRowsForRegistry,
86
94
  getOrchestrationGraphUiState,
87
95
  redactSecretsFromText
88
96
  } from "@/lib/orchestration-graph";
@@ -517,8 +525,11 @@ function StaticSelect({ value, options, disabled, onChange, placeholder = "Selec
517
525
  );
518
526
  }
519
527
 
520
- function DrawerSection({ title, children, defaultOpen = false }) {
528
+ function DrawerSection({ title, children, defaultOpen = false, forceOpen = false }) {
521
529
  const [open, setOpen] = useState(defaultOpen);
530
+ useEffect(() => {
531
+ if (forceOpen) setOpen(true);
532
+ }, [forceOpen]);
522
533
  return (
523
534
  <section className={`dm-drawer-section${open ? " open" : ""}`}>
524
535
  <button type="button" className="dm-drawer-section-toggle" onClick={() => setOpen((current) => !current)}>
@@ -652,13 +663,30 @@ function SandboxRecordFields({
652
663
  onOpenGraphSidecar,
653
664
  onOpenTraceSidecar
654
665
  }) {
666
+ const router = useRouter();
655
667
  const [sandboxAdapters, setSandboxAdapters] = useState([]);
668
+ const [serverlessSignals, setServerlessSignals] = useState({ configuredEnvRefs: [], persistenceAdapters: [] });
656
669
  useEffect(() => {
657
670
  fetch("/api/workspace/sandbox-adapters", { cache: "no-store" })
658
671
  .then((res) => res.json())
659
672
  .then((payload) => setSandboxAdapters(Array.isArray(payload.adapters) ? payload.adapters : []))
660
673
  .catch(() => setSandboxAdapters([]));
661
674
  }, []);
675
+ // Real runtime truth for the serverless/persistence cockpit (env-status).
676
+ useEffect(() => {
677
+ let cancelled = false;
678
+ fetch("/api/workspace/env-status", { cache: "no-store" })
679
+ .then((res) => (res.ok ? res.json() : {}))
680
+ .then((payload) => {
681
+ if (cancelled) return;
682
+ setServerlessSignals({
683
+ configuredEnvRefs: Array.isArray(payload.configuredEnvRefs) ? payload.configuredEnvRefs : [],
684
+ persistenceAdapters: Array.isArray(payload.persistenceAdapters) ? payload.persistenceAdapters : [],
685
+ });
686
+ })
687
+ .catch(() => {});
688
+ return () => { cancelled = true; };
689
+ }, [rowIndex, table.objectId]);
662
690
 
663
691
  const locality = String(draft.runLocality || "local").trim().toLowerCase() === "serverless" ? "serverless" : "local";
664
692
  const savedEnvRefs = useMemo(() => listSavedEnvRefs(workspaceConfig || {}), [workspaceConfig]);
@@ -697,8 +725,30 @@ function SandboxRecordFields({
697
725
 
698
726
  const netOn = ["true", "1", "on", "yes"].includes(String(draft.networkAllow || "").trim().toLowerCase());
699
727
 
728
+ // Same cockpit interface + mental model as the API Registry lane, driven by
729
+ // the serverless/scheduling/persistence derivation. Steps are status-only
730
+ // here (inlineEditing) — the editable fields below are the editor.
731
+ const serverlessState = deriveSandboxServerlessState({
732
+ sandboxRow: draft,
733
+ workspaceConfig,
734
+ configuredEnvRefs: serverlessSignals.configuredEnvRefs,
735
+ persistenceAdapters: serverlessSignals.persistenceAdapters,
736
+ inlineEditing: true,
737
+ });
738
+ function handleServerlessAction(action) {
739
+ if (!action) return;
740
+ if (action.id === "toggle-locality") setRunLocality(serverlessState.isServerless ? "local" : "serverless");
741
+ else if (action.id === "open-settings") router.push(action.href || "/settings");
742
+ }
743
+
700
744
  return (
701
745
  <div className="dm-sandbox-config">
746
+ <ApiRegistryCreationCockpit
747
+ state={serverlessState}
748
+ onAction={handleServerlessAction}
749
+ disabled={!table.mutable || saving}
750
+ eyebrow={serverlessState.isServerless ? "Serverless workflow" : "Workflow runtime"}
751
+ />
702
752
  <DrawerSection title="Identity & Mode">
703
753
  <label className="dm-record-field">
704
754
  <span>Name</span>
@@ -1141,9 +1191,20 @@ function DataModelRecordDrawer({
1141
1191
  const [createdSandboxMeta, setCreatedSandboxMeta] = useState(null);
1142
1192
  const [createdSandboxTesting, setCreatedSandboxTesting] = useState(false);
1143
1193
  const [createdSandboxTestMessage, setCreatedSandboxTestMessage] = useState("");
1194
+ const [creatingDataSource, setCreatingDataSource] = useState(false);
1195
+ const [createdDataSourceMeta, setCreatedDataSourceMeta] = useState(null);
1196
+ const [dataSourceMessage, setDataSourceMessage] = useState("");
1197
+ const [cockpitBusy, setCockpitBusy] = useState("");
1198
+ const [cockpitCollapsed, setCockpitCollapsed] = useState(false);
1199
+ // Real runtime truth for the creation cockpit: which auth refs resolve in the
1200
+ // server runtime, and the live source-records sidecar. Fetched (never guessed)
1201
+ // so auth/refresh readiness reflect actual state, and refreshed after actions.
1202
+ const [creationSignals, setCreationSignals] = useState({ configuredEnvRefs: [], sourceRecords: {} });
1203
+ const [creationReceipts, setCreationReceipts] = useState([]);
1144
1204
  const [sidecarMode, setSidecarMode] = useState(null);
1145
1205
  const [traceField, setTraceField] = useState(null);
1146
1206
  const [traceRunId, setTraceRunId] = useState("");
1207
+ const drawerScrollRef = useRef(null);
1147
1208
  const drawerKeyRef = useRef("");
1148
1209
  const router = useRouter();
1149
1210
 
@@ -1165,6 +1226,14 @@ function DataModelRecordDrawer({
1165
1226
  setSandboxToolDraft({});
1166
1227
  setCreatedSandboxMeta(null);
1167
1228
  setCreatedSandboxTestMessage("");
1229
+ setCreatingDataSource(false);
1230
+ setCreatedDataSourceMeta(null);
1231
+ setDataSourceMessage("");
1232
+ setCreationReceipts([]);
1233
+ setCockpitCollapsed(false);
1234
+ requestAnimationFrame(() => {
1235
+ if (drawerScrollRef.current) drawerScrollRef.current.scrollTop = 0;
1236
+ });
1168
1237
  }
1169
1238
  if (initialSidecar?.mode === "trace") {
1170
1239
  setSidecarMode("trace");
@@ -1177,6 +1246,34 @@ function DataModelRecordDrawer({
1177
1246
  }
1178
1247
  }, [row, rowIndex, initialSidecar, table.id, table.objectId, table.source, table.columns, table.fieldSettings?.hidden]);
1179
1248
 
1249
+ // Load real cockpit truth (configured auth refs + source-records sidecar) when
1250
+ // an API Registry row is open. The creation cockpit derives auth/refresh
1251
+ // readiness from these — never guessed, never faked.
1252
+ useEffect(() => {
1253
+ if (table.objectType !== "api-registry" || rowIndex === null || rowIndex === undefined) return;
1254
+ let cancelled = false;
1255
+ (async () => {
1256
+ try {
1257
+ const [statusRes, wsRes] = await Promise.all([
1258
+ fetch("/api/workspace/env-status", { cache: "no-store" }),
1259
+ fetch("/api/workspace", { cache: "no-store" }),
1260
+ ]);
1261
+ const status = statusRes.ok ? await statusRes.json() : {};
1262
+ const ws = wsRes.ok ? await wsRes.json() : {};
1263
+ if (cancelled) return;
1264
+ setCreationSignals({
1265
+ configuredEnvRefs: Array.isArray(status.configuredEnvRefs) ? status.configuredEnvRefs : [],
1266
+ sourceRecords: ws.workspaceSourceRecords && typeof ws.workspaceSourceRecords === "object"
1267
+ ? ws.workspaceSourceRecords
1268
+ : {},
1269
+ });
1270
+ } catch {
1271
+ /* leave signals as-is — cockpit degrades to pending, never fakes */
1272
+ }
1273
+ })();
1274
+ return () => { cancelled = true; };
1275
+ }, [table.objectType, table.objectId, rowIndex]);
1276
+
1180
1277
  if (rowIndex === null || rowIndex === undefined || !row) return null;
1181
1278
 
1182
1279
  const isApiRegistry = table.objectType === "api-registry";
@@ -1258,6 +1355,14 @@ function DataModelRecordDrawer({
1258
1355
  });
1259
1356
  setDraft((current) => ({ ...current, status, lastTested: new Date().toISOString(), lastResponse: responseText }));
1260
1357
  setTestMessage(payload.ok ? "Connected" : payload.error || "Connection failed");
1358
+ if (isApiRegistry) {
1359
+ if (payload.ok) {
1360
+ pushReceipt({ kind: "api-test", ok: true, detail: `Tested — HTTP ${payload.status ?? 200}${payload.usedServerSecret ? " · used server secret" : ""}.` });
1361
+ } else {
1362
+ const recovery = classifyCreationError({ phase: "test", httpStatus: payload.status, detail: redactSecretsFromText(payload.error || `HTTP ${payload.status ?? ""} failed`) });
1363
+ pushReceipt({ kind: "api-test", ok: false, detail: `${recovery.safeDetail}. ${recovery.requiredAction}` });
1364
+ }
1365
+ }
1261
1366
  } catch (err) {
1262
1367
  const responseText = JSON.stringify({ error: err.message || "Connection failed" }, null, 2);
1263
1368
  onSave((config) => {
@@ -1341,6 +1446,221 @@ function DataModelRecordDrawer({
1341
1446
  }
1342
1447
  }
1343
1448
 
1449
+ function createDataSourceFromRegistry() {
1450
+ const integrationId = String(draft?.integrationId || "").trim();
1451
+ if (!integrationId) {
1452
+ setDataSourceMessage("This API Registry row needs an integrationId before a Data Source can reference it.");
1453
+ return;
1454
+ }
1455
+ if (findDataSourceRowsForRegistry(workspaceConfig, integrationId).length > 0) {
1456
+ setDataSourceMessage("A Data Source already references this API. Open it instead of creating a duplicate.");
1457
+ return;
1458
+ }
1459
+ setCreatingDataSource(true);
1460
+ setDataSourceMessage("");
1461
+ try {
1462
+ let createdMeta = null;
1463
+ onSave((config) => {
1464
+ let next = config;
1465
+ if (findDataSourceRowsForRegistry(next, integrationId).length > 0) return next;
1466
+ // Dedicated data-source object per API (one object = one live integration,
1467
+ // matching the refresh-sources contract which binds integrationId per object).
1468
+ const dsName = `${String(draft?.Name || integrationId).trim()} Source`;
1469
+ const beforeIds = new Set((next.dataModel?.objects || []).map((o) => o.id));
1470
+ next = createTypedBusinessObject(next, { name: dsName, objectType: "data-source" });
1471
+ const newObj = (next.dataModel?.objects || []).find((o) => o.objectType === "data-source" && !beforeIds.has(o.id));
1472
+ if (!newObj) return next;
1473
+ const sourceTable = listWorkspaceDataModelTables(next).find((t) => t.objectId === newObj.id);
1474
+ const profile = profileApiResponse(draft?.lastResponse);
1475
+ const newRow = buildDataSourceRowFromApiRegistry(next, draft, {
1476
+ entityType: profile?.parsed ? profile.suggestedEntityType : undefined,
1477
+ });
1478
+ if (sourceTable) next = appendRowsToTable(next, sourceTable, [newRow]);
1479
+ // Make the object live-backed so refresh-sources hydrates the sidecar
1480
+ // (keyed by object id). Without this binding, refresh skips it as
1481
+ // "not-live-backed" and the journey would never close.
1482
+ next = {
1483
+ ...next,
1484
+ dataModel: {
1485
+ ...next.dataModel,
1486
+ objects: (next.dataModel.objects || []).map((o) =>
1487
+ o.id === newObj.id
1488
+ ? {
1489
+ ...o,
1490
+ sourceId: newRow.sourceId,
1491
+ binding: {
1492
+ mode: "integration",
1493
+ lane: "data-source",
1494
+ sourceStorage: "workspace-source-records",
1495
+ integrationId,
1496
+ sourceId: newRow.sourceId,
1497
+ source: newRow.Name,
1498
+ },
1499
+ }
1500
+ : o
1501
+ ),
1502
+ },
1503
+ };
1504
+ createdMeta = { objectId: newObj.id, name: newRow.Name, sourceId: newRow.sourceId };
1505
+ return next;
1506
+ });
1507
+ if (createdMeta) {
1508
+ setCreatedDataSourceMeta(createdMeta);
1509
+ setDataSourceMessage("Data Source created and live-backed. Use Refresh to pull records into the workspace — nothing auto-fetches.");
1510
+ pushReceipt({ kind: "data-source-created", ok: true, detail: `Created "${createdMeta.name}" (sourceId ${createdMeta.sourceId}), live-backed via registryId ${integrationId}.` });
1511
+ reloadCreationSignals();
1512
+ } else {
1513
+ setDataSourceMessage("A Data Source already references this API.");
1514
+ }
1515
+ } finally {
1516
+ setCreatingDataSource(false);
1517
+ }
1518
+ }
1519
+
1520
+ function openDataSourceRow(objectIdOverride) {
1521
+ const objectId = String(objectIdOverride || createdDataSourceMeta?.objectId || "").trim();
1522
+ if (!objectId) return;
1523
+ onClose();
1524
+ router.push(`/data-model?object=${encodeURIComponent(objectId)}`);
1525
+ }
1526
+
1527
+ // Append a creation receipt (test / create / refresh outcomes). Secret-safe —
1528
+ // detail strings are caller-redacted; receipts hold no values.
1529
+ function pushReceipt(entry) {
1530
+ setCreationReceipts((cur) => [{ at: new Date().toISOString(), ...entry }, ...cur].slice(0, 12));
1531
+ }
1532
+
1533
+ // Pull real cockpit truth: configured auth refs (server-resolved, slugs only)
1534
+ // + the live source-records sidecar. Safe to call repeatedly; never throws.
1535
+ async function reloadCreationSignals() {
1536
+ try {
1537
+ const [statusRes, wsRes] = await Promise.all([
1538
+ fetch("/api/workspace/env-status", { cache: "no-store" }),
1539
+ fetch("/api/workspace", { cache: "no-store" }),
1540
+ ]);
1541
+ const status = statusRes.ok ? await statusRes.json() : {};
1542
+ const ws = wsRes.ok ? await wsRes.json() : {};
1543
+ setCreationSignals({
1544
+ configuredEnvRefs: Array.isArray(status.configuredEnvRefs) ? status.configuredEnvRefs : [],
1545
+ sourceRecords: ws.workspaceSourceRecords && typeof ws.workspaceSourceRecords === "object"
1546
+ ? ws.workspaceSourceRecords
1547
+ : {},
1548
+ });
1549
+ } catch {
1550
+ /* signals stay at their last value — the cockpit degrades to pending, never fakes */
1551
+ }
1552
+ }
1553
+
1554
+ // Refresh the linked Data Source through the sidecar dispatcher
1555
+ // (refresh-sources, plural) keyed by the data-source OBJECT id, so the records
1556
+ // land in the source-records sidecar the cockpit reads — then reload signals
1557
+ // so the refresh step flips to complete from real state.
1558
+ async function refreshLinkedSource({ objectId }) {
1559
+ const sourceObjectId = String(objectId || "").trim();
1560
+ if (!sourceObjectId) {
1561
+ setDataSourceMessage("No linked Data Source object to refresh yet.");
1562
+ return;
1563
+ }
1564
+ setDataSourceMessage("");
1565
+ try {
1566
+ const res = await fetch("/api/workspace/refresh-sources", {
1567
+ method: "POST",
1568
+ headers: { "content-type": "application/json" },
1569
+ body: JSON.stringify({ sourceIds: [sourceObjectId] }),
1570
+ });
1571
+ const payload = await res.json();
1572
+ const result = Array.isArray(payload.refreshed) ? payload.refreshed.find((r) => r.sourceId === sourceObjectId) : null;
1573
+ if (res.ok && result) {
1574
+ const msg = `Refreshed — ${result.recordCount ?? 0} record(s) pulled into the sidecar.`;
1575
+ setDataSourceMessage("");
1576
+ pushReceipt({ kind: "source-refresh", ok: true, detail: msg });
1577
+ } else if (res.ok && Array.isArray(payload.skipped) && payload.skipped.includes(sourceObjectId)) {
1578
+ const detail = (payload.skippedDetail || []).find((d) => d.sourceId === sourceObjectId);
1579
+ const recovery = classifyCreationError({ phase: "refresh", reason: detail?.reason });
1580
+ setDataSourceMessage(`Refresh skipped: ${recovery.safeDetail}. ${recovery.requiredAction}`);
1581
+ pushReceipt({ kind: "source-refresh", ok: false, detail: `Skipped (${recovery.errorKind}). ${recovery.requiredAction}` });
1582
+ } else {
1583
+ const recovery = classifyCreationError({ phase: "refresh", httpStatus: res.status, detail: redactSecretsFromText(payload.error || "Refresh failed") });
1584
+ setDataSourceMessage(`${recovery.safeDetail} — ${recovery.requiredAction}`);
1585
+ pushReceipt({ kind: "source-refresh", ok: false, detail: `${recovery.safeDetail}. ${recovery.requiredAction}` });
1586
+ }
1587
+ await reloadCreationSignals();
1588
+ } catch (err) {
1589
+ setDataSourceMessage(redactSecretsFromText(err.message || "Refresh failed"));
1590
+ }
1591
+ }
1592
+
1593
+ // The creation cockpit emits a single action descriptor per step; map it to
1594
+ // the drawer's existing governed handlers. No new mutation paths.
1595
+ async function handleCockpitAction(action) {
1596
+ if (!action || !action.id) return;
1597
+ const tag = `${action.stepId}:${action.id}`;
1598
+ setCockpitBusy(tag);
1599
+ try {
1600
+ switch (action.id) {
1601
+ case "edit":
1602
+ setEditMode(true);
1603
+ setCockpitCollapsed(true);
1604
+ break;
1605
+ case "open-settings":
1606
+ onClose();
1607
+ router.push(action.href || "/settings");
1608
+ break;
1609
+ case "open-resolver":
1610
+ // Hand off to the governed helper widget — the resolver proposal lane.
1611
+ // Carries the integrationId so the helper can scope a resolver proposal.
1612
+ onClose();
1613
+ router.push(`/data-model?helper=open&resolverFor=${encodeURIComponent(String(draft?.integrationId || "").trim())}`);
1614
+ break;
1615
+ case "test":
1616
+ await testApiRecord();
1617
+ await reloadCreationSignals();
1618
+ break;
1619
+ case "create-data-source":
1620
+ createDataSourceFromRegistry();
1621
+ break;
1622
+ case "open-data-source":
1623
+ openDataSourceRow(action.objectId);
1624
+ break;
1625
+ case "create-sandbox-tool":
1626
+ setSandboxToolFlow("draft");
1627
+ break;
1628
+ case "refresh-source":
1629
+ await refreshLinkedSource({ objectId: action.objectId });
1630
+ break;
1631
+ default:
1632
+ break;
1633
+ }
1634
+ } finally {
1635
+ setCockpitBusy("");
1636
+ }
1637
+ }
1638
+
1639
+ const creationState = isApiRegistry
1640
+ ? deriveApiRegistryCreationState({
1641
+ workspaceConfig,
1642
+ registryRow: draft,
1643
+ sourceRecords: creationSignals.sourceRecords,
1644
+ runtime: { configuredEnvRefs: creationSignals.configuredEnvRefs },
1645
+ })
1646
+ : null;
1647
+ // Shape analysis from the tested response — drives the resolver recommendation
1648
+ // and the field candidates the operator sees before creating a Data Source.
1649
+ const creationProfile = isApiRegistry && creationState?.tested
1650
+ ? profileApiResponse(draft?.lastResponse)
1651
+ : null;
1652
+ const creationResolverRec = creationProfile ? recommendResolver(creationProfile) : null;
1653
+ // Preview the exact Data Source that "Create Data Source" will produce, before
1654
+ // any mutation — shown once tested and while no source is linked yet.
1655
+ const creationDataSourcePreview = isApiRegistry && creationState?.tested && !creationState.sourceExists
1656
+ ? {
1657
+ row: buildDataSourceRowFromApiRegistry(workspaceConfig, draft, {
1658
+ entityType: creationProfile?.suggestedEntityType,
1659
+ }),
1660
+ fields: creationProfile?.fields || [],
1661
+ }
1662
+ : null;
1663
+
1344
1664
  async function runSandboxToolByName({ objectId, name }) {
1345
1665
  const rowName = String(name || "").trim();
1346
1666
  const objectIdValue = String(objectId || "").trim();
@@ -1524,7 +1844,9 @@ function DataModelRecordDrawer({
1524
1844
  <header className="dm-record-drawer-head">
1525
1845
  <div>
1526
1846
  <p>Record</p>
1527
- <h2>{draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}</h2>
1847
+ <h2 title={draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}>
1848
+ {draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}
1849
+ </h2>
1528
1850
  </div>
1529
1851
  <div className="dm-record-drawer-actions">
1530
1852
  {isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && (
@@ -1535,7 +1857,7 @@ function DataModelRecordDrawer({
1535
1857
  onClick={runSandbox}
1536
1858
  >
1537
1859
  <Play size={13} aria-hidden />
1538
- {sandboxRunning ? "Running…" : "Run sandbox"}
1860
+ {sandboxRunning ? "Running…" : "Execute"}
1539
1861
  </button>
1540
1862
  )}
1541
1863
  {!isSandbox && sandboxToolFlow !== "draft" && (
@@ -1557,6 +1879,7 @@ function DataModelRecordDrawer({
1557
1879
  onTest={testApiRecord}
1558
1880
  />
1559
1881
  )}
1882
+ <div className="dm-record-scroll" ref={drawerScrollRef}>
1560
1883
  {isApiRegistry && sandboxToolFlow === "created" && createdSandboxMeta && (
1561
1884
  <section className="dm-api-action-card dm-api-action-card-success" aria-label="Sandbox tool created">
1562
1885
  <div className="dm-api-action-card-body">
@@ -1585,6 +1908,24 @@ function DataModelRecordDrawer({
1585
1908
  </div>
1586
1909
  </section>
1587
1910
  )}
1911
+ {isApiRegistry && sandboxToolFlow !== "draft" && sandboxToolFlow !== "confirm" && creationState && (
1912
+ <>
1913
+ <ApiRegistryCreationCockpit
1914
+ state={creationState}
1915
+ onAction={handleCockpitAction}
1916
+ busyAction={cockpitBusy}
1917
+ disabled={saving || creatingDataSource || testing}
1918
+ profile={creationProfile}
1919
+ resolverRec={creationResolverRec}
1920
+ receipts={creationReceipts}
1921
+ dataSourcePreview={creationDataSourcePreview}
1922
+ defaultCollapsed={cockpitCollapsed}
1923
+ hideWhenComplete
1924
+ onCollapsedChange={setCockpitCollapsed}
1925
+ />
1926
+ {dataSourceMessage ? <p className="dm-sandbox-tool-test-msg">{dataSourceMessage}</p> : null}
1927
+ </>
1928
+ )}
1588
1929
  {isApiRegistry && sandboxToolFlow === "draft" && (
1589
1930
  <SandboxToolDraftPanel
1590
1931
  registryRow={draft}
@@ -1667,7 +2008,11 @@ function DataModelRecordDrawer({
1667
2008
  rowIndex={rowIndex}
1668
2009
  />
1669
2010
  ) : groupRecordColumns(table.columns || []).map((section) => (
1670
- <DrawerSection key={section.title} title={section.title}>
2011
+ <DrawerSection
2012
+ key={section.title}
2013
+ title={section.title}
2014
+ forceOpen={isApiRegistry && editMode}
2015
+ >
1671
2016
  {section.columns.map((column) => (
1672
2017
  <RecordFieldEditor
1673
2018
  key={column}
@@ -1685,7 +2030,7 @@ function DataModelRecordDrawer({
1685
2030
  </DrawerSection>
1686
2031
  ))}
1687
2032
  {!isSandbox && editMode && (
1688
- <DrawerSection title="Fields" defaultOpen>
2033
+ <DrawerSection title="Fields">
1689
2034
  <div className="dm-drawer-field-editor">
1690
2035
  {pendingColumns.map((column, index) => (
1691
2036
  <div key={`${column}-${index}`} className="dm-drawer-field-row">
@@ -1714,6 +2059,7 @@ function DataModelRecordDrawer({
1714
2059
  </DrawerSection>
1715
2060
  )}
1716
2061
  </div>
2062
+ </div>
1717
2063
  {!isSandbox && editMode && (
1718
2064
  <footer className="dm-record-drawer-foot">
1719
2065
  <button type="button" className="dm-btn-outline" onClick={cancelEdits}>Cancel</button>
@@ -1763,7 +2109,9 @@ function DataModelTableSurface({
1763
2109
  onSave,
1764
2110
  onOpenThread,
1765
2111
  focusSandboxRowName,
2112
+ focusRecordValue,
1766
2113
  onFocusSandboxRowConsumed,
2114
+ onFocusRecordConsumed,
1767
2115
  onFocusSandboxRow,
1768
2116
  selectedRecordIndex,
1769
2117
  onSelectedRecordIndexChange,
@@ -1861,6 +2209,24 @@ function DataModelTableSurface({
1861
2209
  onFocusSandboxRowConsumed?.();
1862
2210
  }, [focusSandboxRowName, table.id, table.objectType, table.rows, rowEntries, pageSize, onFocusSandboxRowConsumed]);
1863
2211
 
2212
+ useEffect(() => {
2213
+ if (!focusRecordValue) return;
2214
+ const wanted = String(focusRecordValue).trim();
2215
+ if (!wanted) return;
2216
+ const originalIndex = (table.rows || []).findIndex((r) => (
2217
+ String(r?.integrationId || "").trim() === wanted
2218
+ || String(r?.Name || r?.name || r?.slug || r?.id || "").trim() === wanted
2219
+ ));
2220
+ if (originalIndex < 0) return;
2221
+ const visibleIndex = rowEntries.findIndex((entry) => entry.originalIndex === originalIndex);
2222
+ if (visibleIndex < 0) return;
2223
+ const pageForRow = Math.floor(visibleIndex / pageSize);
2224
+ setPageIndex(pageForRow);
2225
+ setSelectedRow(visibleIndex);
2226
+ selectOriginalIndex(originalIndex);
2227
+ onFocusRecordConsumed?.();
2228
+ }, [focusRecordValue, table.id, table.rows, rowEntries, pageSize, onFocusRecordConsumed]);
2229
+
1864
2230
  useEffect(() => {
1865
2231
  setPageIndex((current) => Math.min(current, pageCount - 1));
1866
2232
  }, [pageCount]);
@@ -2094,9 +2460,9 @@ function DataModelTableSurface({
2094
2460
  const a = document.createElement("a");
2095
2461
  a.href = url; a.download = `${table.source.replace(/\s+/g, "-").toLowerCase()}.csv`;
2096
2462
  a.click(); URL.revokeObjectURL(url);
2097
- }}>Export CSV</button>
2463
+ }}><Download size={13} />Export CSV</button>
2098
2464
  )}
2099
- {table.mutable && <button type="button" className="dm-btn-ghost" onClick={() => setCsvOpen((open) => !open)}>Import CSV</button>}
2465
+ {table.mutable && <button type="button" className="dm-btn-ghost" onClick={() => setCsvOpen((open) => !open)}><Upload size={13} />Import CSV</button>}
2100
2466
  {table.mutable && (
2101
2467
  <button type="button" className="dm-btn-primary-sm" disabled={saving} onClick={() => onSave((config) => addTableRow(config, table))}>
2102
2468
  <Plus size={13} />Add record
@@ -2648,9 +3014,11 @@ export default function DataModelShell() {
2648
3014
  const [helperInitialThread, setHelperInitialThread] = useState(null);
2649
3015
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
2650
3016
  const [focusSandboxRowName, setFocusSandboxRowName] = useState(null);
3017
+ const [focusRecordValue, setFocusRecordValue] = useState(null);
2651
3018
  const [selectedRecordByTable, setSelectedRecordByTable] = useState({});
2652
3019
  const pendingPatchRef = useRef({});
2653
3020
  const saveTimerRef = useRef(null);
3021
+ const consumedHelperRouteRef = useRef("");
2654
3022
 
2655
3023
  // Cross-page rail entrypoints. Settings / integrations pages render
2656
3024
  // <WorkspaceRail> without an in-process helper handler — clicking the
@@ -2663,7 +3031,11 @@ export default function DataModelShell() {
2663
3031
  if (!workspaceConfig) return;
2664
3032
  const helperParam = searchParams?.get("helper");
2665
3033
  const threadParam = searchParams?.get("thread");
2666
- if (!helperParam && !threadParam) return;
3034
+ const resolverForParam = searchParams?.get("resolverFor");
3035
+ if (!helperParam && !threadParam && !resolverForParam) return;
3036
+ const routeKey = `${helperParam || ""}:${threadParam || ""}:${resolverForParam || ""}`;
3037
+ if (routeKey === consumedHelperRouteRef.current) return;
3038
+ consumedHelperRouteRef.current = routeKey;
2667
3039
  if (threadParam) {
2668
3040
  const ht = (workspaceConfig?.dataModel?.objects || []).find((o) => o?.id === "helper-threads");
2669
3041
  const row = (ht?.rows || []).find((r) => r?.id === threadParam);
@@ -2671,6 +3043,12 @@ export default function DataModelShell() {
2671
3043
  setHelperInitialThread(row);
2672
3044
  setHelperOpen(true);
2673
3045
  }
3046
+ } else if (resolverForParam) {
3047
+ setHelperIntent("register_api");
3048
+ setHelperInitialThread(null);
3049
+ setHelperInitialPrompt(`Create the response resolver for API Registry integration "${resolverForParam}". Use the tested lastResponse from that registry row, extract the records from the response into governed Data Source rows, and keep the resolver scoped to this integrationId.`);
3050
+ setHelperOpen(true);
3051
+ return;
2674
3052
  } else if (helperParam === "open") {
2675
3053
  setHelperInitialThread(null);
2676
3054
  setHelperOpen(true);
@@ -2678,6 +3056,7 @@ export default function DataModelShell() {
2678
3056
  const next = new URLSearchParams(searchParams.toString());
2679
3057
  next.delete("helper");
2680
3058
  next.delete("thread");
3059
+ next.delete("resolverFor");
2681
3060
  const query = next.toString();
2682
3061
  router.replace(query ? `/data-model?${query}` : "/data-model", { scroll: false });
2683
3062
  }, [workspaceConfig, searchParams, router]);
@@ -2778,7 +3157,20 @@ export default function DataModelShell() {
2778
3157
  useEffect(() => {
2779
3158
  const rowParam = searchParams?.get("row");
2780
3159
  if (!rowParam || !tables.length) return;
2781
- focusSandboxEnvironmentRow({ rowName: rowParam, deferOpen: true });
3160
+ const objectParam = searchParams?.get("object");
3161
+ const target = objectParam
3162
+ ? tables.find((table) => (
3163
+ table.objectId === objectParam
3164
+ || table.id === objectParam
3165
+ || table.source === objectParam
3166
+ || table.label === objectParam
3167
+ ))
3168
+ : null;
3169
+ if (target?.objectType === "sandbox-environment" || (!target && rowParam)) {
3170
+ focusSandboxEnvironmentRow({ rowName: rowParam, deferOpen: true });
3171
+ return;
3172
+ }
3173
+ requestAnimationFrame(() => setFocusRecordValue(rowParam));
2782
3174
  }, [focusSandboxEnvironmentRow, searchParams, tables]);
2783
3175
 
2784
3176
  // Flush any accumulated patch keys to the server. Called by the debounce
@@ -3155,6 +3547,17 @@ export default function DataModelShell() {
3155
3547
  router.push(`/?dashboard=${encodeURIComponent(target.dashboardId)}`);
3156
3548
  }
3157
3549
  }}
3550
+ onOpenSwarmWorkflow={(target) => {
3551
+ const objectId = String(target?.objectId || "").trim();
3552
+ const rowName = String(target?.name || "").trim();
3553
+ if (!objectId || !rowName) return;
3554
+ const params = new URLSearchParams({
3555
+ object: objectId,
3556
+ row: rowName,
3557
+ field: "orchestrationGraph"
3558
+ });
3559
+ router.push(`/workflows?${params.toString()}`);
3560
+ }}
3158
3561
  onApplied={(updatedConfig) => {
3159
3562
  // Anchor the user on the most recently created/updated Data Model
3160
3563
  // object so a helper-driven object.create lands on the surface
@@ -3205,7 +3608,9 @@ export default function DataModelShell() {
3205
3608
  onSave={save}
3206
3609
  onOpenThread={openHelperThreadFromRow}
3207
3610
  focusSandboxRowName={focusSandboxRowName}
3611
+ focusRecordValue={focusRecordValue}
3208
3612
  onFocusSandboxRowConsumed={() => setFocusSandboxRowName(null)}
3613
+ onFocusRecordConsumed={() => setFocusRecordValue(null)}
3209
3614
  onFocusSandboxRow={focusSandboxEnvironmentRow}
3210
3615
  selectedRecordIndex={selectedTableKey ? selectedRecordByTable[selectedTableKey] ?? null : null}
3211
3616
  onSelectedRecordIndexChange={(index) => {