@growthub/cli 0.13.0 → 0.13.2

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 (27) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +50 -25
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +38 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +522 -35
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +242 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +52 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +1203 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +163 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +190 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +64 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +376 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +6 -1
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1062 -2
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +10 -7
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +906 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/page.jsx +12 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +492 -28
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +114 -30
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/nav-workflows.js +54 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +322 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +734 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +73 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-sidecar-routing.js +24 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +21 -1
  27. package/package.json +1 -1
@@ -35,6 +35,7 @@ import {
35
35
  Tag,
36
36
  Terminal,
37
37
  ToggleLeft,
38
+ Trash2,
38
39
  Type,
39
40
  Users,
40
41
  X,
@@ -69,6 +70,17 @@ import { SandboxRunPanel } from "./SandboxRunPanel.jsx";
69
70
  import { StatusPill } from "./StatusPill.jsx";
70
71
  import { SegmentedToggle, ToggleField } from "./ToggleField.jsx";
71
72
  import { SourceTestPanel } from "./SourceTestPanel.jsx";
73
+ import { ApiRegistryActionCard } from "./ApiRegistryActionCard.jsx";
74
+ import { SandboxToolDraftPanel } from "./SandboxToolDraftPanel.jsx";
75
+ import { SandboxToolConfirmModal } from "./SandboxToolConfirmModal.jsx";
76
+ import { SandboxOrchestrationEditorPanel } from "./SandboxOrchestrationEditorPanel.jsx";
77
+ import { OrchestrationRunTracePanel } from "./OrchestrationRunTracePanel.jsx";
78
+ import {
79
+ buildSandboxRowFromApiRegistry,
80
+ findSandboxRowsForRegistry,
81
+ getOrchestrationGraphUiState,
82
+ redactSecretsFromText
83
+ } from "@/lib/orchestration-graph";
72
84
  import {
73
85
  FIELD_TYPE_ICON_NAMES,
74
86
  ICON_PICKER_SET,
@@ -551,6 +563,24 @@ function RecordFieldEditor({ table, tables, column, value, saving, editable, onD
551
563
  );
552
564
  }
553
565
 
566
+ function SandboxTraceFieldButton({ label, value, disabled, onOpen }) {
567
+ const hasValue = value !== null && value !== undefined && String(value).trim() !== "";
568
+ return (
569
+ <label className="dm-record-field dm-field-link">
570
+ <span>{label}</span>
571
+ <button
572
+ type="button"
573
+ className="dm-field-link__btn"
574
+ disabled={disabled || !hasValue}
575
+ onClick={() => onOpen?.()}
576
+ >
577
+ {hasValue ? String(value).slice(0, 80) + (String(value).length > 80 ? "…" : "") : "—"}
578
+ </button>
579
+ <span className="dm-field-link__hint">Opens run trace viewer</span>
580
+ </label>
581
+ );
582
+ }
583
+
554
584
  function SandboxRecordFields({
555
585
  draft,
556
586
  setDraft,
@@ -564,7 +594,8 @@ function SandboxRecordFields({
564
594
  sandboxHistoryMessage,
565
595
  loadingSandboxHistory,
566
596
  onLoadSandboxHistory,
567
- onExpandLastResponse
597
+ onOpenGraphSidecar,
598
+ onOpenTraceSidecar
568
599
  }) {
569
600
  const [sandboxAdapters, setSandboxAdapters] = useState([]);
570
601
  useEffect(() => {
@@ -830,32 +861,72 @@ function SandboxRecordFields({
830
861
  </label>
831
862
  </DrawerSection>
832
863
 
864
+ <DrawerSection title="Orchestration">
865
+ <div className="dm-record-field">
866
+ <span>{draft.orchestrationConfig !== undefined ? "orchestrationConfig" : "orchestrationGraph"}</span>
867
+ <button
868
+ type="button"
869
+ className="dm-btn-outline"
870
+ disabled={saving}
871
+ onClick={() => onOpenGraphSidecar?.()}
872
+ >
873
+ {getOrchestrationGraphUiState(draft.orchestrationGraph ?? draft.orchestrationConfig) === "populated" ? "Edit orchestration graph" : "Start orchestration graph"}
874
+ </button>
875
+ </div>
876
+ </DrawerSection>
877
+
833
878
  <DrawerSection title="Response & History">
834
- <label className="dm-record-field">
835
- <span>lastRunId</span>
836
- <input readOnly value={draft.lastRunId ?? ""} />
837
- </label>
879
+ <SandboxTraceFieldButton
880
+ label="lastRunId"
881
+ value={draft.lastRunId}
882
+ disabled={saving}
883
+ onOpen={() => onOpenTraceSidecar?.({ field: "lastRunId", runId: draft.lastRunId })}
884
+ />
838
885
 
839
- <label className="dm-record-field">
840
- <span>lastSourceId</span>
841
- <input readOnly value={draft.lastSourceId ?? ""} />
842
- </label>
886
+ <SandboxTraceFieldButton
887
+ label="lastSourceId"
888
+ value={draft.lastSourceId}
889
+ disabled={saving}
890
+ onOpen={() => onOpenTraceSidecar?.({ field: "lastSourceId" })}
891
+ />
843
892
 
844
- <label className="dm-record-field dm-json-field">
893
+ <label className="dm-record-field dm-field-link">
845
894
  <span>lastResponse</span>
846
895
  <button
847
896
  type="button"
848
- className="dm-json-expand"
849
- aria-label="Expand lastResponse JSON"
850
- title="Expand JSON"
851
- disabled={!draft.lastResponse}
852
- onClick={onExpandLastResponse}
897
+ className="dm-field-link__btn"
898
+ disabled={saving || !draft.lastResponse}
899
+ onClick={() => onOpenTraceSidecar?.({ field: "lastResponse" })}
853
900
  >
854
- <Maximize2 size={14} aria-hidden="true" />
901
+ {draft.lastResponse ? "View run trace" : "—"}
855
902
  </button>
856
- <textarea rows={10} readOnly value={draft.lastResponse ?? ""} />
903
+ <span className="dm-field-link__hint">Run output not the graph builder</span>
857
904
  </label>
858
905
 
906
+ <label className="dm-record-field">
907
+ <span>status</span>
908
+ <div className="dm-field-link__row">
909
+ <StatusPill value={draft.status} />
910
+ {(draft.lastRunId || draft.lastResponse) && (
911
+ <button
912
+ type="button"
913
+ className="dm-btn-ghost"
914
+ disabled={saving}
915
+ onClick={() => onOpenTraceSidecar?.({ field: "lastResponse", runId: draft.lastRunId })}
916
+ >
917
+ View latest run
918
+ </button>
919
+ )}
920
+ </div>
921
+ </label>
922
+
923
+ {draft.lastTested && (
924
+ <label className="dm-record-field">
925
+ <span>lastTested</span>
926
+ <input readOnly value={draft.lastTested} />
927
+ </label>
928
+ )}
929
+
859
930
  <div className="dm-record-field">
860
931
  <span>Run history</span>
861
932
  <button type="button" className="dm-btn-ghost" disabled={loadingSandboxHistory} onClick={onLoadSandboxHistory}>
@@ -885,7 +956,19 @@ function SandboxRecordFields({
885
956
  );
886
957
  }
887
958
 
888
- function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row, saving, onClose, onSave }) {
959
+ function DataModelRecordDrawer({
960
+ table,
961
+ tables,
962
+ workspaceConfig,
963
+ rowIndex,
964
+ row,
965
+ saving,
966
+ onClose,
967
+ onSave,
968
+ onFocusSandboxRow,
969
+ initialSidecar,
970
+ onClearInitialSidecar,
971
+ }) {
889
972
  const [draft, setDraft] = useState(row || {});
890
973
  const [editMode, setEditMode] = useState(false);
891
974
  const [pendingColumns, setPendingColumns] = useState(table.columns || []);
@@ -898,6 +981,15 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
898
981
  const [sandboxHistoryMessage, setSandboxHistoryMessage] = useState("");
899
982
  const [loadingSandboxHistory, setLoadingSandboxHistory] = useState(false);
900
983
  const [expandedJson, setExpandedJson] = useState(null);
984
+ const [sandboxToolFlow, setSandboxToolFlow] = useState(null);
985
+ const [sandboxToolDraft, setSandboxToolDraft] = useState({});
986
+ const [sandboxToolCreating, setSandboxToolCreating] = useState(false);
987
+ const [createdSandboxMeta, setCreatedSandboxMeta] = useState(null);
988
+ const [createdSandboxTesting, setCreatedSandboxTesting] = useState(false);
989
+ const [createdSandboxTestMessage, setCreatedSandboxTestMessage] = useState("");
990
+ const [sidecarMode, setSidecarMode] = useState(null);
991
+ const [traceField, setTraceField] = useState(null);
992
+ const [traceRunId, setTraceRunId] = useState("");
901
993
 
902
994
  useEffect(() => {
903
995
  setDraft(row || {});
@@ -909,10 +1001,28 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
909
1001
  setSandboxHistory([]);
910
1002
  setSandboxHistoryMessage("");
911
1003
  setExpandedJson(null);
912
- }, [row, rowIndex]);
1004
+ setSandboxToolFlow(null);
1005
+ setSandboxToolDraft({});
1006
+ setCreatedSandboxMeta(null);
1007
+ setCreatedSandboxTestMessage("");
1008
+ if (initialSidecar?.mode === "graph") {
1009
+ setSidecarMode("graph");
1010
+ setTraceField(null);
1011
+ setTraceRunId("");
1012
+ } else if (initialSidecar?.mode === "trace") {
1013
+ setSidecarMode("trace");
1014
+ setTraceField(initialSidecar.field || "lastResponse");
1015
+ setTraceRunId(String(initialSidecar.runId || row?.lastRunId || "").trim());
1016
+ } else {
1017
+ setSidecarMode(null);
1018
+ setTraceField(null);
1019
+ setTraceRunId("");
1020
+ }
1021
+ }, [row, rowIndex, initialSidecar]);
913
1022
 
914
1023
  if (rowIndex === null || rowIndex === undefined || !row) return null;
915
1024
 
1025
+ const isApiRegistry = table.objectType === "api-registry";
916
1026
  const isSandbox = table.objectType === "sandbox-environment";
917
1027
  const isDirty = JSON.stringify(draft || {}) !== JSON.stringify(row || {}) || JSON.stringify(pendingColumns) !== JSON.stringify(table.columns || []) || JSON.stringify(pendingHidden) !== JSON.stringify(table.fieldSettings?.hidden || []);
918
1028
 
@@ -1004,6 +1114,147 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1004
1114
  }
1005
1115
  }
1006
1116
 
1117
+ function ensureSandboxColumns(config, sandboxTable) {
1118
+ let next = config;
1119
+ let current = sandboxTable;
1120
+ for (const field of ["orchestrationGraph", "description"]) {
1121
+ if (!current.columns.includes(field)) {
1122
+ next = addTableField(next, current, field);
1123
+ const tables = listWorkspaceDataModelTables(next);
1124
+ current = tables.find((t) => t.objectId === sandboxTable.objectId) || current;
1125
+ }
1126
+ }
1127
+ return { config: next, sandboxTable: current };
1128
+ }
1129
+
1130
+ function createSandboxToolFromRegistry() {
1131
+ if (!sandboxToolDraft?.name?.trim()) return;
1132
+ const integrationId = String(draft?.integrationId || "").trim();
1133
+ if (integrationId && findSandboxRowsForRegistry(workspaceConfig, integrationId).length > 0) {
1134
+ setCreatedSandboxTestMessage("A sandbox tool already exists for this API Registry entry. Open it instead of creating a duplicate.");
1135
+ return;
1136
+ }
1137
+ setSandboxToolCreating(true);
1138
+ try {
1139
+ onSave((config) => {
1140
+ let next = config;
1141
+ if (integrationId && findSandboxRowsForRegistry(next, integrationId).length > 0) {
1142
+ return next;
1143
+ }
1144
+ let sandboxTable = listWorkspaceDataModelTables(next).find((t) => t.objectType === "sandbox-environment");
1145
+ if (!sandboxTable) {
1146
+ next = createTypedBusinessObject(next, {
1147
+ name: "Sandbox Environments",
1148
+ objectType: "sandbox-environment"
1149
+ });
1150
+ sandboxTable = listWorkspaceDataModelTables(next).find((t) => t.objectType === "sandbox-environment");
1151
+ }
1152
+ if (!sandboxTable) return next;
1153
+ const ensured = ensureSandboxColumns(next, sandboxTable);
1154
+ next = ensured.config;
1155
+ sandboxTable = ensured.sandboxTable;
1156
+ const newRow = buildSandboxRowFromApiRegistry(next, draft, {
1157
+ name: sandboxToolDraft.name,
1158
+ description: sandboxToolDraft.description,
1159
+ runLocality: sandboxToolDraft.runLocality,
1160
+ adapter: sandboxToolDraft.adapter,
1161
+ authRef: sandboxToolDraft.authRef,
1162
+ envRefs: sandboxToolDraft.envRefs,
1163
+ networkAllow: sandboxToolDraft.networkAllow,
1164
+ timeoutMs: sandboxToolDraft.timeoutMs,
1165
+ rootPath: sandboxToolDraft.rootPath,
1166
+ instructions: sandboxToolDraft.instructions,
1167
+ agentHost: sandboxToolDraft.agentHost,
1168
+ schedulerRegistryId: sandboxToolDraft.schedulerRegistryId,
1169
+ orchestrationGraph: sandboxToolDraft.orchestrationGraph
1170
+ });
1171
+ next = appendRowsToTable(next, sandboxTable, [newRow]);
1172
+ setCreatedSandboxMeta({
1173
+ objectId: sandboxTable.objectId,
1174
+ name: newRow.Name,
1175
+ authRef: newRow.authRef || sandboxToolDraft.authRef
1176
+ });
1177
+ setSandboxToolFlow("created");
1178
+ onFocusSandboxRow?.({ rowName: newRow.Name, deferOpen: true });
1179
+ return next;
1180
+ });
1181
+ } finally {
1182
+ setSandboxToolCreating(false);
1183
+ }
1184
+ }
1185
+
1186
+ async function runSandboxToolByName({ objectId, name }) {
1187
+ const rowName = String(name || "").trim();
1188
+ const objectIdValue = String(objectId || "").trim();
1189
+ if (!rowName || !objectIdValue) return;
1190
+ setCreatedSandboxTesting(true);
1191
+ setCreatedSandboxTestMessage("");
1192
+ try {
1193
+ const res = await fetch("/api/workspace/sandbox-run", {
1194
+ method: "POST",
1195
+ headers: { "content-type": "application/json" },
1196
+ body: JSON.stringify({ objectId: objectIdValue, name: rowName }),
1197
+ });
1198
+ const payload = await res.json();
1199
+ const responseText = redactSecretsFromText(JSON.stringify(payload.response ?? payload, null, 2));
1200
+ const status = payload.ok && String(payload.status || "").toLowerCase() === "connected" ? "connected" : "failed";
1201
+ const testedAt = payload.response?.ranAt || new Date().toISOString();
1202
+ const lastRunId = payload.runId || payload.response?.runId || "";
1203
+ const lastSourceId = payload.sourceId || payload.response?.sourceId || "";
1204
+ onSave((config) => {
1205
+ const sandboxTable = listWorkspaceDataModelTables(config).find((t) => t.objectType === "sandbox-environment");
1206
+ if (!sandboxTable) return config;
1207
+ const idx = (sandboxTable.rows || []).findIndex((r) => String(r?.Name || "").trim() === rowName);
1208
+ if (idx < 0) return config;
1209
+ let next = updateTableCell(config, sandboxTable, idx, "status", status);
1210
+ next = updateTableCell(next, sandboxTable, idx, "lastTested", testedAt);
1211
+ next = updateTableCell(next, sandboxTable, idx, "lastRunId", lastRunId);
1212
+ next = updateTableCell(next, sandboxTable, idx, "lastSourceId", lastSourceId);
1213
+ next = updateTableCell(next, sandboxTable, idx, "lastResponse", responseText);
1214
+ return next;
1215
+ });
1216
+ const safeError = redactSecretsFromText(
1217
+ payload.response?.error || payload.error || "Sandbox run failed"
1218
+ );
1219
+ setCreatedSandboxTestMessage(
1220
+ status === "connected"
1221
+ ? "Sandbox run succeeded — lastResponse and source record saved."
1222
+ : safeError
1223
+ );
1224
+ } catch (err) {
1225
+ setCreatedSandboxTestMessage(redactSecretsFromText(err.message || "Sandbox run failed"));
1226
+ } finally {
1227
+ setCreatedSandboxTesting(false);
1228
+ }
1229
+ }
1230
+
1231
+ function resolveSandboxTableMeta() {
1232
+ const sandboxTable = tables.find((t) => t.objectType === "sandbox-environment")
1233
+ || (workspaceConfig ? listWorkspaceDataModelTables(workspaceConfig).find((t) => t.objectType === "sandbox-environment") : null);
1234
+ return sandboxTable?.objectId ? { objectId: sandboxTable.objectId, table: sandboxTable } : null;
1235
+ }
1236
+
1237
+ async function runCreatedSandboxTest() {
1238
+ if (!createdSandboxMeta?.objectId || !createdSandboxMeta?.name) return;
1239
+ await runSandboxToolByName(createdSandboxMeta);
1240
+ }
1241
+
1242
+ async function runExistingSandboxTool({ name }) {
1243
+ const meta = resolveSandboxTableMeta();
1244
+ if (!meta) {
1245
+ setCreatedSandboxTestMessage("No sandbox-environment table in this workspace.");
1246
+ return;
1247
+ }
1248
+ await runSandboxToolByName({ objectId: meta.objectId, name });
1249
+ }
1250
+
1251
+ function openSandboxToolRow({ name }) {
1252
+ const rowName = String(name || "").trim();
1253
+ if (!rowName) return;
1254
+ onFocusSandboxRow?.({ rowName });
1255
+ onClose();
1256
+ }
1257
+
1007
1258
  async function runSandbox() {
1008
1259
  if (!table.objectId) {
1009
1260
  setSandboxMessage("Missing object id for this sandbox table.");
@@ -1078,17 +1329,49 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1078
1329
  }
1079
1330
  }
1080
1331
 
1332
+ function closeSidecar() {
1333
+ setSidecarMode(null);
1334
+ setTraceField(null);
1335
+ setTraceRunId("");
1336
+ onClearInitialSidecar?.();
1337
+ }
1338
+
1339
+ function openGraphSidecar() {
1340
+ setSidecarMode("graph");
1341
+ onClearInitialSidecar?.();
1342
+ }
1343
+
1344
+ function openTraceSidecar({ field, runId } = {}) {
1345
+ setSidecarMode("trace");
1346
+ setTraceField(field || "lastResponse");
1347
+ setTraceRunId(String(runId || draft?.lastRunId || "").trim());
1348
+ onClearInitialSidecar?.();
1349
+ }
1350
+
1351
+ function saveOrchestrationGraph(serialized) {
1352
+ if (rowIndex === null || rowIndex === undefined) return;
1353
+ const graphField = draft.orchestrationConfig !== undefined ? "orchestrationConfig" : "orchestrationGraph";
1354
+ onSave((config) => updateTableCell(config, table, rowIndex, graphField, serialized));
1355
+ setDraft((current) => ({ ...current, [graphField]: serialized }));
1356
+ }
1357
+
1358
+ const drawerWide = sandboxToolFlow === "draft" || sidecarMode === "graph" || sidecarMode === "trace";
1359
+ const hideRecordFields = isSandbox && (sidecarMode === "graph" || sidecarMode === "trace");
1360
+
1081
1361
  return (
1082
1362
  <>
1083
1363
  <div className="dm-record-backdrop" onClick={onClose} />
1084
- <aside className="dm-record-drawer" aria-label="Record details">
1364
+ <aside
1365
+ className={`dm-record-drawer${drawerWide ? " dm-record-drawer-wide" : ""}`}
1366
+ aria-label="Record details"
1367
+ >
1085
1368
  <header className="dm-record-drawer-head">
1086
1369
  <div>
1087
1370
  <p>Record</p>
1088
1371
  <h2>{draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}</h2>
1089
1372
  </div>
1090
1373
  <div className="dm-record-drawer-actions">
1091
- {!isSandbox && (
1374
+ {!isSandbox && sandboxToolFlow !== "draft" && (
1092
1375
  <button type="button" className="dm-sidebar-close" onClick={() => setEditMode((current) => !current)} aria-label="Toggle edit mode">
1093
1376
  <Pencil size={16} />
1094
1377
  </button>
@@ -1098,7 +1381,7 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1098
1381
  </button>
1099
1382
  </div>
1100
1383
  </header>
1101
- {(table.objectType === "api-registry" || table.objectType === "data-source") && (
1384
+ {(table.objectType === "api-registry" || table.objectType === "data-source") && sandboxToolFlow !== "draft" && (
1102
1385
  <SourceTestPanel
1103
1386
  status={draft.status}
1104
1387
  testing={testing}
@@ -1107,7 +1390,67 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1107
1390
  onTest={testApiRecord}
1108
1391
  />
1109
1392
  )}
1110
- {isSandbox && (
1393
+ {isApiRegistry && sandboxToolFlow !== "draft" && sandboxToolFlow !== "created" && (
1394
+ <ApiRegistryActionCard
1395
+ registryRow={draft}
1396
+ workspaceConfig={workspaceConfig}
1397
+ disabled={saving || sandboxToolCreating}
1398
+ testing={testing}
1399
+ sandboxRunning={createdSandboxTesting}
1400
+ onTestConnection={testApiRecord}
1401
+ onCreateSandboxTool={() => setSandboxToolFlow("draft")}
1402
+ onOpenSandboxTool={openSandboxToolRow}
1403
+ onRunSandboxTool={runExistingSandboxTool}
1404
+ />
1405
+ )}
1406
+ {isApiRegistry && sandboxToolFlow === "created" && createdSandboxMeta && (
1407
+ <section className="dm-api-action-card dm-api-action-card-success" aria-label="Sandbox tool created">
1408
+ <div className="dm-api-action-card-body">
1409
+ <p className="dm-api-action-card-eyebrow">Sandbox tool created</p>
1410
+ <h3>{createdSandboxMeta.name}</h3>
1411
+ <p>Governed sandbox row saved with orchestrationGraph. Run test to persist lastResponse — nothing auto-runs.</p>
1412
+ {createdSandboxTestMessage && <p className="dm-sandbox-tool-test-msg">{createdSandboxTestMessage}</p>}
1413
+ </div>
1414
+ <div className="dm-api-action-card-actions">
1415
+ <button
1416
+ type="button"
1417
+ className="dm-btn-outline dm-api-action-card-cta"
1418
+ disabled={saving}
1419
+ onClick={() => openSandboxToolRow({ name: createdSandboxMeta.name })}
1420
+ >
1421
+ Open sandbox tool
1422
+ </button>
1423
+ <button
1424
+ type="button"
1425
+ className="dm-btn-primary-sm dm-api-action-card-cta"
1426
+ disabled={createdSandboxTesting || saving}
1427
+ onClick={runCreatedSandboxTest}
1428
+ >
1429
+ {createdSandboxTesting ? "Running…" : "Run sandbox"}
1430
+ </button>
1431
+ </div>
1432
+ </section>
1433
+ )}
1434
+ {isApiRegistry && sandboxToolFlow === "draft" && (
1435
+ <SandboxToolDraftPanel
1436
+ registryRow={draft}
1437
+ draftOptions={sandboxToolDraft}
1438
+ disabled={saving || sandboxToolCreating}
1439
+ onDraftChange={setSandboxToolDraft}
1440
+ onRequestConfirm={() => setSandboxToolFlow("confirm")}
1441
+ onCancel={() => setSandboxToolFlow(null)}
1442
+ />
1443
+ )}
1444
+ <SandboxToolConfirmModal
1445
+ open={isApiRegistry && sandboxToolFlow === "confirm"}
1446
+ toolName={sandboxToolDraft?.name || ""}
1447
+ authRef={sandboxToolDraft?.authRef || draft.authRef}
1448
+ orchestrationGraph={sandboxToolDraft?.orchestrationGraph}
1449
+ creating={sandboxToolCreating}
1450
+ onConfirm={createSandboxToolFromRegistry}
1451
+ onCancel={() => setSandboxToolFlow("draft")}
1452
+ />
1453
+ {isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && (
1111
1454
  <SandboxRunPanel
1112
1455
  status={draft.status}
1113
1456
  sandboxRunning={sandboxRunning}
@@ -1117,8 +1460,27 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1117
1460
  onRun={runSandbox}
1118
1461
  />
1119
1462
  )}
1463
+ {isSandbox && sidecarMode === "graph" && (
1464
+ <SandboxOrchestrationEditorPanel
1465
+ sandboxRow={draft}
1466
+ workspaceConfig={workspaceConfig}
1467
+ disabled={saving}
1468
+ onSaveGraph={saveOrchestrationGraph}
1469
+ onBack={closeSidecar}
1470
+ />
1471
+ )}
1472
+ {isSandbox && sidecarMode === "trace" && (
1473
+ <OrchestrationRunTracePanel
1474
+ row={draft}
1475
+ objectId={table.objectId}
1476
+ fieldName={traceField || "lastResponse"}
1477
+ selectedRunId={traceRunId}
1478
+ onBack={closeSidecar}
1479
+ onOpenGraph={openGraphSidecar}
1480
+ />
1481
+ )}
1120
1482
  <div className="dm-record-fields">
1121
- {isSandbox ? (
1483
+ {isApiRegistry && sandboxToolFlow === "draft" ? null : hideRecordFields ? null : isSandbox ? (
1122
1484
  <SandboxRecordFields
1123
1485
  draft={draft}
1124
1486
  setDraft={setDraft}
@@ -1132,7 +1494,8 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1132
1494
  sandboxHistoryMessage={sandboxHistoryMessage}
1133
1495
  loadingSandboxHistory={loadingSandboxHistory}
1134
1496
  onLoadSandboxHistory={loadSandboxHistory}
1135
- onExpandLastResponse={expandLastResponse}
1497
+ onOpenGraphSidecar={openGraphSidecar}
1498
+ onOpenTraceSidecar={openTraceSidecar}
1136
1499
  />
1137
1500
  ) : groupRecordColumns(table.columns || []).map((section) => (
1138
1501
  <DrawerSection key={section.title} title={section.title}>
@@ -1209,8 +1572,34 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1209
1572
  );
1210
1573
  }
1211
1574
 
1212
- function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave, onOpenThread }) {
1575
+ const SANDBOX_SIDECAR_COLUMNS = new Set(["orchestrationGraph", "orchestrationConfig", "lastResponse", "lastRunId", "lastSourceId"]);
1576
+
1577
+ function sandboxSidecarForColumn(column, row) {
1578
+ if (column === "orchestrationGraph" || column === "orchestrationConfig") return { mode: "graph" };
1579
+ if (column === "lastResponse") return { mode: "trace", field: "lastResponse" };
1580
+ if (column === "lastRunId") return { mode: "trace", field: "lastRunId", runId: row?.lastRunId };
1581
+ if (column === "lastSourceId") return { mode: "trace", field: "lastSourceId" };
1582
+ return null;
1583
+ }
1584
+
1585
+ function isSandboxSidecarCell(table, column) {
1586
+ return table?.objectType === "sandbox-environment" && SANDBOX_SIDECAR_COLUMNS.has(column);
1587
+ }
1588
+
1589
+ function DataModelTableSurface({
1590
+ table,
1591
+ tables,
1592
+ workspaceConfig,
1593
+ saving,
1594
+ onSave,
1595
+ onOpenThread,
1596
+ focusSandboxRowName,
1597
+ onFocusSandboxRowConsumed,
1598
+ onFocusSandboxRow,
1599
+ }) {
1600
+ const router = useRouter();
1213
1601
  const [selectedRow, setSelectedRow] = useState(null);
1602
+ const [initialSidecar, setInitialSidecar] = useState(null);
1214
1603
  const [fieldName, setFieldName] = useState("");
1215
1604
  const [fieldType, setFieldType] = useState("text");
1216
1605
  const [addingField, setAddingField] = useState(false);
@@ -1237,6 +1626,7 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1237
1626
  setSelectMenuOpen(false);
1238
1627
  setPageIndex(0);
1239
1628
  }, [table.id]);
1629
+
1240
1630
  useEffect(() => {
1241
1631
  setFieldName("");
1242
1632
  setFieldType("text");
@@ -1269,6 +1659,20 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1269
1659
  const pageSelectedCount = pageEntries.filter((entry) => selectedRows.has(entry.originalIndex)).length;
1270
1660
  const allPageSelected = pageEntries.length > 0 && pageSelectedCount === pageEntries.length;
1271
1661
 
1662
+ useEffect(() => {
1663
+ if (!focusSandboxRowName || table.objectType !== "sandbox-environment") return;
1664
+ const wanted = String(focusSandboxRowName).trim();
1665
+ if (!wanted) return;
1666
+ const originalIndex = (table.rows || []).findIndex((r) => String(r?.Name || "").trim() === wanted);
1667
+ if (originalIndex < 0) return;
1668
+ const visibleIndex = rowEntries.findIndex((entry) => entry.originalIndex === originalIndex);
1669
+ if (visibleIndex < 0) return;
1670
+ const pageForRow = Math.floor(visibleIndex / pageSize);
1671
+ setPageIndex(pageForRow);
1672
+ setSelectedRow(visibleIndex - pageForRow * pageSize);
1673
+ onFocusSandboxRowConsumed?.();
1674
+ }, [focusSandboxRowName, table.id, table.objectType, table.rows, rowEntries, pageSize, onFocusSandboxRowConsumed]);
1675
+
1272
1676
  useEffect(() => {
1273
1677
  setPageIndex((current) => Math.min(current, pageCount - 1));
1274
1678
  }, [pageCount]);
@@ -1352,6 +1756,15 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1352
1756
  setFilterTarget("");
1353
1757
  }
1354
1758
 
1759
+ function openSandboxGraph(column, row) {
1760
+ const rowId = String(row?.Name || row?.name || row?.slug || row?.id || "").trim();
1761
+ const field = String(column || "orchestrationConfig").trim();
1762
+ if (!table.objectId || !rowId) return;
1763
+ router.push(
1764
+ `/workflows?object=${encodeURIComponent(table.objectId)}&row=${encodeURIComponent(rowId)}&field=${encodeURIComponent(field)}`
1765
+ );
1766
+ }
1767
+
1355
1768
  function removeFilter(fieldId) {
1356
1769
  updateSettings((current) => {
1357
1770
  const clauses = (current.filter?.clauses || []).filter((clause) => clause.fieldId !== fieldId);
@@ -1431,13 +1844,10 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1431
1844
 
1432
1845
  function deleteSelectedRows() {
1433
1846
  if (!selectedRows.size) return;
1434
- if (!confirmDeleteSelection) {
1435
- setConfirmDeleteSelection(true);
1436
- return;
1437
- }
1438
1847
  const rowIndexes = Array.from(selectedRows).sort((a, b) => b - a);
1439
1848
  onSave((config) => rowIndexes.reduce((nextConfig, rowIndex) => deleteTableRow(nextConfig, table, rowIndex), config));
1440
1849
  setSelectedRow(null);
1850
+ setConfirmDeleteSelection(false);
1441
1851
  clearRowSelection();
1442
1852
  }
1443
1853
 
@@ -1504,8 +1914,8 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1504
1914
  {table.mutable && selectedRowCount > 0 && (
1505
1915
  <>
1506
1916
  <button type="button" className="dm-btn-ghost" disabled={saving} onClick={clearRowSelection}>Cancel selection</button>
1507
- <button type="button" className="dm-btn-danger-sm" disabled={saving} onClick={deleteSelectedRows}>
1508
- {confirmDeleteSelection ? `Confirm delete ${selectedRowCount}` : "Delete"}
1917
+ <button type="button" className="dm-btn-danger-sm" disabled={saving} onClick={() => setConfirmDeleteSelection(true)}>
1918
+ <Trash2 size={13} />Delete
1509
1919
  </button>
1510
1920
  </>
1511
1921
  )}
@@ -1667,6 +2077,26 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1667
2077
  />
1668
2078
  ) : column.toLowerCase() === "status" ? (
1669
2079
  <StatusPill value={row?.[column]} />
2080
+ ) : isSandboxSidecarCell(table, column) ? (
2081
+ <button
2082
+ type="button"
2083
+ className={`dm-cell-link${row?.[column] ? "" : " dm-cell-empty"}`}
2084
+ disabled={column !== "orchestrationGraph" && column !== "orchestrationConfig" && !row?.[column]}
2085
+ onClick={(event) => {
2086
+ event.stopPropagation();
2087
+ if (column === "orchestrationGraph" || column === "orchestrationConfig") {
2088
+ openSandboxGraph(column, row);
2089
+ return;
2090
+ }
2091
+ const sidecar = sandboxSidecarForColumn(column, row);
2092
+ setSelectedRow(visibleIndex);
2093
+ setInitialSidecar(sidecar);
2094
+ }}
2095
+ >
2096
+ {column === "orchestrationGraph" || column === "orchestrationConfig"
2097
+ ? (getOrchestrationGraphUiState(row?.[column]) === "populated" ? "Edit graph" : "Start graph")
2098
+ : (formatCellValue(row?.[column], column) || "View trace")}
2099
+ </button>
1670
2100
  ) : (
1671
2101
  <span className={row?.[column] ? "" : "dm-cell-empty"}>
1672
2102
  {formatCellValue(row?.[column], column) || "—"}
@@ -1711,9 +2141,36 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1711
2141
  rowIndex={selectedEntry?.originalIndex ?? null}
1712
2142
  row={selectedRecord}
1713
2143
  saving={saving}
1714
- onClose={() => setSelectedRow(null)}
2144
+ onClose={() => { setSelectedRow(null); setInitialSidecar(null); }}
1715
2145
  onSave={onSave}
2146
+ onFocusSandboxRow={onFocusSandboxRow}
2147
+ initialSidecar={initialSidecar}
2148
+ onClearInitialSidecar={() => setInitialSidecar(null)}
1716
2149
  />
2150
+ {confirmDeleteSelection && selectedRowCount > 0 && (
2151
+ <div className="dm-orch-modal-backdrop" onClick={() => setConfirmDeleteSelection(false)}>
2152
+ <section className="dm-orch-modal" role="dialog" aria-modal="true" aria-label="Confirm row deletion" onClick={(event) => event.stopPropagation()}>
2153
+ <header className="dm-orch-modal-head">
2154
+ <div>
2155
+ <p>Confirm deletion</p>
2156
+ <h2>Delete selected records?</h2>
2157
+ </div>
2158
+ <button type="button" className="dm-icon-btn" onClick={() => setConfirmDeleteSelection(false)} aria-label="Close delete confirmation">
2159
+ <X size={15} />
2160
+ </button>
2161
+ </header>
2162
+ <div className="dm-orch-modal-body">
2163
+ <p>This will permanently remove {pluralize(selectedRowCount, "selected record")} from {table.label || table.source}.</p>
2164
+ </div>
2165
+ <footer className="dm-orch-modal-foot">
2166
+ <button type="button" className="dm-btn-outline" onClick={() => setConfirmDeleteSelection(false)}>Cancel</button>
2167
+ <button type="button" className="dm-btn-danger-sm" disabled={saving} onClick={deleteSelectedRows}>
2168
+ <Trash2 size={13} />Delete {selectedRowCount}
2169
+ </button>
2170
+ </footer>
2171
+ </section>
2172
+ </div>
2173
+ )}
1717
2174
  </div>
1718
2175
  );
1719
2176
  }
@@ -1991,6 +2448,7 @@ export default function DataModelShell() {
1991
2448
  const [helperInitialPrompt, setHelperInitialPrompt] = useState("");
1992
2449
  const [helperInitialThread, setHelperInitialThread] = useState(null);
1993
2450
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
2451
+ const [focusSandboxRowName, setFocusSandboxRowName] = useState(null);
1994
2452
  const pendingPatchRef = useRef({});
1995
2453
  const saveTimerRef = useRef(null);
1996
2454
 
@@ -2083,6 +2541,19 @@ export default function DataModelShell() {
2083
2541
 
2084
2542
  const selectedTable = tables.find((t) => t.source === selectedSource) || tables[0] || null;
2085
2543
 
2544
+ const focusSandboxEnvironmentRow = useCallback(({ rowName, deferOpen = false } = {}) => {
2545
+ const wanted = String(rowName || "").trim();
2546
+ if (!wanted) return;
2547
+ const sandboxTable = tables.find((t) => t.objectType === "sandbox-environment");
2548
+ if (!sandboxTable?.source) return;
2549
+ setSelectedSource(sandboxTable.source);
2550
+ if (!deferOpen) {
2551
+ setFocusSandboxRowName(wanted);
2552
+ } else {
2553
+ requestAnimationFrame(() => setFocusSandboxRowName(wanted));
2554
+ }
2555
+ }, [tables]);
2556
+
2086
2557
  useEffect(() => {
2087
2558
  if (!selectedSource && tables[0]) setSelectedSource(tables[0].source);
2088
2559
  }, [selectedSource, tables]);
@@ -2101,6 +2572,12 @@ export default function DataModelShell() {
2101
2572
  }
2102
2573
  }, [searchParams, selectedSource, tables]);
2103
2574
 
2575
+ useEffect(() => {
2576
+ const rowParam = searchParams?.get("row");
2577
+ if (!rowParam || !tables.length) return;
2578
+ focusSandboxEnvironmentRow({ rowName: rowParam, deferOpen: true });
2579
+ }, [focusSandboxEnvironmentRow, searchParams, tables]);
2580
+
2104
2581
  // Flush any accumulated patch keys to the server. Called by the debounce
2105
2582
  // timer and on visibilitychange/beforeunload so no local edit is lost.
2106
2583
  const flushPendingPatch = useCallback(async () => {
@@ -2271,7 +2748,7 @@ export default function DataModelShell() {
2271
2748
  run: () => setAddOpen(true)
2272
2749
  },
2273
2750
  {
2274
- id: "nav.dashboards", group: "Navigation", label: "Go to Dashboards",
2751
+ id: "nav.builder", group: "Navigation", label: "Go to Builder",
2275
2752
  run: () => { window.location.href = "/"; }
2276
2753
  },
2277
2754
  {
@@ -2416,7 +2893,17 @@ export default function DataModelShell() {
2416
2893
  selectedTable && (
2417
2894
  <section className="dm-detail-v2 dm-detail-v3">
2418
2895
  <SourceValidationBanner table={selectedTable} />
2419
- <DataModelTableSurface workspaceConfig={workspaceConfig} table={selectedTable} tables={tables} saving={saving} onSave={save} onOpenThread={openHelperThreadFromRow} />
2896
+ <DataModelTableSurface
2897
+ workspaceConfig={workspaceConfig}
2898
+ table={selectedTable}
2899
+ tables={tables}
2900
+ saving={saving}
2901
+ onSave={save}
2902
+ onOpenThread={openHelperThreadFromRow}
2903
+ focusSandboxRowName={focusSandboxRowName}
2904
+ onFocusSandboxRowConsumed={() => setFocusSandboxRowName(null)}
2905
+ onFocusSandboxRow={focusSandboxEnvironmentRow}
2906
+ />
2420
2907
  </section>
2421
2908
  )
2422
2909
  )}