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