@growthub/cli 0.14.0 → 0.14.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 (40) 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-agent-auth/login/route.js +3 -2
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +84 -10
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +2 -2
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +107 -34
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +72 -15
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +264 -22
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +179 -117
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +136 -3
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +61 -13
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +224 -11
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +254 -4
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +10 -2
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -1
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +1 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +23 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +554 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  40. 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";
@@ -15,7 +15,7 @@
15
15
  * NEVER written to `growthub.config.json`.
16
16
  *
17
17
  * Request body:
18
- * { objectId: string, name: string }
18
+ * { objectId: string, name: string, agentHost?: string }
19
19
  *
20
20
  * Response:
21
21
  * {
@@ -49,6 +49,7 @@ async function POST(request) {
49
49
 
50
50
  const objectId = typeof body?.objectId === "string" ? body.objectId.trim() : "";
51
51
  const name = typeof body?.name === "string" ? body.name.trim() : "";
52
+ const agentHost = typeof body?.agentHost === "string" ? body.agentHost.trim() : "";
52
53
  if (!objectId || !name) {
53
54
  return NextResponse.json(
54
55
  { ok: false, error: "objectId and name are required" },
@@ -57,7 +58,7 @@ async function POST(request) {
57
58
  }
58
59
 
59
60
  try {
60
- const result = await runAgentLogin({ objectId, name });
61
+ const result = await runAgentLogin({ objectId, name, agentHost });
61
62
  return NextResponse.json(result);
62
63
  } catch (error) {
63
64
  return NextResponse.json(
@@ -10,7 +10,7 @@
10
10
  * host's `notes` string.
11
11
  *
12
12
  * Request body:
13
- * { objectId: string, name: string }
13
+ * { objectId: string, name: string, agentHost?: string }
14
14
  *
15
15
  * Response:
16
16
  * {
@@ -42,6 +42,7 @@ async function POST(request) {
42
42
 
43
43
  const objectId = typeof body?.objectId === "string" ? body.objectId.trim() : "";
44
44
  const name = typeof body?.name === "string" ? body.name.trim() : "";
45
+ const agentHost = typeof body?.agentHost === "string" ? body.agentHost.trim() : "";
45
46
  if (!objectId || !name) {
46
47
  return NextResponse.json(
47
48
  { ok: false, error: "objectId and name are required" },
@@ -50,7 +51,7 @@ async function POST(request) {
50
51
  }
51
52
 
52
53
  try {
53
- const result = await runAgentLogout({ objectId, name });
54
+ const result = await runAgentLogout({ objectId, name, agentHost });
54
55
  return NextResponse.json(result);
55
56
  } catch (error) {
56
57
  return NextResponse.json(
@@ -20,7 +20,7 @@
20
20
  * load.
21
21
  *
22
22
  * Request body:
23
- * { objectId: string, name: string }
23
+ * { objectId: string, name: string, agentHost?: string }
24
24
  *
25
25
  * Response (success):
26
26
  * {
@@ -52,6 +52,7 @@ async function POST(request) {
52
52
 
53
53
  const objectId = typeof body?.objectId === "string" ? body.objectId.trim() : "";
54
54
  const name = typeof body?.name === "string" ? body.name.trim() : "";
55
+ const agentHost = typeof body?.agentHost === "string" ? body.agentHost.trim() : "";
55
56
  if (!objectId || !name) {
56
57
  return NextResponse.json(
57
58
  { ok: false, error: "objectId and name are required" },
@@ -60,7 +61,7 @@ async function POST(request) {
60
61
  }
61
62
 
62
63
  try {
63
- const result = await checkAgentStatus({ objectId, name });
64
+ const result = await checkAgentStatus({ objectId, name, agentHost });
64
65
  return NextResponse.json(result);
65
66
  } catch (error) {
66
67
  return NextResponse.json(
@@ -43,6 +43,7 @@
43
43
  * envRefsMissing: string[],
44
44
  * networkAllow: boolean,
45
45
  * allowList: string[],
46
+ * browserAccess: boolean, // first-class browser capability (implies networkAllow)
46
47
  * adapterMeta?: Record<string, unknown>
47
48
  * }
48
49
  * }
@@ -173,6 +174,7 @@ async function runServerlessScheduler({
173
174
  timeoutMs,
174
175
  networkAllow,
175
176
  allowList,
177
+ browserAccess,
176
178
  envRefSlugs,
177
179
  envRefsResolved,
178
180
  envRefsMissing
@@ -251,6 +253,7 @@ async function runServerlessScheduler({
251
253
  timeoutMs,
252
254
  networkAllow,
253
255
  allowList,
256
+ browserAccess,
254
257
  envRefSlugs,
255
258
  envRefsResolved,
256
259
  envRefsMissing
@@ -338,6 +341,8 @@ async function runServerlessScheduler({
338
341
  function buildRunResponse({
339
342
  runId,
340
343
  ranAt,
344
+ objectId,
345
+ name,
341
346
  runLocality,
342
347
  schedulerRegistryId,
343
348
  runtime,
@@ -351,6 +356,7 @@ function buildRunResponse({
351
356
  envRefsMissing,
352
357
  networkAllow,
353
358
  allowList,
359
+ browserAccess,
354
360
  result,
355
361
  timeoutMs,
356
362
  row,
@@ -359,6 +365,10 @@ function buildRunResponse({
359
365
  const base = {
360
366
  runId,
361
367
  ranAt,
368
+ // Identity travels with the persisted record so run-console consumers
369
+ // (lineage, swarm projection title) don't depend on the row context.
370
+ objectId: objectId ? String(objectId).trim() : undefined,
371
+ name: name ? String(name).trim() : undefined,
362
372
  runLocality,
363
373
  schedulerRegistryId: schedulerRegistryId ? String(schedulerRegistryId).trim() : null,
364
374
  runtime,
@@ -378,6 +388,7 @@ function buildRunResponse({
378
388
  envRefsMissing,
379
389
  networkAllow,
380
390
  allowList,
391
+ browserAccess,
381
392
  adapterMeta: result.adapterMeta || null
382
393
  };
383
394
  if (row && (row.resolverTemplateId || row.connectorKind || row.executionLane)) {
@@ -439,14 +450,7 @@ async function GET(request) {
439
450
  });
440
451
  }
441
452
 
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
-
453
+ async function executeSandboxRun(body, { emit } = {}) {
450
454
  const objectId = typeof body?.objectId === "string" ? body.objectId.trim() : "";
451
455
  const name = typeof body?.name === "string" ? body.name.trim() : "";
452
456
  const useDraft = body?.useDraft === true;
@@ -498,7 +502,11 @@ async function POST(request) {
498
502
  let adapterId = (typeof rowForRun.adapter === "string" && rowForRun.adapter.trim()) ? rowForRun.adapter.trim() : DEFAULT_SANDBOX_ADAPTER;
499
503
  const agentHost = typeof rowForRun.agentHost === "string" ? rowForRun.agentHost.trim() : "";
500
504
  const schedulerRegistryId = typeof rowForRun.schedulerRegistryId === "string" ? rowForRun.schedulerRegistryId.trim() : "";
501
- const networkAllow = coerceBoolean(rowForRun.networkAllow);
505
+ const browserAccess = coerceBoolean(rowForRun.browserAccess);
506
+ // Browser access implies outbound network — the same deterministic
507
+ // normalization the sidecar toggle applies, enforced server-side so
508
+ // rows patched via the API behave identically to rows saved in the UI.
509
+ const networkAllow = coerceBoolean(rowForRun.networkAllow) || browserAccess;
502
510
  const allowList = parseSandboxAllowList(rowForRun.allowList);
503
511
  const envRefSlugs = parseSandboxEnvRefs(rowForRun.envRefs);
504
512
  const command = typeof rowForRun.command === "string" ? rowForRun.command : "";
@@ -577,10 +585,12 @@ async function POST(request) {
577
585
  envRefsResolved,
578
586
  networkAllow,
579
587
  allowList,
588
+ browserAccess,
580
589
  instructions,
581
590
  command,
582
591
  timeoutMs,
583
- sandboxName: rowForRun.Name || name
592
+ sandboxName: rowForRun.Name || name,
593
+ onEvent: emit
584
594
  }
585
595
  });
586
596
  if (graphResult !== null) {
@@ -607,6 +617,7 @@ async function POST(request) {
607
617
  timeoutMs,
608
618
  networkAllow,
609
619
  allowList,
620
+ browserAccess,
610
621
  envRefSlugs,
611
622
  envRefsResolved,
612
623
  envRefsMissing
@@ -640,6 +651,7 @@ async function POST(request) {
640
651
  timeoutMs,
641
652
  networkAllow,
642
653
  allowList,
654
+ browserAccess,
643
655
  env,
644
656
  envRefSlugs,
645
657
  envRefsMissing,
@@ -665,6 +677,8 @@ async function POST(request) {
665
677
  const response = buildRunResponse({
666
678
  runId,
667
679
  ranAt,
680
+ objectId,
681
+ name: rowForRun.Name || name,
668
682
  runLocality,
669
683
  schedulerRegistryId: runLocality === "serverless" ? schedulerRegistryId : null,
670
684
  runtime,
@@ -678,6 +692,7 @@ async function POST(request) {
678
692
  envRefsMissing,
679
693
  networkAllow,
680
694
  allowList,
695
+ browserAccess,
681
696
  result,
682
697
  timeoutMs,
683
698
  row: rowForRun,
@@ -754,4 +769,63 @@ async function POST(request) {
754
769
  });
755
770
  }
756
771
 
772
+ async function POST(request) {
773
+ const accept = request.headers.get("accept") || "";
774
+ let body;
775
+ try {
776
+ body = await request.json();
777
+ } catch {
778
+ return NextResponse.json({ ok: false, error: "invalid JSON body" }, { status: 400 });
779
+ }
780
+
781
+ const wantsStream = body?.stream === true || accept.includes("application/x-ndjson");
782
+ if (!wantsStream) {
783
+ return executeSandboxRun(body);
784
+ }
785
+
786
+ const encoder = new TextEncoder();
787
+ const stream = new ReadableStream({
788
+ start(controller) {
789
+ const emit = (event) => {
790
+ controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`));
791
+ };
792
+ emit({
793
+ kind: "growthub-sandbox-run-delta-v1",
794
+ type: "sandbox-run.accepted",
795
+ emittedAt: new Date().toISOString(),
796
+ objectId: typeof body?.objectId === "string" ? body.objectId.trim() : "",
797
+ name: typeof body?.name === "string" ? body.name.trim() : ""
798
+ });
799
+ executeSandboxRun(body, { emit })
800
+ .then(async (response) => {
801
+ const finalPayload = await response.json().catch(() => ({ ok: false, error: "stream final payload unreadable" }));
802
+ emit({
803
+ kind: "growthub-sandbox-run-delta-v1",
804
+ type: "sandbox-run.final",
805
+ emittedAt: new Date().toISOString(),
806
+ status: response.status,
807
+ payload: finalPayload
808
+ });
809
+ })
810
+ .catch((error) => {
811
+ emit({
812
+ kind: "growthub-sandbox-run-delta-v1",
813
+ type: "sandbox-run.final",
814
+ emittedAt: new Date().toISOString(),
815
+ status: 500,
816
+ payload: { ok: false, error: error?.message || "sandbox run failed" }
817
+ });
818
+ })
819
+ .finally(() => controller.close());
820
+ }
821
+ });
822
+
823
+ return new Response(stream, {
824
+ headers: {
825
+ "content-type": "application/x-ndjson; charset=utf-8",
826
+ "cache-control": "no-store"
827
+ }
828
+ });
829
+ }
830
+
757
831
  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 = [
@@ -255,7 +255,7 @@ function WorkspaceHelperSetupModal({ workspaceConfig, open, onClose, onSaved })
255
255
  />
256
256
  serverless
257
257
  </label>
258
- <small>Local uses process sandbox or Paperclip agent host on this machine. Serverless delegates to an API Registry URL.</small>
258
+ <small>Choose local execution or a scheduled serverless run.</small>
259
259
  </div>
260
260
  <div className="workspace-helper-setup-field-stack">
261
261
  <label>