@growthub/cli 0.12.2 → 0.13.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 (31) 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 +556 -248
  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/components/dm-shared.jsx +8 -2
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +6 -1
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +2897 -934
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +10 -7
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/views/[viewId]/page.jsx +206 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +906 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/page.jsx +12 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +493 -28
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +1363 -8
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/nav-workflows.js +54 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +322 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +734 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +73 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-sidecar-routing.js +24 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +13 -4
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +96 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +122 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +1 -0
  31. package/package.json +1 -1
@@ -25,18 +25,17 @@ import {
25
25
  Layers,
26
26
  Link2,
27
27
  Lock,
28
- List,
29
28
  Mail,
30
29
  Maximize2,
31
30
  MoreHorizontal,
32
31
  Plus,
33
- Pin,
34
32
  Pencil,
35
33
  Search,
36
34
  ShoppingCart,
37
35
  Tag,
38
36
  Terminal,
39
37
  ToggleLeft,
38
+ Trash2,
40
39
  Type,
41
40
  Users,
42
41
  X,
@@ -62,7 +61,6 @@ import {
62
61
  parseSandboxAllowList,
63
62
  parseSandboxEnvRefs,
64
63
  replaceTableContent,
65
- snapshotTableViewState,
66
64
  transformTableSchema,
67
65
  updateTableFieldSettings,
68
66
  updateTableCell,
@@ -72,6 +70,17 @@ import { SandboxRunPanel } from "./SandboxRunPanel.jsx";
72
70
  import { StatusPill } from "./StatusPill.jsx";
73
71
  import { SegmentedToggle, ToggleField } from "./ToggleField.jsx";
74
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";
75
84
  import {
76
85
  FIELD_TYPE_ICON_NAMES,
77
86
  ICON_PICKER_SET,
@@ -199,88 +208,20 @@ function applyRowsView(rows, settings) {
199
208
  });
200
209
  }
201
210
 
202
- function ObjectViewPicker({ tables, selectedTable, saving, onSelectSource, onSave }) {
211
+ function ObjectViewPicker({ tables, selectedTable, onSelectSource }) {
203
212
  const pickerRef = useRef(null);
204
213
  const [open, setOpen] = useState(false);
205
- const [mode, setMode] = useState("all");
206
- const [newViewName, setNewViewName] = useState("");
207
- const [viewMenuId, setViewMenuId] = useState("");
208
- const currentViews = selectedTable?.fieldSettings?.views || [];
209
- const favoriteObjects = tables.filter((table) => table.fieldSettings?.favorite);
210
214
 
211
215
  useEffect(() => {
212
216
  function handlePointer(event) {
213
217
  if (!pickerRef.current?.contains(event.target)) {
214
- setViewMenuId("");
218
+ setOpen(false);
215
219
  }
216
220
  }
217
221
  document.addEventListener("pointerdown", handlePointer);
218
222
  return () => document.removeEventListener("pointerdown", handlePointer);
219
223
  }, []);
220
224
 
221
- function applyView(view) {
222
- if (!selectedTable) return;
223
- const nextState = view
224
- ? { ...snapshotTableViewState(view), activeViewId: view.id }
225
- : { activeViewId: "", hidden: [], order: selectedTable.columns, sort: [], filter: null };
226
- onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
227
- ...settings,
228
- ...nextState
229
- })));
230
- setOpen(false);
231
- }
232
-
233
- function createView() {
234
- const name = newViewName.trim();
235
- if (!selectedTable || !name) return;
236
- const viewId = `view_${Date.now().toString(36)}`;
237
- onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
238
- ...settings,
239
- activeViewId: viewId,
240
- views: [...(settings.views || []), {
241
- id: viewId,
242
- name,
243
- favorite: false,
244
- locked: false,
245
- ...snapshotTableViewState(settings)
246
- }]
247
- })));
248
- setNewViewName("");
249
- }
250
-
251
- function toggleViewFavorite(viewId) {
252
- if (!selectedTable) return;
253
- onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
254
- ...settings,
255
- views: (settings.views || []).map((view) => view.id === viewId ? { ...view, favorite: !view.favorite } : view)
256
- })));
257
- }
258
-
259
- function deleteView(viewId) {
260
- if (!selectedTable) return;
261
- onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
262
- ...settings,
263
- activeViewId: settings.activeViewId === viewId ? "" : settings.activeViewId,
264
- views: (settings.views || []).filter((view) => view.id !== viewId)
265
- })));
266
- setViewMenuId("");
267
- }
268
-
269
- function renameView(view) {
270
- if (!selectedTable) return;
271
- const nextName = window.prompt("Rename view", view.name);
272
- if (!nextName?.trim()) return;
273
- onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
274
- ...settings,
275
- views: (settings.views || []).map((candidate) => candidate.id === view.id ? { ...candidate, name: nextName.trim() } : candidate)
276
- })));
277
- setViewMenuId("");
278
- }
279
-
280
- const activeView = currentViews.find((view) => view.id === selectedTable?.fieldSettings?.activeViewId) || null;
281
- const objects = mode === "views" ? [] : tables;
282
- const views = mode === "objects" ? [] : currentViews;
283
-
284
225
  return (
285
226
  <div
286
227
  ref={pickerRef}
@@ -288,125 +229,36 @@ function ObjectViewPicker({ tables, selectedTable, saving, onSelectSource, onSav
288
229
  onBlur={(event) => {
289
230
  if (!event.currentTarget.contains(event.relatedTarget)) {
290
231
  setOpen(false);
291
- setViewMenuId("");
292
232
  }
293
233
  }}
294
234
  >
295
235
  <button type="button" className="dm-picker-trigger" onClick={() => setOpen((current) => !current)}>
296
236
  <LucideIcon name={selectedTable?.icon || OBJECT_TYPE_PRESETS[selectedTable?.objectType]?.icon || "Database"} size={14} />
297
237
  <span className="dm-picker-trigger-copy">
298
- <strong>{activeView?.name || selectedTable?.label || "Object"}</strong>
238
+ <strong>{selectedTable?.label || "Object"}</strong>
299
239
  <em>{pluralize(selectedTable?.columns?.length || 0, "field")} · {pluralize(selectedTable?.rows?.length || 0, "record")}</em>
300
240
  </span>
301
241
  <ChevronDown size={14} />
302
242
  </button>
303
243
  {open && (
304
244
  <div className="dm-picker-popover">
305
- {favoriteObjects.length > 0 && (
306
- <div className="dm-picker-section">
307
- <p>Favorites</p>
308
- {favoriteObjects.map((table, favIdx) => (
309
- <button key={`favorite-${table.id || table.source}-${favIdx}`} type="button" className="dm-picker-row" onClick={() => onSelectSource(table.source)}>
310
- <Pin size={14} />
311
- <span>{table.label}</span>
312
- </button>
245
+ <div className="dm-picker-section">
246
+ <p>Objects</p>
247
+ <div className="dm-picker-scroll">
248
+ {tables.map((table, objIdx) => (
249
+ <div key={`${table.id || table.source}:${objIdx}`} className={`dm-picker-item${selectedTable?.source === table.source ? " active" : ""}`}>
250
+ <button type="button" className="dm-picker-row" onClick={() => {
251
+ onSelectSource(table.source);
252
+ setOpen(false);
253
+ }}>
254
+ <LucideIcon name={table.icon || OBJECT_TYPE_PRESETS[table.objectType]?.icon || "Database"} size={14} />
255
+ <span>{table.label}</span>
256
+ {isLockedObject(table) && <Lock size={12} className="dm-picker-lock" />}
257
+ </button>
258
+ </div>
313
259
  ))}
314
260
  </div>
315
- )}
316
- <div className="dm-picker-tabs">
317
- {[
318
- { id: "all", label: "All" },
319
- { id: "objects", label: "Objects" },
320
- { id: "views", label: "Views" },
321
- ].map((item) => (
322
- <button key={item.id} type="button" className={mode === item.id ? "active" : ""} onClick={() => setMode(item.id)}>
323
- {item.label}
324
- </button>
325
- ))}
326
261
  </div>
327
- {objects.length > 0 && (
328
- <div className="dm-picker-section">
329
- <p>Objects</p>
330
- <div className="dm-picker-scroll">
331
- {objects.map((table, objIdx) => (
332
- <div key={`${table.id || table.source}:${objIdx}`} className={`dm-picker-item${selectedTable?.source === table.source ? " active" : ""}`}>
333
- <button type="button" className="dm-picker-row" onClick={() => {
334
- onSelectSource(table.source);
335
- setOpen(false);
336
- }}>
337
- <LucideIcon name={table.icon || OBJECT_TYPE_PRESETS[table.objectType]?.icon || "Database"} size={14} />
338
- <span>{table.label}</span>
339
- {isLockedObject(table) && <Lock size={12} className="dm-picker-lock" />}
340
- </button>
341
- </div>
342
- ))}
343
- </div>
344
- </div>
345
- )}
346
- {selectedTable && (
347
- <div className="dm-picker-section">
348
- <p>Views</p>
349
- <button type="button" className={`dm-picker-row${!activeView ? " active" : ""}`} onClick={() => applyView(null)}>
350
- <List size={14} />
351
- <span>{selectedTable.label}</span>
352
- {isLockedObject(selectedTable) && <Lock size={12} className="dm-picker-lock" />}
353
- </button>
354
- <div className="dm-picker-scroll">
355
- {views.map((view) => (
356
- <div key={view.id} className={`dm-picker-item${activeView?.id === view.id ? " active" : ""}`}>
357
- <button type="button" className="dm-picker-row" onClick={() => applyView(view)}>
358
- <List size={14} />
359
- <span>{view.name}</span>
360
- </button>
361
- <div className="dm-picker-actions">
362
- <button
363
- type="button"
364
- className="dm-picker-icon-btn"
365
- aria-label="View actions"
366
- onClick={(event) => {
367
- event.stopPropagation();
368
- setViewMenuId((current) => current === view.id ? "" : view.id);
369
- }}
370
- >
371
- <MoreHorizontal size={12} style={{ transform: "rotate(90deg)" }} />
372
- </button>
373
- {viewMenuId === view.id && (
374
- <div className="dm-picker-menu">
375
- <button type="button" onClick={() => toggleViewFavorite(view.id)}>
376
- <Pin size={13} />
377
- {view.favorite ? "Unpin" : "Pin"}
378
- </button>
379
- <button type="button" onClick={() => renameView(view)}>
380
- <Type size={13} />
381
- Rename
382
- </button>
383
- {!view.locked && (
384
- <button type="button" className="danger" onClick={() => deleteView(view.id)}>
385
- <X size={13} />
386
- Delete
387
- </button>
388
- )}
389
- </div>
390
- )}
391
- </div>
392
- </div>
393
- ))}
394
- </div>
395
- <div className="dm-picker-create">
396
- <input
397
- value={newViewName}
398
- placeholder="New view name"
399
- onChange={(event) => setNewViewName(event.target.value)}
400
- onKeyDown={(event) => {
401
- if (event.key === "Enter") createView();
402
- }}
403
- />
404
- <button type="button" className="dm-btn-outline" disabled={saving || !newViewName.trim()} onClick={createView}>
405
- <Plus size={13} />Add view
406
- </button>
407
- </div>
408
- </div>
409
- )}
410
262
  </div>
411
263
  )}
412
264
  </div>
@@ -711,6 +563,24 @@ function RecordFieldEditor({ table, tables, column, value, saving, editable, onD
711
563
  );
712
564
  }
713
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
+
714
584
  function SandboxRecordFields({
715
585
  draft,
716
586
  setDraft,
@@ -724,7 +594,8 @@ function SandboxRecordFields({
724
594
  sandboxHistoryMessage,
725
595
  loadingSandboxHistory,
726
596
  onLoadSandboxHistory,
727
- onExpandLastResponse
597
+ onOpenGraphSidecar,
598
+ onOpenTraceSidecar
728
599
  }) {
729
600
  const [sandboxAdapters, setSandboxAdapters] = useState([]);
730
601
  useEffect(() => {
@@ -990,32 +861,72 @@ function SandboxRecordFields({
990
861
  </label>
991
862
  </DrawerSection>
992
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
+
993
878
  <DrawerSection title="Response & History">
994
- <label className="dm-record-field">
995
- <span>lastRunId</span>
996
- <input readOnly value={draft.lastRunId ?? ""} />
997
- </label>
879
+ <SandboxTraceFieldButton
880
+ label="lastRunId"
881
+ value={draft.lastRunId}
882
+ disabled={saving}
883
+ onOpen={() => onOpenTraceSidecar?.({ field: "lastRunId", runId: draft.lastRunId })}
884
+ />
998
885
 
999
- <label className="dm-record-field">
1000
- <span>lastSourceId</span>
1001
- <input readOnly value={draft.lastSourceId ?? ""} />
1002
- </label>
886
+ <SandboxTraceFieldButton
887
+ label="lastSourceId"
888
+ value={draft.lastSourceId}
889
+ disabled={saving}
890
+ onOpen={() => onOpenTraceSidecar?.({ field: "lastSourceId" })}
891
+ />
1003
892
 
1004
- <label className="dm-record-field dm-json-field">
893
+ <label className="dm-record-field dm-field-link">
1005
894
  <span>lastResponse</span>
1006
895
  <button
1007
896
  type="button"
1008
- className="dm-json-expand"
1009
- aria-label="Expand lastResponse JSON"
1010
- title="Expand JSON"
1011
- disabled={!draft.lastResponse}
1012
- onClick={onExpandLastResponse}
897
+ className="dm-field-link__btn"
898
+ disabled={saving || !draft.lastResponse}
899
+ onClick={() => onOpenTraceSidecar?.({ field: "lastResponse" })}
1013
900
  >
1014
- <Maximize2 size={14} aria-hidden="true" />
901
+ {draft.lastResponse ? "View run trace" : "—"}
1015
902
  </button>
1016
- <textarea rows={10} readOnly value={draft.lastResponse ?? ""} />
903
+ <span className="dm-field-link__hint">Run output not the graph builder</span>
1017
904
  </label>
1018
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
+
1019
930
  <div className="dm-record-field">
1020
931
  <span>Run history</span>
1021
932
  <button type="button" className="dm-btn-ghost" disabled={loadingSandboxHistory} onClick={onLoadSandboxHistory}>
@@ -1045,7 +956,19 @@ function SandboxRecordFields({
1045
956
  );
1046
957
  }
1047
958
 
1048
- 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
+ }) {
1049
972
  const [draft, setDraft] = useState(row || {});
1050
973
  const [editMode, setEditMode] = useState(false);
1051
974
  const [pendingColumns, setPendingColumns] = useState(table.columns || []);
@@ -1058,6 +981,15 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1058
981
  const [sandboxHistoryMessage, setSandboxHistoryMessage] = useState("");
1059
982
  const [loadingSandboxHistory, setLoadingSandboxHistory] = useState(false);
1060
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("");
1061
993
 
1062
994
  useEffect(() => {
1063
995
  setDraft(row || {});
@@ -1069,10 +1001,28 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1069
1001
  setSandboxHistory([]);
1070
1002
  setSandboxHistoryMessage("");
1071
1003
  setExpandedJson(null);
1072
- }, [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]);
1073
1022
 
1074
1023
  if (rowIndex === null || rowIndex === undefined || !row) return null;
1075
1024
 
1025
+ const isApiRegistry = table.objectType === "api-registry";
1076
1026
  const isSandbox = table.objectType === "sandbox-environment";
1077
1027
  const isDirty = JSON.stringify(draft || {}) !== JSON.stringify(row || {}) || JSON.stringify(pendingColumns) !== JSON.stringify(table.columns || []) || JSON.stringify(pendingHidden) !== JSON.stringify(table.fieldSettings?.hidden || []);
1078
1028
 
@@ -1164,6 +1114,147 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1164
1114
  }
1165
1115
  }
1166
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
+
1167
1258
  async function runSandbox() {
1168
1259
  if (!table.objectId) {
1169
1260
  setSandboxMessage("Missing object id for this sandbox table.");
@@ -1238,17 +1329,49 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1238
1329
  }
1239
1330
  }
1240
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
+
1241
1361
  return (
1242
1362
  <>
1243
1363
  <div className="dm-record-backdrop" onClick={onClose} />
1244
- <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
+ >
1245
1368
  <header className="dm-record-drawer-head">
1246
1369
  <div>
1247
1370
  <p>Record</p>
1248
1371
  <h2>{draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}</h2>
1249
1372
  </div>
1250
1373
  <div className="dm-record-drawer-actions">
1251
- {!isSandbox && (
1374
+ {!isSandbox && sandboxToolFlow !== "draft" && (
1252
1375
  <button type="button" className="dm-sidebar-close" onClick={() => setEditMode((current) => !current)} aria-label="Toggle edit mode">
1253
1376
  <Pencil size={16} />
1254
1377
  </button>
@@ -1258,7 +1381,7 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1258
1381
  </button>
1259
1382
  </div>
1260
1383
  </header>
1261
- {(table.objectType === "api-registry" || table.objectType === "data-source") && (
1384
+ {(table.objectType === "api-registry" || table.objectType === "data-source") && sandboxToolFlow !== "draft" && (
1262
1385
  <SourceTestPanel
1263
1386
  status={draft.status}
1264
1387
  testing={testing}
@@ -1267,7 +1390,67 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1267
1390
  onTest={testApiRecord}
1268
1391
  />
1269
1392
  )}
1270
- {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" && (
1271
1454
  <SandboxRunPanel
1272
1455
  status={draft.status}
1273
1456
  sandboxRunning={sandboxRunning}
@@ -1277,8 +1460,27 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1277
1460
  onRun={runSandbox}
1278
1461
  />
1279
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
+ )}
1280
1482
  <div className="dm-record-fields">
1281
- {isSandbox ? (
1483
+ {isApiRegistry && sandboxToolFlow === "draft" ? null : hideRecordFields ? null : isSandbox ? (
1282
1484
  <SandboxRecordFields
1283
1485
  draft={draft}
1284
1486
  setDraft={setDraft}
@@ -1292,7 +1494,8 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1292
1494
  sandboxHistoryMessage={sandboxHistoryMessage}
1293
1495
  loadingSandboxHistory={loadingSandboxHistory}
1294
1496
  onLoadSandboxHistory={loadSandboxHistory}
1295
- onExpandLastResponse={expandLastResponse}
1497
+ onOpenGraphSidecar={openGraphSidecar}
1498
+ onOpenTraceSidecar={openTraceSidecar}
1296
1499
  />
1297
1500
  ) : groupRecordColumns(table.columns || []).map((section) => (
1298
1501
  <DrawerSection key={section.title} title={section.title}>
@@ -1369,8 +1572,34 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
1369
1572
  );
1370
1573
  }
1371
1574
 
1372
- 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();
1373
1601
  const [selectedRow, setSelectedRow] = useState(null);
1602
+ const [initialSidecar, setInitialSidecar] = useState(null);
1374
1603
  const [fieldName, setFieldName] = useState("");
1375
1604
  const [fieldType, setFieldType] = useState("text");
1376
1605
  const [addingField, setAddingField] = useState(false);
@@ -1397,13 +1626,14 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1397
1626
  setSelectMenuOpen(false);
1398
1627
  setPageIndex(0);
1399
1628
  }, [table.id]);
1629
+
1400
1630
  useEffect(() => {
1401
1631
  setFieldName("");
1402
1632
  setFieldType("text");
1403
1633
  setFilterDraft({ fieldId: table.columns[0] || "", operator: "eq", value: "" });
1404
1634
  }, [table.id, table.columns]);
1405
1635
 
1406
- const settings = table.fieldSettings || { hidden: [], order: table.columns, sort: [], filter: null, views: [], activeViewId: "" };
1636
+ const settings = table.fieldSettings || { hidden: [], order: table.columns, sort: [], filter: null };
1407
1637
  const orderedColumns = useMemo(() => mergeColumnOrder(settings.order, table.columns), [settings.order, table.columns]);
1408
1638
  const visibleColumns = useMemo(() => orderedColumns.filter((column) => !settings.hidden.includes(column)), [orderedColumns, settings.hidden]);
1409
1639
  const rowEntries = useMemo(() => {
@@ -1420,10 +1650,6 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1420
1650
  return 0;
1421
1651
  });
1422
1652
  }, [table.rows, settings]);
1423
- const activeView = useMemo(
1424
- () => (settings.views || []).find((view) => view.id === settings.activeViewId) || null,
1425
- [settings.views, settings.activeViewId]
1426
- );
1427
1653
  const selectedRowCount = selectedRows.size;
1428
1654
  const pageCount = Math.max(1, Math.ceil(rowEntries.length / pageSize));
1429
1655
  const safePageIndex = Math.min(pageIndex, pageCount - 1);
@@ -1433,6 +1659,20 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1433
1659
  const pageSelectedCount = pageEntries.filter((entry) => selectedRows.has(entry.originalIndex)).length;
1434
1660
  const allPageSelected = pageEntries.length > 0 && pageSelectedCount === pageEntries.length;
1435
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
+
1436
1676
  useEffect(() => {
1437
1677
  setPageIndex((current) => Math.min(current, pageCount - 1));
1438
1678
  }, [pageCount]);
@@ -1516,6 +1756,15 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1516
1756
  setFilterTarget("");
1517
1757
  }
1518
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
+
1519
1768
  function removeFilter(fieldId) {
1520
1769
  updateSettings((current) => {
1521
1770
  const clauses = (current.filter?.clauses || []).filter((clause) => clause.fieldId !== fieldId);
@@ -1529,27 +1778,7 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1529
1778
  hidden: [],
1530
1779
  order: table.columns,
1531
1780
  sort: [],
1532
- filter: null,
1533
- activeViewId: ""
1534
- }));
1535
- }
1536
-
1537
- function saveCurrentAsNewView() {
1538
- const name = window.prompt("View name");
1539
- if (!name?.trim()) return;
1540
- const viewId = `view_${Date.now().toString(36)}`;
1541
- updateSettings((current) => ({
1542
- ...current,
1543
- activeViewId: viewId,
1544
- views: [...(current.views || []), { id: viewId, name: name.trim(), favorite: false, locked: false, ...snapshotTableViewState(current) }]
1545
- }));
1546
- }
1547
-
1548
- function updateCurrentView() {
1549
- if (!activeView) return;
1550
- updateSettings((current) => ({
1551
- ...current,
1552
- views: (current.views || []).map((view) => view.id === activeView.id ? { ...view, ...snapshotTableViewState(current) } : view)
1781
+ filter: null
1553
1782
  }));
1554
1783
  }
1555
1784
 
@@ -1615,13 +1844,10 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1615
1844
 
1616
1845
  function deleteSelectedRows() {
1617
1846
  if (!selectedRows.size) return;
1618
- if (!confirmDeleteSelection) {
1619
- setConfirmDeleteSelection(true);
1620
- return;
1621
- }
1622
1847
  const rowIndexes = Array.from(selectedRows).sort((a, b) => b - a);
1623
1848
  onSave((config) => rowIndexes.reduce((nextConfig, rowIndex) => deleteTableRow(nextConfig, table, rowIndex), config));
1624
1849
  setSelectedRow(null);
1850
+ setConfirmDeleteSelection(false);
1625
1851
  clearRowSelection();
1626
1852
  }
1627
1853
 
@@ -1670,15 +1896,6 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1670
1896
  </div>
1671
1897
  )}
1672
1898
  </span>
1673
- {activeView ? (
1674
- <button type="button" className="dm-btn-ghost" onClick={updateCurrentView}>
1675
- Update view
1676
- </button>
1677
- ) : (
1678
- <button type="button" className="dm-btn-ghost" onClick={saveCurrentAsNewView}>
1679
- Save as new view
1680
- </button>
1681
- )}
1682
1899
  {table.rows.length > 0 && (
1683
1900
  <button type="button" className="dm-btn-ghost" onClick={() => {
1684
1901
  const blob = new Blob([exportTableAsCsv(table)], { type: "text/csv" });
@@ -1697,8 +1914,8 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1697
1914
  {table.mutable && selectedRowCount > 0 && (
1698
1915
  <>
1699
1916
  <button type="button" className="dm-btn-ghost" disabled={saving} onClick={clearRowSelection}>Cancel selection</button>
1700
- <button type="button" className="dm-btn-danger-sm" disabled={saving} onClick={deleteSelectedRows}>
1701
- {confirmDeleteSelection ? `Confirm delete ${selectedRowCount}` : "Delete"}
1917
+ <button type="button" className="dm-btn-danger-sm" disabled={saving} onClick={() => setConfirmDeleteSelection(true)}>
1918
+ <Trash2 size={13} />Delete
1702
1919
  </button>
1703
1920
  </>
1704
1921
  )}
@@ -1860,6 +2077,26 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1860
2077
  />
1861
2078
  ) : column.toLowerCase() === "status" ? (
1862
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>
1863
2100
  ) : (
1864
2101
  <span className={row?.[column] ? "" : "dm-cell-empty"}>
1865
2102
  {formatCellValue(row?.[column], column) || "—"}
@@ -1904,9 +2141,36 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1904
2141
  rowIndex={selectedEntry?.originalIndex ?? null}
1905
2142
  row={selectedRecord}
1906
2143
  saving={saving}
1907
- onClose={() => setSelectedRow(null)}
2144
+ onClose={() => { setSelectedRow(null); setInitialSidecar(null); }}
1908
2145
  onSave={onSave}
2146
+ onFocusSandboxRow={onFocusSandboxRow}
2147
+ initialSidecar={initialSidecar}
2148
+ onClearInitialSidecar={() => setInitialSidecar(null)}
1909
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
+ )}
1910
2174
  </div>
1911
2175
  );
1912
2176
  }
@@ -2184,6 +2448,7 @@ export default function DataModelShell() {
2184
2448
  const [helperInitialPrompt, setHelperInitialPrompt] = useState("");
2185
2449
  const [helperInitialThread, setHelperInitialThread] = useState(null);
2186
2450
  const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
2451
+ const [focusSandboxRowName, setFocusSandboxRowName] = useState(null);
2187
2452
  const pendingPatchRef = useRef({});
2188
2453
  const saveTimerRef = useRef(null);
2189
2454
 
@@ -2276,10 +2541,43 @@ export default function DataModelShell() {
2276
2541
 
2277
2542
  const selectedTable = tables.find((t) => t.source === selectedSource) || tables[0] || null;
2278
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
+
2279
2557
  useEffect(() => {
2280
2558
  if (!selectedSource && tables[0]) setSelectedSource(tables[0].source);
2281
2559
  }, [selectedSource, tables]);
2282
2560
 
2561
+ useEffect(() => {
2562
+ const objectParam = searchParams?.get("object");
2563
+ if (!objectParam || !tables.length) return;
2564
+ const target = tables.find((table) => (
2565
+ table.objectId === objectParam
2566
+ || table.id === objectParam
2567
+ || table.source === objectParam
2568
+ || table.label === objectParam
2569
+ ));
2570
+ if (target && target.source !== selectedSource) {
2571
+ setSelectedSource(target.source);
2572
+ }
2573
+ }, [searchParams, selectedSource, tables]);
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
+
2283
2581
  // Flush any accumulated patch keys to the server. Called by the debounce
2284
2582
  // timer and on visibilitychange/beforeunload so no local edit is lost.
2285
2583
  const flushPendingPatch = useCallback(async () => {
@@ -2439,7 +2737,7 @@ export default function DataModelShell() {
2439
2737
  },
2440
2738
  {
2441
2739
  id: "helper.repair", group: "Ask helper", label: "Ask helper — repair workspace",
2442
- run: () => openHelperWith("repair", "Inspect this workspace for missing references, broken bindings, or incomplete views. Propose the smallest fix for each issue.")
2740
+ run: () => openHelperWith("repair", "Inspect this workspace for missing references, broken bindings, or incomplete object configuration. Propose the smallest fix for each issue.")
2443
2741
  },
2444
2742
  {
2445
2743
  id: "helper.explain", group: "Ask helper", label: "Ask helper — explain this workspace",
@@ -2450,7 +2748,7 @@ export default function DataModelShell() {
2450
2748
  run: () => setAddOpen(true)
2451
2749
  },
2452
2750
  {
2453
- id: "nav.dashboards", group: "Navigation", label: "Go to Dashboards",
2751
+ id: "nav.builder", group: "Navigation", label: "Go to Builder",
2454
2752
  run: () => { window.location.href = "/"; }
2455
2753
  },
2456
2754
  {
@@ -2595,7 +2893,17 @@ export default function DataModelShell() {
2595
2893
  selectedTable && (
2596
2894
  <section className="dm-detail-v2 dm-detail-v3">
2597
2895
  <SourceValidationBanner table={selectedTable} />
2598
- <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
+ />
2599
2907
  </section>
2600
2908
  )
2601
2909
  )}