@growthub/cli 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +99 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +70 -9
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +1 -1
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +61 -35
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +18 -4
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +264 -22
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +70 -85
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +129 -3
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +48 -9
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +139 -4
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +246 -4
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +6 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +411 -1
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +23 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +551 -0
  23. package/package.json +1 -1
@@ -43,6 +43,17 @@ import {
43
43
  } from "@/lib/workspace-helper-apply";
44
44
  import { RESOLVER_PROPOSAL_TYPE, buildResolverProposal, validateResolverProposal } from "@/lib/workspace-resolver-proposal";
45
45
  import { writeResolverProposalFile } from "@/lib/server-resolver-write";
46
+ import {
47
+ SWARM_PROPOSAL_TYPES,
48
+ SWARM_RUN_RESUME_PROPOSAL_TYPE,
49
+ SWARM_WORKFLOWS_OBJECT_ID,
50
+ validateSwarmRunProposal,
51
+ buildSandboxRowFromSwarmProposal,
52
+ deriveHelperWidgetCausationState,
53
+ upsertSwarmRunRow,
54
+ findSwarmRunRows,
55
+ summarizeSwarmRunProposal,
56
+ } from "@/lib/workspace-swarm-proposal";
46
57
 
47
58
  const HELPER_APPLY_SOURCE_KEY = "helper:apply:receipts";
48
59
 
@@ -133,6 +144,65 @@ function normalizeApplyProposal(proposal, config, context = {}) {
133
144
  return normalizeDataModelObjectProposal(normalizeResolverProposal(proposal, config), config, context.integrationId);
134
145
  }
135
146
 
147
+ /**
148
+ * Swarm lane (SWARM_RUN_CONTRACT_V1) — normalize a validated swarm proposal
149
+ * into the next workspace config plus an artifact target the sidecar cockpit
150
+ * can open. The model's intent payload is reduced through
151
+ * buildDefaultAgentSwarmGraph inside buildSandboxRowFromSwarmProposal — final
152
+ * graph JSON from the model is never trusted verbatim. Nothing executes here;
153
+ * runs stay behind POST /api/workspace/sandbox-run.
154
+ *
155
+ * Returns { ok, config, artifact, summary, error? }.
156
+ */
157
+ function normalizeSwarmRunProposal(proposal, workspaceConfig) {
158
+ const validation = validateSwarmRunProposal(proposal);
159
+ if (!validation.ok) {
160
+ return { ok: false, config: workspaceConfig, artifact: null, summary: "", error: validation.error };
161
+ }
162
+
163
+ if (proposal.type === SWARM_RUN_RESUME_PROPOSAL_TYPE) {
164
+ const name = String(proposal.payload?.name || "").trim();
165
+ const matches = findSwarmRunRows(workspaceConfig, { name });
166
+ if (matches.length === 0) {
167
+ return {
168
+ ok: false,
169
+ config: workspaceConfig,
170
+ artifact: null,
171
+ summary: "",
172
+ error: `no governed swarm workflow named "${name}" to resume`,
173
+ };
174
+ }
175
+ // Resume is a governed pointer, not an execution: the receipt records the
176
+ // intent and the cockpit re-launches through sandbox-run.
177
+ return {
178
+ ok: true,
179
+ config: workspaceConfig,
180
+ artifact: { surface: "swarm-run", objectId: matches[0].objectId, name: matches[0].row.Name },
181
+ summary: summarizeSwarmRunProposal(proposal),
182
+ };
183
+ }
184
+
185
+ const helperState = deriveHelperWidgetCausationState(workspaceConfig);
186
+ if (!helperState.ready) {
187
+ return {
188
+ ok: false,
189
+ config: workspaceConfig,
190
+ artifact: null,
191
+ summary: "",
192
+ error: helperState.guidance,
193
+ };
194
+ }
195
+
196
+ const row = buildSandboxRowFromSwarmProposal(workspaceConfig, proposal);
197
+ const nextConfig = upsertSwarmRunRow(workspaceConfig, row);
198
+ return {
199
+ ok: true,
200
+ config: nextConfig,
201
+ artifact: { surface: "swarm-run", objectId: SWARM_WORKFLOWS_OBJECT_ID, name: row.Name },
202
+ summary: summarizeSwarmRunProposal(proposal),
203
+ };
204
+ }
205
+
136
206
  async function POST(request) {
137
207
  let body;
138
208
  try {
@@ -177,7 +247,10 @@ async function POST(request) {
177
247
  normalizeApplyProposal(proposal, currentConfig, { integrationId: fallbackIntegrationId })
178
248
  );
179
249
  const resolverProposals = normalizedProposals.filter((p) => p?.type === RESOLVER_PROPOSAL_TYPE);
180
- const configProposals = normalizedProposals.filter((p) => p?.type !== RESOLVER_PROPOSAL_TYPE);
250
+ const swarmProposals = normalizedProposals.filter((p) => SWARM_PROPOSAL_TYPES.includes(p?.type));
251
+ const configProposals = normalizedProposals.filter(
252
+ (p) => p?.type !== RESOLVER_PROPOSAL_TYPE && !SWARM_PROPOSAL_TYPES.includes(p?.type)
253
+ );
181
254
 
182
255
  for (const proposal of resolverProposals) {
183
256
  const validation = validateResolverProposal(proposal);
@@ -201,6 +274,23 @@ async function POST(request) {
201
274
  }
202
275
  }
203
276
 
277
+ // Swarm lane — governed sandbox-environment rows in the EXISTING dataModel
278
+ // patch field. Apply creates/updates the row; execution stays behind
279
+ // POST /api/workspace/sandbox-run, launched explicitly from the cockpit.
280
+ for (const proposal of swarmProposals) {
281
+ const result = normalizeSwarmRunProposal(proposal, workingConfig);
282
+ if (!result.ok) {
283
+ skipped.push({ proposal, reason: result.error || "invalid swarm proposal" });
284
+ continue;
285
+ }
286
+ workingConfig = result.config;
287
+ applied.push({
288
+ ...buildApplyReceipt({ ...proposal, affectedField: "dataModel" }, appliedAt, reviewedBy, sessionId),
289
+ artifact: result.artifact,
290
+ summary: result.summary,
291
+ });
292
+ }
293
+
204
294
  for (const proposal of configProposals) {
205
295
  if (
206
296
  !proposal ||
@@ -238,7 +328,11 @@ async function POST(request) {
238
328
  // refreshes in the same atomic write as the proposed mutations).
239
329
  // resolver.create writes a server file (affectedField "server-file"), so it
240
330
  // must NOT contribute a field to the config PATCH — exclude it here.
241
- const mutatingApplied = applied.filter((r) => r.type !== "explain.object" && r.affectedField !== "server-file");
331
+ // swarm.run.resume is a governed pointer (no config mutation) exclude it
332
+ // alongside explain.object so it never forces a config write on its own.
333
+ const mutatingApplied = applied.filter(
334
+ (r) => r.type !== "explain.object" && r.type !== SWARM_RUN_RESUME_PROPOSAL_TYPE && r.affectedField !== "server-file"
335
+ );
242
336
 
243
337
  // Upsert the thread row so audit history reflects this apply turn even
244
338
  // when nothing mutated (all skipped / explain-only) and even when the
@@ -308,6 +402,9 @@ async function POST(request) {
308
402
  "dataModel.row.add": "create_object",
309
403
  "repair.binding": "repair",
310
404
  "explain.object": "explain",
405
+ "swarm.run.propose": "swarm",
406
+ "swarm.workflow.save": "swarm",
407
+ "swarm.run.resume": "swarm",
311
408
  };
312
409
  const proposalIntent = firstProposal?.type ? TYPE_TO_INTENT_HINT[firstProposal.type] : null;
313
410
  const safeIntent = existingRow.intent || proposalIntent || "explain";
@@ -70,6 +70,7 @@ const VALID_INTENTS = [
70
70
  "edit_view",
71
71
  "repair",
72
72
  "explain",
73
+ "swarm",
73
74
  ];
74
75
 
75
76
  const HELPER_SOURCE_KEY_PREFIX = "helper";
@@ -338,6 +338,8 @@ async function runServerlessScheduler({
338
338
  function buildRunResponse({
339
339
  runId,
340
340
  ranAt,
341
+ objectId,
342
+ name,
341
343
  runLocality,
342
344
  schedulerRegistryId,
343
345
  runtime,
@@ -359,6 +361,10 @@ function buildRunResponse({
359
361
  const base = {
360
362
  runId,
361
363
  ranAt,
364
+ // Identity travels with the persisted record so run-console consumers
365
+ // (lineage, swarm projection title) don't depend on the row context.
366
+ objectId: objectId ? String(objectId).trim() : undefined,
367
+ name: name ? String(name).trim() : undefined,
362
368
  runLocality,
363
369
  schedulerRegistryId: schedulerRegistryId ? String(schedulerRegistryId).trim() : null,
364
370
  runtime,
@@ -439,14 +445,7 @@ async function GET(request) {
439
445
  });
440
446
  }
441
447
 
442
- async function POST(request) {
443
- let body;
444
- try {
445
- body = await request.json();
446
- } catch {
447
- return NextResponse.json({ ok: false, error: "invalid JSON body" }, { status: 400 });
448
- }
449
-
448
+ async function executeSandboxRun(body, { emit } = {}) {
450
449
  const objectId = typeof body?.objectId === "string" ? body.objectId.trim() : "";
451
450
  const name = typeof body?.name === "string" ? body.name.trim() : "";
452
451
  const useDraft = body?.useDraft === true;
@@ -580,7 +579,8 @@ async function POST(request) {
580
579
  instructions,
581
580
  command,
582
581
  timeoutMs,
583
- sandboxName: rowForRun.Name || name
582
+ sandboxName: rowForRun.Name || name,
583
+ onEvent: emit
584
584
  }
585
585
  });
586
586
  if (graphResult !== null) {
@@ -665,6 +665,8 @@ async function POST(request) {
665
665
  const response = buildRunResponse({
666
666
  runId,
667
667
  ranAt,
668
+ objectId,
669
+ name: rowForRun.Name || name,
668
670
  runLocality,
669
671
  schedulerRegistryId: runLocality === "serverless" ? schedulerRegistryId : null,
670
672
  runtime,
@@ -754,4 +756,63 @@ async function POST(request) {
754
756
  });
755
757
  }
756
758
 
759
+ async function POST(request) {
760
+ const accept = request.headers.get("accept") || "";
761
+ let body;
762
+ try {
763
+ body = await request.json();
764
+ } catch {
765
+ return NextResponse.json({ ok: false, error: "invalid JSON body" }, { status: 400 });
766
+ }
767
+
768
+ const wantsStream = body?.stream === true || accept.includes("application/x-ndjson");
769
+ if (!wantsStream) {
770
+ return executeSandboxRun(body);
771
+ }
772
+
773
+ const encoder = new TextEncoder();
774
+ const stream = new ReadableStream({
775
+ start(controller) {
776
+ const emit = (event) => {
777
+ controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`));
778
+ };
779
+ emit({
780
+ kind: "growthub-sandbox-run-delta-v1",
781
+ type: "sandbox-run.accepted",
782
+ emittedAt: new Date().toISOString(),
783
+ objectId: typeof body?.objectId === "string" ? body.objectId.trim() : "",
784
+ name: typeof body?.name === "string" ? body.name.trim() : ""
785
+ });
786
+ executeSandboxRun(body, { emit })
787
+ .then(async (response) => {
788
+ const finalPayload = await response.json().catch(() => ({ ok: false, error: "stream final payload unreadable" }));
789
+ emit({
790
+ kind: "growthub-sandbox-run-delta-v1",
791
+ type: "sandbox-run.final",
792
+ emittedAt: new Date().toISOString(),
793
+ status: response.status,
794
+ payload: finalPayload
795
+ });
796
+ })
797
+ .catch((error) => {
798
+ emit({
799
+ kind: "growthub-sandbox-run-delta-v1",
800
+ type: "sandbox-run.final",
801
+ emittedAt: new Date().toISOString(),
802
+ status: 500,
803
+ payload: { ok: false, error: error?.message || "sandbox run failed" }
804
+ });
805
+ })
806
+ .finally(() => controller.close());
807
+ }
808
+ });
809
+
810
+ return new Response(stream, {
811
+ headers: {
812
+ "content-type": "application/x-ndjson; charset=utf-8",
813
+ "cache-control": "no-store"
814
+ }
815
+ });
816
+ }
817
+
757
818
  export { GET, POST };
@@ -14,7 +14,7 @@ const HELPER_AGENT_CHOICES = [
14
14
  { id: "gemini_local", label: "Gemini CLI (local)", body: "Uses Gemini CLI." },
15
15
  { id: "opencode_local", label: "OpenCode (local)", body: "Uses OpenCode on this machine." },
16
16
  { id: "pi_local", label: "Pi (local)", body: "Uses Pi on this machine." },
17
- { id: "qwen_code_local", label: "Qwen Code (local)", body: "Uses Qwen Code on this machine." },
17
+ { id: "qwen_local", label: "Qwen Code (local)", body: "Uses Qwen Code on this machine." },
18
18
  ];
19
19
 
20
20
  const HELPER_EXECUTION_ADAPTERS = [
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useMemo } from "react";
4
- import { Plus, Trash2 } from "lucide-react";
4
+ import { Check, Plus, Trash2 } from "lucide-react";
5
5
  import { HOST_AUTH_CATALOG } from "@/lib/sandbox-agent-host-catalog";
6
6
 
7
7
  function getHostOptions() {
@@ -11,31 +11,63 @@ function getHostOptions() {
11
11
  }));
12
12
  }
13
13
 
14
- function patchOrchestrator(graph, patch) {
14
+ function nodeSandboxRecordRef(objectId, rowName, nodeId) {
15
+ return {
16
+ objectId: String(objectId || "").trim(),
17
+ rowName: String(rowName || "").trim(),
18
+ nodeId: String(nodeId || "").trim()
19
+ };
20
+ }
21
+
22
+ function withRecordRef(patch, objectId, rowName, nodeId) {
23
+ return {
24
+ ...patch,
25
+ sandboxRecordRef: nodeSandboxRecordRef(objectId, rowName, nodeId)
26
+ };
27
+ }
28
+
29
+ function WorkflowCheckbox({ checked, disabled, onChange, children, title }) {
30
+ return (
31
+ <label className="dm-orchestration-config__field dm-orchestration-config__field-inline dm-workflow-check" title={title}>
32
+ <input
33
+ type="checkbox"
34
+ checked={checked}
35
+ disabled={disabled}
36
+ onChange={(event) => onChange?.(event.target.checked)}
37
+ />
38
+ <span className="dm-workflow-check__box" aria-hidden="true">
39
+ {checked ? <Check size={13} strokeWidth={2.4} /> : null}
40
+ </span>
41
+ <span>{children}</span>
42
+ </label>
43
+ );
44
+ }
45
+
46
+ function patchOrchestrator(graph, patch, objectId, rowName) {
15
47
  const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
16
48
  return {
17
49
  ...graph,
18
50
  nodes: nodes.map((node) =>
19
51
  node?.type === "thinAdapter"
20
- ? { ...node, config: { ...(node.config || {}), ...patch } }
52
+ ? { ...node, config: { ...(node.config || {}), ...withRecordRef(patch, objectId, rowName, node.id) } }
21
53
  : node
22
54
  )
23
55
  };
24
56
  }
25
57
 
26
- function patchSynthesis(graph, patch) {
58
+ function patchSynthesis(graph, patch, objectId, rowName) {
27
59
  const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
28
60
  return {
29
61
  ...graph,
30
62
  nodes: nodes.map((node) =>
31
63
  node?.type === "tool-result"
32
- ? { ...node, config: { ...(node.config || {}), ...patch } }
64
+ ? { ...node, config: { ...(node.config || {}), ...withRecordRef(patch, objectId, rowName, node.id) } }
33
65
  : node
34
66
  )
35
67
  };
36
68
  }
37
69
 
38
- function patchSubagent(graph, nodeId, patch) {
70
+ function patchSubagent(graph, nodeId, patch, objectId, rowName) {
39
71
  const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
40
72
  return {
41
73
  ...graph,
@@ -44,7 +76,7 @@ function patchSubagent(graph, nodeId, patch) {
44
76
  ? {
45
77
  ...node,
46
78
  label: patch.role != null ? String(patch.role) : node.label,
47
- config: { ...(node.config || {}), ...patch }
79
+ config: { ...(node.config || {}), ...withRecordRef(patch, objectId, rowName, node.id) }
48
80
  }
49
81
  : node
50
82
  )
@@ -107,7 +139,7 @@ function patchSwarmConfig(graph, patch) {
107
139
  return { ...graph, swarm: { ...base, ...patch } };
108
140
  }
109
141
 
110
- export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
142
+ export function AgentSwarmPanel({ graph, objectId, rowName, onGraphChange, disabled }) {
111
143
  const hostOptions = useMemo(getHostOptions, []);
112
144
  if (!graph || typeof graph !== "object") return null;
113
145
 
@@ -133,7 +165,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
133
165
  rows={3}
134
166
  value={orchestrator?.config?.prompt || ""}
135
167
  disabled={disabled || !orchestrator}
136
- onChange={(e) => patchGraph((g) => patchOrchestrator(g, { prompt: e.target.value }))}
168
+ onChange={(e) => patchGraph((g) => patchOrchestrator(g, { prompt: e.target.value }, objectId, rowName))}
137
169
  />
138
170
  </label>
139
171
  </div>
@@ -163,7 +195,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
163
195
  placeholder="Role"
164
196
  value={cfg.role || node.label || ""}
165
197
  disabled={disabled}
166
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { role: e.target.value }))}
198
+ onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { role: e.target.value }, objectId, rowName))}
167
199
  />
168
200
  <button
169
201
  type="button"
@@ -181,7 +213,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
181
213
  placeholder="One-sentence charter"
182
214
  value={cfg.description || ""}
183
215
  disabled={disabled}
184
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { description: e.target.value }))}
216
+ onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { description: e.target.value }, objectId, rowName))}
185
217
  />
186
218
  </label>
187
219
  <label className="dm-orchestration-config__field">
@@ -190,7 +222,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
190
222
  rows={2}
191
223
  value={cfg.taskPrompt || ""}
192
224
  disabled={disabled}
193
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { taskPrompt: e.target.value }))}
225
+ onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { taskPrompt: e.target.value }, objectId, rowName))}
194
226
  />
195
227
  </label>
196
228
  <label className="dm-orchestration-config__field">
@@ -201,7 +233,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
201
233
  disabled={disabled}
202
234
  onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, {
203
235
  tools: e.target.value.split(",").map((t) => t.trim()).filter(Boolean)
204
- }))}
236
+ }, objectId, rowName))}
205
237
  />
206
238
  </label>
207
239
  <label className="dm-orchestration-config__field">
@@ -212,7 +244,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
212
244
  placeholder="0 = inherit"
213
245
  value={cfg.maxTokens || 0}
214
246
  disabled={disabled}
215
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { maxTokens: Math.max(0, Number(e.target.value) || 0) }))}
247
+ onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { maxTokens: Math.max(0, Number(e.target.value) || 0) }, objectId, rowName))}
216
248
  />
217
249
  </label>
218
250
  <label className="dm-orchestration-config__field">
@@ -220,7 +252,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
220
252
  <select
221
253
  value={cfg.agentHost || ""}
222
254
  disabled={disabled}
223
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { agentHost: e.target.value }))}
255
+ onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { agentHost: e.target.value }, objectId, rowName))}
224
256
  >
225
257
  <option value="">Inherit</option>
226
258
  {hostOptions.map((opt) => (
@@ -228,27 +260,21 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
228
260
  ))}
229
261
  </select>
230
262
  </label>
231
- <label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
232
- <input
233
- type="checkbox"
234
- checked={cfg.required !== false}
235
- disabled={disabled}
236
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { required: e.target.checked }))}
237
- />
238
- <span>Required</span>
239
- </label>
240
- <label
241
- className="dm-orchestration-config__field dm-orchestration-config__field-inline"
263
+ <WorkflowCheckbox
264
+ checked={cfg.required !== false}
265
+ disabled={disabled}
266
+ onChange={(checked) => patchGraph((g) => patchSubagent(g, node.id, { required: checked }, objectId, rowName))}
267
+ >
268
+ Required
269
+ </WorkflowCheckbox>
270
+ <WorkflowCheckbox
271
+ checked={cfg.networkAccess === true}
272
+ disabled={disabled}
242
273
  title="Network is granted only when both this and the row's networkAllow are on."
274
+ onChange={(checked) => patchGraph((g) => patchSubagent(g, node.id, { networkAccess: checked }, objectId, rowName))}
243
275
  >
244
- <input
245
- type="checkbox"
246
- checked={cfg.networkAccess === true}
247
- disabled={disabled}
248
- onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { networkAccess: e.target.checked }))}
249
- />
250
- <span>Network</span>
251
- </label>
276
+ Network
277
+ </WorkflowCheckbox>
252
278
  </div>
253
279
  );
254
280
  })}
@@ -315,7 +341,7 @@ export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
315
341
  rows={2}
316
342
  value={synthesis?.config?.outcomePrompt || ""}
317
343
  disabled={disabled}
318
- onChange={(e) => patchGraph((g) => patchSynthesis(g, { outcomePrompt: e.target.value }))}
344
+ onChange={(e) => patchGraph((g) => patchSynthesis(g, { outcomePrompt: e.target.value }, objectId, rowName))}
319
345
  />
320
346
  </label>
321
347
  </div>
@@ -42,6 +42,7 @@ import {
42
42
  Trash2,
43
43
  Type,
44
44
  Unlock,
45
+ Upload,
45
46
  Users,
46
47
  X,
47
48
  Zap,
@@ -1843,7 +1844,9 @@ function DataModelRecordDrawer({
1843
1844
  <header className="dm-record-drawer-head">
1844
1845
  <div>
1845
1846
  <p>Record</p>
1846
- <h2>{draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}</h2>
1847
+ <h2 title={draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}>
1848
+ {draft.Name || draft.integrationId || draft.id || `Row ${rowIndex + 1}`}
1849
+ </h2>
1847
1850
  </div>
1848
1851
  <div className="dm-record-drawer-actions">
1849
1852
  {isSandbox && sidecarMode !== "graph" && sidecarMode !== "trace" && (
@@ -1854,7 +1857,7 @@ function DataModelRecordDrawer({
1854
1857
  onClick={runSandbox}
1855
1858
  >
1856
1859
  <Play size={13} aria-hidden />
1857
- {sandboxRunning ? "Running…" : "Run sandbox"}
1860
+ {sandboxRunning ? "Running…" : "Execute"}
1858
1861
  </button>
1859
1862
  )}
1860
1863
  {!isSandbox && sandboxToolFlow !== "draft" && (
@@ -2457,9 +2460,9 @@ function DataModelTableSurface({
2457
2460
  const a = document.createElement("a");
2458
2461
  a.href = url; a.download = `${table.source.replace(/\s+/g, "-").toLowerCase()}.csv`;
2459
2462
  a.click(); URL.revokeObjectURL(url);
2460
- }}>Export CSV</button>
2463
+ }}><Download size={13} />Export CSV</button>
2461
2464
  )}
2462
- {table.mutable && <button type="button" className="dm-btn-ghost" onClick={() => setCsvOpen((open) => !open)}>Import CSV</button>}
2465
+ {table.mutable && <button type="button" className="dm-btn-ghost" onClick={() => setCsvOpen((open) => !open)}><Upload size={13} />Import CSV</button>}
2463
2466
  {table.mutable && (
2464
2467
  <button type="button" className="dm-btn-primary-sm" disabled={saving} onClick={() => onSave((config) => addTableRow(config, table))}>
2465
2468
  <Plus size={13} />Add record
@@ -3544,6 +3547,17 @@ export default function DataModelShell() {
3544
3547
  router.push(`/?dashboard=${encodeURIComponent(target.dashboardId)}`);
3545
3548
  }
3546
3549
  }}
3550
+ onOpenSwarmWorkflow={(target) => {
3551
+ const objectId = String(target?.objectId || "").trim();
3552
+ const rowName = String(target?.name || "").trim();
3553
+ if (!objectId || !rowName) return;
3554
+ const params = new URLSearchParams({
3555
+ object: objectId,
3556
+ row: rowName,
3557
+ field: "orchestrationGraph"
3558
+ });
3559
+ router.push(`/workflows?${params.toString()}`);
3560
+ }}
3547
3561
  onApplied={(updatedConfig) => {
3548
3562
  // Anchor the user on the most recently created/updated Data Model
3549
3563
  // object so a helper-driven object.create lands on the surface