@growthub/cli 0.13.2 → 0.13.5

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 (42) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +184 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +24 -2
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +14 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +74 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +67 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +77 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +72 -4
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +123 -27
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +224 -1
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +754 -92
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +224 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxRunPanel.jsx +32 -1
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +530 -9
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +8 -1
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +10 -7
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/RunSetupPanel.jsx +261 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +119 -9
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +779 -138
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +91 -14
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +35 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +28 -3
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +216 -5
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +366 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +34 -3
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth-eligibility.js +50 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth-redaction.js +64 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +665 -0
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-host-catalog.js +168 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +595 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +164 -7
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +11 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +111 -1
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +14 -0
  42. package/package.json +1 -1
@@ -1,22 +1,376 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useMemo, useState } from "react";
4
- import { ArrowLeft, GitBranch } from "lucide-react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { ArrowLeft, Download, GitBranch, Play, RefreshCw, Search, Square } from "lucide-react";
5
5
  import { parseSandboxRunTrace, normalizeRunRecord } from "@/lib/orchestration-run-trace";
6
+ import {
7
+ buildRunTimeline,
8
+ downloadRunBundle,
9
+ filterRunLogTree,
10
+ formatRunDuration,
11
+ normalizeRunConsoleRecord
12
+ } from "@/lib/orchestration-run-console";
6
13
  import { redactSecretsFromText } from "@/lib/orchestration-graph";
7
14
 
15
+ const ACTIVE_STATUSES = new Set(["executing", "queued", "testing", "running"]);
16
+ const LIVE_POLL_MS = 1500;
17
+
18
+ function formatTimestamp(iso) {
19
+ const text = String(iso || "").trim();
20
+ if (!text) return "—";
21
+ const ms = Date.parse(text);
22
+ if (!Number.isFinite(ms)) return text;
23
+ try {
24
+ return new Date(ms).toLocaleString();
25
+ } catch {
26
+ return text;
27
+ }
28
+ }
29
+
30
+ function statusToVariant(status) {
31
+ switch (status) {
32
+ case "completed":
33
+ return "ok";
34
+ case "failed":
35
+ return "fail";
36
+ case "executing":
37
+ case "queued":
38
+ case "testing":
39
+ case "running":
40
+ return "active";
41
+ case "canceled":
42
+ return "canceled";
43
+ default:
44
+ return "neutral";
45
+ }
46
+ }
47
+
48
+ function describeStatus(status) {
49
+ switch (status) {
50
+ case "completed":
51
+ return "Completed";
52
+ case "failed":
53
+ return "Failed";
54
+ case "executing":
55
+ return "Executing";
56
+ case "queued":
57
+ return "Queued";
58
+ case "testing":
59
+ return "Testing";
60
+ case "running":
61
+ return "Running";
62
+ case "canceled":
63
+ return "Canceled";
64
+ default:
65
+ return "Unknown";
66
+ }
67
+ }
68
+
69
+ function previewRecordFromRow(row, rowTrace) {
70
+ const ranAt = rowTrace.ranAt || row?.lastTested || "";
71
+ const previewSource = {
72
+ runId: rowTrace.runId || row?.lastRunId || "",
73
+ ranAt,
74
+ exitCode: rowTrace.exitCode ?? null,
75
+ durationMs: rowTrace.durationMs ?? null,
76
+ error: rowTrace.error || "",
77
+ stdout: rowTrace.stdout || "",
78
+ stderr: rowTrace.stderr || "",
79
+ output: rowTrace.output || "",
80
+ runtime: rowTrace.runtime || row?.runtime || "",
81
+ adapter: rowTrace.adapter || row?.adapter || "",
82
+ runLocality: rowTrace.runLocality || row?.runLocality || "",
83
+ envRefsResolved: rowTrace.envRefsResolved || [],
84
+ envRefsMissing: rowTrace.envRefsMissing || [],
85
+ sourceId: row?.lastSourceId || "",
86
+ version: row?.version || "",
87
+ lifecycleStatus: row?.lifecycleStatus || ""
88
+ };
89
+ return normalizeRunConsoleRecord(previewSource);
90
+ }
91
+
92
+ function bundleToBlobUrl(bundle) {
93
+ const text = JSON.stringify(bundle, null, 2);
94
+ const blob = new Blob([text], { type: "application/json" });
95
+ return URL.createObjectURL(blob);
96
+ }
97
+
98
+ function downloadFile(filename, bundle) {
99
+ if (typeof window === "undefined") return;
100
+ const url = bundleToBlobUrl(bundle);
101
+ const link = document.createElement("a");
102
+ link.href = url;
103
+ link.download = filename;
104
+ document.body.appendChild(link);
105
+ link.click();
106
+ link.remove();
107
+ setTimeout(() => URL.revokeObjectURL(url), 0);
108
+ }
109
+
110
+ function LogTreeNode({ node, depth, selectedId, onSelect, timelineMax }) {
111
+ const isSelected = String(selectedId) === String(node.id);
112
+ const ratio = timelineMax > 0 && node.durationMs > 0
113
+ ? Math.min(1, node.durationMs / timelineMax)
114
+ : 0;
115
+ const variant = statusToVariant(node.status);
116
+ return (
117
+ <div className="dm-run-console__tree-block" data-depth={depth}>
118
+ <button
119
+ type="button"
120
+ className={`dm-run-console__tree-row${isSelected ? " is-active" : ""}`}
121
+ data-variant={variant}
122
+ onClick={() => onSelect(node)}
123
+ title={node.label}
124
+ >
125
+ <span className="dm-run-console__tree-indent" aria-hidden="true" style={{ width: `${depth * 12}px` }} />
126
+ <span className="dm-run-console__tree-dot" data-variant={variant} aria-hidden="true" />
127
+ <span className="dm-run-console__tree-label">{node.label}</span>
128
+ <span className="dm-run-console__tree-meta">{formatRunDuration(node.durationMs)}</span>
129
+ <span className="dm-run-console__tree-bar" aria-hidden="true">
130
+ <span style={{ width: `${Math.round(ratio * 100)}%` }} data-variant={variant} />
131
+ </span>
132
+ </button>
133
+ {Array.isArray(node.children) && node.children.length > 0 ? (
134
+ <div className="dm-run-console__tree-children">
135
+ {node.children.map((child) => (
136
+ <LogTreeNode
137
+ key={child.id}
138
+ node={child}
139
+ depth={depth + 1}
140
+ selectedId={selectedId}
141
+ onSelect={onSelect}
142
+ timelineMax={timelineMax}
143
+ />
144
+ ))}
145
+ </div>
146
+ ) : null}
147
+ </div>
148
+ );
149
+ }
150
+
151
+ function LifecycleBlock({ lifecycle }) {
152
+ if (!Array.isArray(lifecycle) || lifecycle.length === 0) {
153
+ return <p className="dm-run-console__hint">Lifecycle timestamps not available.</p>;
154
+ }
155
+ return (
156
+ <ol className="dm-run-console__lifecycle">
157
+ {lifecycle.map((step) => (
158
+ <li key={step.label}>
159
+ <span className="dm-run-console__lifecycle-label">{step.label}</span>
160
+ <span className="dm-run-console__lifecycle-at">{formatTimestamp(step.at)}</span>
161
+ {step.durationMs > 0 ? (
162
+ <span className="dm-run-console__lifecycle-dur">{formatRunDuration(step.durationMs)}</span>
163
+ ) : null}
164
+ </li>
165
+ ))}
166
+ </ol>
167
+ );
168
+ }
169
+
170
+ function KeyValueBlock({ entries }) {
171
+ const list = entries.filter(([, value]) => value !== "" && value != null);
172
+ if (!list.length) return null;
173
+ return (
174
+ <dl className="dm-run-console__kv">
175
+ {list.map(([label, value]) => (
176
+ <div key={label}>
177
+ <dt>{label}</dt>
178
+ <dd>{value}</dd>
179
+ </div>
180
+ ))}
181
+ </dl>
182
+ );
183
+ }
184
+
185
+ function CodeBlock({ label, body }) {
186
+ if (!body) return null;
187
+ return (
188
+ <div className="dm-run-console__code">
189
+ <span>{label}</span>
190
+ <pre>{body}</pre>
191
+ </div>
192
+ );
193
+ }
194
+
195
+ function downloadText(filename, text, mime = "text/plain") {
196
+ if (typeof window === "undefined") return;
197
+ const blob = new Blob([text || ""], { type: mime });
198
+ const url = URL.createObjectURL(blob);
199
+ const link = document.createElement("a");
200
+ link.href = url;
201
+ link.download = filename;
202
+ document.body.appendChild(link);
203
+ link.click();
204
+ link.remove();
205
+ setTimeout(() => URL.revokeObjectURL(url), 0);
206
+ }
207
+
208
+ async function copyToClipboard(text) {
209
+ if (typeof navigator === "undefined" || !navigator?.clipboard?.writeText) return false;
210
+ try {
211
+ await navigator.clipboard.writeText(text || "");
212
+ return true;
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+
218
+ function formatRewardScore(value) {
219
+ const n = Number(value);
220
+ if (!Number.isFinite(n)) return "—";
221
+ return n.toFixed(2);
222
+ }
223
+
224
+ function SwarmSection({ swarm }) {
225
+ if (!swarm || typeof swarm !== "object") return null;
226
+ const tasks = Array.isArray(swarm.tasks) ? swarm.tasks : [];
227
+ if (tasks.length === 0 && !swarm.orchestrator?.plan && !swarm.synthesis?.answer) return null;
228
+ const completed = tasks.filter((t) => t?.status === "completed").length;
229
+ const score = swarm.reward ? formatRewardScore(swarm.reward.score) : "—";
230
+ const kind = swarm.reward?.kind || "structural-v1";
231
+ const synthesis = swarm.synthesis || null;
232
+ return (
233
+ <section className="dm-run-console__section">
234
+ <h3>Swarm</h3>
235
+ <p className="dm-swarm-summary__line">
236
+ <span><strong>{completed}/{tasks.length}</strong></span>
237
+ <span>score <strong>{score}</strong></span>
238
+ <span className="dm-swarm-summary__kind" title={swarm.reward?.note || ""}>{kind}</span>
239
+ </p>
240
+ {synthesis?.answer ? (
241
+ <details className="dm-swarm-phase" open>
242
+ <summary>
243
+ synthesizer
244
+ {synthesis.parsedOutcomeScore != null
245
+ ? ` · ${Number(synthesis.parsedOutcomeScore).toFixed(2)}`
246
+ : ""}
247
+ </summary>
248
+ <pre>{synthesis.answer}</pre>
249
+ </details>
250
+ ) : null}
251
+ </section>
252
+ );
253
+ }
254
+
255
+ function InputsSection({ payload }) {
256
+ const runInputs = payload?.runInputs;
257
+ const summary = payload?.inputSummary;
258
+ if (!runInputs && (!summary || summary.fieldCount === 0)) {
259
+ return (
260
+ <section className="dm-run-console__section">
261
+ <h3>Inputs</h3>
262
+ <p className="dm-run-console__hint">No manual inputs were submitted for this run.</p>
263
+ </section>
264
+ );
265
+ }
266
+ const valueEntries = runInputs && runInputs.values && typeof runInputs.values === "object"
267
+ ? Object.entries(runInputs.values)
268
+ : [];
269
+ return (
270
+ <section className="dm-run-console__section">
271
+ <h3>Inputs</h3>
272
+ <KeyValueBlock
273
+ entries={[
274
+ ["Source", summary?.source || runInputs?.source || "manual"],
275
+ ["Fields", String(summary?.fieldCount ?? valueEntries.length)],
276
+ ["Files", String(summary?.fileCount ?? (Array.isArray(runInputs?.files) ? runInputs.files.length : 0))]
277
+ ]}
278
+ />
279
+ {valueEntries.length > 0 ? (
280
+ <dl className="dm-run-console__kv">
281
+ {valueEntries.map(([key, value]) => {
282
+ const isSecretRef = value && typeof value === "object" && "secretRef" in value;
283
+ const display = isSecretRef
284
+ ? `secretRef: ${String(value.secretRef || "[redacted]")}`
285
+ : typeof value === "string"
286
+ ? value
287
+ : JSON.stringify(value);
288
+ return (
289
+ <div key={key}>
290
+ <dt>{key}</dt>
291
+ <dd>{display}</dd>
292
+ </div>
293
+ );
294
+ })}
295
+ </dl>
296
+ ) : null}
297
+ </section>
298
+ );
299
+ }
300
+
301
+ function ExportActions({ exports, output, record, selectedLogNode }) {
302
+ const available = Array.isArray(exports?.available) ? exports.available : [];
303
+ if (available.length === 0) {
304
+ return <p className="dm-run-console__hint">No exports available for this run.</p>;
305
+ }
306
+ const runLabel = String(record?.runId || "run").replace(/[^a-zA-Z0-9_-]+/g, "-") || "run";
307
+ const [copied, setCopied] = useState("");
308
+
309
+ async function handleCopy() {
310
+ const ok = await copyToClipboard(output?.normalizedOutput || output?.stdout || "");
311
+ setCopied(ok ? "Output copied" : "Clipboard unavailable");
312
+ setTimeout(() => setCopied(""), 1600);
313
+ }
314
+
315
+ return (
316
+ <div className="dm-run-console__exports">
317
+ {available.includes("copy-output") && (
318
+ <button type="button" className="dm-btn-outline" onClick={handleCopy}>Copy output</button>
319
+ )}
320
+ {available.includes("download-stdout") && output?.stdout && (
321
+ <button
322
+ type="button"
323
+ className="dm-btn-outline"
324
+ onClick={() => downloadText(`growthub-run-${runLabel}-stdout.txt`, output.stdout)}
325
+ >Download stdout</button>
326
+ )}
327
+ {available.includes("download-stderr") && output?.stderr && (
328
+ <button
329
+ type="button"
330
+ className="dm-btn-outline"
331
+ onClick={() => downloadText(`growthub-run-${runLabel}-stderr.txt`, output.stderr)}
332
+ >Download stderr</button>
333
+ )}
334
+ {available.includes("download-normalized-output") && output?.normalizedOutput && output.normalizedOutput !== output.stdout && (
335
+ <button
336
+ type="button"
337
+ className="dm-btn-outline"
338
+ onClick={() => downloadText(`growthub-run-${runLabel}-output.json`, output.normalizedOutput, "application/json")}
339
+ >Download output JSON</button>
340
+ )}
341
+ {available.includes("download-log-node") && selectedLogNode?.text && (
342
+ <button
343
+ type="button"
344
+ className="dm-btn-outline"
345
+ onClick={() => downloadText(`growthub-run-${runLabel}-${selectedLogNode.id || "log"}.txt`, selectedLogNode.text)}
346
+ >Download selected log node</button>
347
+ )}
348
+ {copied && <span className="dm-run-console__export-toast">{copied}</span>}
349
+ </div>
350
+ );
351
+ }
352
+
8
353
  export function OrchestrationRunTracePanel({
9
354
  row,
10
355
  objectId,
11
356
  fieldName,
12
357
  selectedRunId,
13
358
  onBack,
14
- onOpenGraph
359
+ onOpenGraph,
360
+ onReplay,
361
+ running
15
362
  }) {
16
363
  const [history, setHistory] = useState([]);
17
364
  const [historyMessage, setHistoryMessage] = useState("");
18
365
  const [loading, setLoading] = useState(false);
19
366
  const [activeRunId, setActiveRunId] = useState(String(selectedRunId || row?.lastRunId || "").trim());
367
+ const [query, setQuery] = useState("");
368
+ const [errorsOnly, setErrorsOnly] = useState(false);
369
+ const [showQueueTime, setShowQueueTime] = useState(false);
370
+ const [activeDetailTab, setActiveDetailTab] = useState("overview");
371
+ const [selectedLogId, setSelectedLogId] = useState("root");
372
+ const [replayPending, setReplayPending] = useState(false);
373
+ const abortRef = useRef(null);
20
374
 
21
375
  const rowTrace = useMemo(() => parseSandboxRunTrace(row?.lastResponse), [row?.lastResponse]);
22
376
 
@@ -24,136 +378,444 @@ export function OrchestrationRunTracePanel({
24
378
  setActiveRunId(String(selectedRunId || row?.lastRunId || "").trim());
25
379
  }, [selectedRunId, row?.lastRunId, row?.lastResponse]);
26
380
 
27
- useEffect(() => {
381
+ const loadHistory = useCallback(async (signal) => {
28
382
  const objectIdValue = String(objectId || "").trim();
29
383
  const name = String(row?.Name || "").trim();
30
384
  if (!objectIdValue || !name) return;
31
385
  setLoading(true);
32
386
  setHistoryMessage("");
33
- fetch(`/api/workspace/sandbox-run?objectId=${encodeURIComponent(objectIdValue)}&name=${encodeURIComponent(name)}`, {
34
- cache: "no-store"
35
- })
36
- .then((res) => res.json())
37
- .then((payload) => {
38
- if (!payload.ok) throw new Error(payload.error || "Could not load run history");
39
- setHistory(Array.isArray(payload.records) ? payload.records.map(normalizeRunRecord).filter(Boolean) : []);
40
- setHistoryMessage(`${payload.recordCount || 0} saved run${payload.recordCount === 1 ? "" : "s"}`);
41
- })
42
- .catch((err) => {
43
- setHistory([]);
44
- setHistoryMessage(err.message || "Could not load run history");
45
- })
46
- .finally(() => setLoading(false));
387
+ try {
388
+ const res = await fetch(
389
+ `/api/workspace/sandbox-run?objectId=${encodeURIComponent(objectIdValue)}&name=${encodeURIComponent(name)}`,
390
+ { cache: "no-store", signal }
391
+ );
392
+ const payload = await res.json();
393
+ if (!payload.ok) throw new Error(payload.error || "Could not load run history");
394
+ const normalized = Array.isArray(payload.records)
395
+ ? payload.records.map(normalizeRunRecord).filter(Boolean)
396
+ : [];
397
+ setHistory(normalized);
398
+ setHistoryMessage(`${payload.recordCount || normalized.length} saved run${(payload.recordCount || normalized.length) === 1 ? "" : "s"}`);
399
+ } catch (err) {
400
+ if (err?.name === "AbortError") return;
401
+ setHistory([]);
402
+ setHistoryMessage(err.message || "Could not load run history");
403
+ } finally {
404
+ setLoading(false);
405
+ }
47
406
  }, [objectId, row?.Name]);
48
407
 
49
- const activeRecord = useMemo(() => {
408
+ useEffect(() => {
409
+ const controller = new AbortController();
410
+ loadHistory(controller.signal);
411
+ return () => controller.abort();
412
+ }, [loadHistory]);
413
+
414
+ const isClientRunning = Boolean(running) || replayPending;
415
+ const liveReloading = isClientRunning;
416
+
417
+ useEffect(() => {
418
+ if (!liveReloading) return undefined;
419
+ const id = setInterval(() => {
420
+ loadHistory();
421
+ }, LIVE_POLL_MS);
422
+ return () => clearInterval(id);
423
+ }, [liveReloading, loadHistory]);
424
+
425
+ const activeRawRecord = useMemo(() => {
50
426
  if (activeRunId && history.length) {
51
427
  const match = history.find((r) => r.runId === activeRunId);
52
428
  if (match) return match;
53
429
  }
54
430
  if (fieldName === "lastResponse" || !activeRunId) {
55
- return {
56
- runId: rowTrace.runId || row?.lastRunId || "",
57
- ranAt: rowTrace.ranAt || row?.lastTested || "",
58
- exitCode: rowTrace.exitCode,
59
- durationMs: rowTrace.durationMs,
60
- error: rowTrace.error,
61
- stdout: rowTrace.stdout,
62
- stderr: rowTrace.stderr,
63
- output: rowTrace.output
64
- };
431
+ return null;
65
432
  }
66
433
  return history[0] || null;
67
- }, [activeRunId, history, rowTrace, fieldName, row?.lastRunId, row?.lastTested]);
434
+ }, [activeRunId, history, fieldName]);
435
+
436
+ const activeConsoleRecord = useMemo(() => {
437
+ if (activeRawRecord) return normalizeRunConsoleRecord(activeRawRecord);
438
+ return previewRecordFromRow(row, rowTrace);
439
+ }, [activeRawRecord, row, rowTrace]);
68
440
 
69
- const summary = fieldName === "lastSourceId"
441
+ const timeline = useMemo(() => buildRunTimeline(history), [history]);
442
+ const timelineMax = timeline.reduce((m, item) => Math.max(m, item.durationMs || 0), 0);
443
+
444
+ const filteredTree = useMemo(() => (
445
+ filterRunLogTree(activeConsoleRecord?.logTree || [], { query, errorsOnly })
446
+ ), [activeConsoleRecord, query, errorsOnly]);
447
+
448
+ const selectedLogNode = useMemo(() => {
449
+ const tree = activeConsoleRecord?.logTree || [];
450
+ const stack = [...tree];
451
+ while (stack.length) {
452
+ const node = stack.pop();
453
+ if (!node) continue;
454
+ if (String(node.id) === String(selectedLogId)) return node;
455
+ if (Array.isArray(node.children)) stack.push(...node.children);
456
+ }
457
+ return tree[0] || null;
458
+ }, [activeConsoleRecord, selectedLogId]);
459
+
460
+ useEffect(() => {
461
+ setSelectedLogId("root");
462
+ }, [activeRunId]);
463
+
464
+ const summaryText = fieldName === "lastSourceId"
70
465
  ? `Source ${String(row?.lastSourceId || "").trim()}`
71
466
  : fieldName === "lastRunId"
72
467
  ? `Run ${String(row?.lastRunId || activeRunId || "").trim()}`
73
468
  : "Latest sandbox run";
74
469
 
470
+ const statusVariant = statusToVariant(activeConsoleRecord?.status);
471
+ const statusLabel = describeStatus(activeConsoleRecord?.status);
472
+
473
+ const canReplay = typeof onReplay === "function";
474
+
475
+ function handleReplay() {
476
+ if (!canReplay) return;
477
+ setReplayPending(true);
478
+ const controller = new AbortController();
479
+ abortRef.current = controller;
480
+ try {
481
+ const result = onReplay({ signal: controller.signal });
482
+ Promise.resolve(result)
483
+ .catch(() => {})
484
+ .finally(() => {
485
+ setReplayPending(false);
486
+ abortRef.current = null;
487
+ loadHistory();
488
+ });
489
+ } catch {
490
+ setReplayPending(false);
491
+ abortRef.current = null;
492
+ }
493
+ }
494
+
495
+ function handleCancel() {
496
+ if (abortRef.current) {
497
+ try { abortRef.current.abort(); } catch { /* noop */ }
498
+ }
499
+ setReplayPending(false);
500
+ }
501
+
502
+ function handleDownload() {
503
+ if (!activeConsoleRecord) return;
504
+ const bundle = downloadRunBundle({
505
+ record: activeRawRecord || activeConsoleRecord,
506
+ runId: activeConsoleRecord.runId,
507
+ sourceId: activeConsoleRecord.sourceId || row?.lastSourceId || ""
508
+ });
509
+ const runLabel = String(bundle.runId || "run").replace(/[^a-zA-Z0-9_-]+/g, "-") || "run";
510
+ downloadFile(`growthub-run-${runLabel}.json`, bundle);
511
+ }
512
+
513
+ const lifecycleEntries = activeConsoleRecord?.lifecycle || [];
514
+ const payload = activeConsoleRecord?.payload || {};
515
+ const output = activeConsoleRecord?.output || {};
516
+ const context = activeConsoleRecord?.context || {};
517
+ const swarmPayload = activeConsoleRecord?.swarm
518
+ || (activeRawRecord && activeRawRecord.swarm)
519
+ || null;
520
+
75
521
  return (
76
- <section className="dm-orchestration-trace" aria-label="Run trace viewer">
77
- <header className="dm-orchestration-trace__head">
78
- <button type="button" className="dm-orchestration-header__back" onClick={onBack} aria-label="Back to record">
79
- <ArrowLeft size={16} />
80
- </button>
81
- <div>
82
- <h2>Run trace</h2>
83
- <p>{summary} · {row?.Name || "Sandbox tool"}</p>
84
- </div>
85
- {onOpenGraph && (
86
- <button type="button" className="dm-btn-outline" onClick={onOpenGraph}>
87
- <GitBranch size={14} aria-hidden="true" />
88
- Edit graph
522
+ <section className="dm-run-console" aria-label="Live runs console">
523
+ <header className="dm-run-console__head">
524
+ {onBack && (
525
+ <button type="button" className="dm-orchestration-header__back" onClick={onBack} aria-label="Back to record">
526
+ <ArrowLeft size={16} />
89
527
  </button>
90
528
  )}
91
- </header>
92
-
93
- <div className="dm-orchestration-trace__body">
94
- <aside className="dm-orchestration-trace__list">
95
- <p className="dm-orchestration-trace__list-title">Run history</p>
96
- {loading && <p className="dm-orchestration-config__hint">Loading…</p>}
97
- {!loading && historyMessage && <p className="dm-orchestration-config__hint">{historyMessage}</p>}
529
+ <div className="dm-run-console__head-titles">
530
+ <span className="dm-run-console__crumbs">
531
+ Runs <span aria-hidden="true">/</span> <code>{activeConsoleRecord?.runId || "preview"}</code>
532
+ </span>
533
+ <h2>Run console</h2>
534
+ <p>{summaryText} · {row?.Name || "Sandbox tool"}</p>
535
+ </div>
536
+ <div className="dm-run-console__head-actions">
537
+ {canReplay && (
538
+ <button
539
+ type="button"
540
+ className="dm-btn-outline"
541
+ onClick={handleReplay}
542
+ disabled={isClientRunning}
543
+ title="Replay current saved config"
544
+ >
545
+ <Play size={13} aria-hidden="true" />
546
+ {replayPending ? "Replaying" : "Replay current config"}
547
+ </button>
548
+ )}
549
+ {isClientRunning && (
550
+ <button
551
+ type="button"
552
+ className="dm-btn-outline dm-run-console__cancel"
553
+ onClick={handleCancel}
554
+ title="Cancel the in-flight client request"
555
+ >
556
+ <Square size={13} aria-hidden="true" />
557
+ Cancel request
558
+ </button>
559
+ )}
98
560
  <button
99
561
  type="button"
100
- className={`dm-orchestration-trace__run${!activeRunId ? " is-active" : ""}`}
101
- onClick={() => setActiveRunId("")}
562
+ className="dm-btn-outline"
563
+ onClick={handleDownload}
564
+ disabled={!activeConsoleRecord}
565
+ title="Download redacted JSON log for this run"
102
566
  >
103
- <span>Row preview</span>
104
- <span>{row?.status || rowTrace.status || "—"} · {row?.lastTested || rowTrace.ranAt || "—"}</span>
567
+ <Download size={13} aria-hidden="true" />
568
+ Download logs
105
569
  </button>
106
- {history.map((record) => (
570
+ {onOpenGraph && (
571
+ <button type="button" className="dm-btn-outline" onClick={onOpenGraph}>
572
+ <GitBranch size={13} aria-hidden="true" />
573
+ Edit graph
574
+ </button>
575
+ )}
576
+ </div>
577
+ </header>
578
+
579
+ <div className="dm-run-console__split">
580
+ <aside className="dm-run-console__left">
581
+ <div className="dm-run-console__toolbar">
582
+ <label className="dm-run-console__search">
583
+ <Search size={12} aria-hidden="true" />
584
+ <input
585
+ type="search"
586
+ placeholder="Search log"
587
+ value={query}
588
+ onChange={(e) => setQuery(e.target.value)}
589
+ aria-label="Search log"
590
+ />
591
+ </label>
592
+ <label className="dm-run-console__toggle">
593
+ <input
594
+ type="checkbox"
595
+ checked={showQueueTime}
596
+ onChange={(e) => setShowQueueTime(e.target.checked)}
597
+ />
598
+ <span>Queue time</span>
599
+ </label>
600
+ <label className="dm-run-console__toggle">
601
+ <input
602
+ type="checkbox"
603
+ checked={errorsOnly}
604
+ onChange={(e) => setErrorsOnly(e.target.checked)}
605
+ />
606
+ <span>Errors only</span>
607
+ </label>
608
+ <span
609
+ className={`dm-run-console__live${liveReloading ? " is-on" : ""}`}
610
+ aria-live="polite"
611
+ >
612
+ <span className="dm-run-console__live-dot" aria-hidden="true" />
613
+ {liveReloading ? "Live reloading" : "Idle"}
614
+ </span>
615
+ </div>
616
+
617
+ <div className="dm-run-console__history">
618
+ <p className="dm-run-console__history-title">Run history</p>
619
+ {loading && <p className="dm-run-console__hint">Loading…</p>}
620
+ {!loading && historyMessage && <p className="dm-run-console__hint">{historyMessage}</p>}
107
621
  <button
108
- key={record.runId || record.ranAt}
109
622
  type="button"
110
- className={`dm-orchestration-trace__run${activeRunId === record.runId ? " is-active" : ""}`}
111
- onClick={() => setActiveRunId(record.runId)}
623
+ className={`dm-run-console__history-row${!activeRunId ? " is-active" : ""}`}
624
+ onClick={() => setActiveRunId("")}
112
625
  >
113
- <span>{record.runId || "run"}</span>
114
- <span>
115
- {record.exitCode === 0 && !record.error ? "success" : "failed"}
116
- {record.ranAt ? ` · ${record.ranAt}` : ""}
626
+ <span className="dm-run-console__history-label">Row preview</span>
627
+ <span className="dm-run-console__history-meta">
628
+ {row?.status || rowTrace.status || ""} · {formatTimestamp(row?.lastTested || rowTrace.ranAt)}
117
629
  </span>
118
630
  </button>
119
- ))}
120
- </aside>
631
+ {history.map((record) => {
632
+ const isActive = activeRunId === record.runId;
633
+ const variant = statusToVariant(normalizeRunConsoleRecord(record)?.status);
634
+ return (
635
+ <button
636
+ key={record.runId || record.ranAt}
637
+ type="button"
638
+ className={`dm-run-console__history-row${isActive ? " is-active" : ""}`}
639
+ data-variant={variant}
640
+ onClick={() => setActiveRunId(record.runId)}
641
+ >
642
+ <span className="dm-run-console__history-label">
643
+ <span className="dm-run-console__tree-dot" data-variant={variant} aria-hidden="true" />
644
+ {record.runId || "run"}
645
+ </span>
646
+ <span className="dm-run-console__history-meta">
647
+ {record.exitCode === 0 && !record.error ? "completed" : "failed"} · {formatTimestamp(record.ranAt)}
648
+ </span>
649
+ </button>
650
+ );
651
+ })}
652
+ </div>
653
+
654
+ <div className="dm-run-console__tree">
655
+ <p className="dm-run-console__history-title">Log tree</p>
656
+ {filteredTree.length === 0 && (
657
+ <p className="dm-run-console__hint">
658
+ {errorsOnly ? "No error entries in this run." : "No log entries yet."}
659
+ </p>
660
+ )}
661
+ {filteredTree.map((node) => (
662
+ <LogTreeNode
663
+ key={node.id}
664
+ node={node}
665
+ depth={0}
666
+ selectedId={selectedLogNode?.id}
667
+ onSelect={(n) => setSelectedLogId(n.id)}
668
+ timelineMax={timelineMax}
669
+ />
670
+ ))}
671
+ </div>
121
672
 
122
- <div className="dm-orchestration-trace__detail">
123
- <dl className="dm-orchestration-trace__meta">
124
- <div><dt>Status</dt><dd>{row?.status || rowTrace.status || (activeRecord?.exitCode === 0 ? "connected" : "—")}</dd></div>
125
- <div><dt>Run ID</dt><dd>{activeRecord?.runId || row?.lastRunId || "—"}</dd></div>
126
- <div><dt>Exit code</dt><dd>{activeRecord?.exitCode ?? rowTrace.exitCode ?? "—"}</dd></div>
127
- <div><dt>Duration</dt><dd>{activeRecord?.durationMs ?? rowTrace.durationMs ?? "—"} ms</dd></div>
128
- <div><dt>Runtime</dt><dd>{rowTrace.runtime || row?.runtime || ""}</dd></div>
129
- <div><dt>Adapter</dt><dd>{rowTrace.adapter || row?.adapter || ""}</dd></div>
130
- <div><dt>Run locality</dt><dd>{rowTrace.runLocality || row?.runLocality || ""}</dd></div>
131
- <div><dt>Tested</dt><dd>{activeRecord?.ranAt || row?.lastTested || rowTrace.ranAt || "—"}</dd></div>
132
- </dl>
133
-
134
- {(activeRecord?.error || rowTrace.error) && (
135
- <div className="dm-orchestration-trace__error">
136
- <span>Error</span>
137
- <pre>{redactSecretsFromText(activeRecord?.error || rowTrace.error)}</pre>
673
+ {showQueueTime && timeline.length > 0 && (
674
+ <div className="dm-run-console__timeline">
675
+ <p className="dm-run-console__history-title">Timeline</p>
676
+ {timeline.map((item) => {
677
+ const variant = statusToVariant(item.status);
678
+ return (
679
+ <div key={item.runId || item.ranAt} className="dm-run-console__timeline-row" data-variant={variant}>
680
+ <span className="dm-run-console__timeline-label" title={item.runId}>{item.runId || "run"}</span>
681
+ <span className="dm-run-console__timeline-bar" aria-hidden="true">
682
+ <span style={{ width: `${Math.round((item.barRatio || 0) * 100)}%` }} data-variant={variant} />
683
+ </span>
684
+ <span className="dm-run-console__timeline-dur">{formatRunDuration(item.durationMs)}</span>
685
+ </div>
686
+ );
687
+ })}
138
688
  </div>
139
689
  )}
690
+ </aside>
140
691
 
141
- <div className="dm-orchestration-trace__output">
142
- <span>Stdout</span>
143
- <pre>{redactSecretsFromText(activeRecord?.stdout || rowTrace.stdout || "")}</pre>
692
+ <div className="dm-run-console__right">
693
+ <div className="dm-run-console__detail-head">
694
+ <div className="dm-run-console__detail-title">
695
+ <span className={`dm-run-console__status-pill is-${statusVariant}`}>{statusLabel}</span>
696
+ <strong>{selectedLogNode?.label || "agent-run"}</strong>
697
+ <small>{activeConsoleRecord?.runId || "—"}</small>
698
+ </div>
699
+ <div className="dm-run-console__detail-tabs" role="tablist">
700
+ {["overview", "detail", "context"].map((tab) => (
701
+ <button
702
+ key={tab}
703
+ type="button"
704
+ role="tab"
705
+ className={`dm-run-console__detail-tab${activeDetailTab === tab ? " is-active" : ""}`}
706
+ onClick={() => setActiveDetailTab(tab)}
707
+ >
708
+ {tab === "overview" ? "Overview" : tab === "detail" ? "Detail" : "Context"}
709
+ </button>
710
+ ))}
711
+ </div>
144
712
  </div>
145
713
 
146
- {(activeRecord?.output || rowTrace.output) && (
147
- <div className="dm-orchestration-trace__output">
148
- <span>Normalized output</span>
149
- <pre>{redactSecretsFromText(activeRecord?.output || rowTrace.output)}</pre>
714
+ {activeDetailTab === "overview" && (
715
+ <div className="dm-run-console__detail-body">
716
+ <section className="dm-run-console__section">
717
+ <h3>Lifecycle</h3>
718
+ <LifecycleBlock lifecycle={lifecycleEntries} />
719
+ </section>
720
+ <section className="dm-run-console__section">
721
+ <h3>Summary</h3>
722
+ <KeyValueBlock
723
+ entries={[
724
+ ["Status", statusLabel],
725
+ ["Run ID", activeConsoleRecord?.runId || "—"],
726
+ ["Exit code", activeConsoleRecord?.exitCode == null ? "—" : String(activeConsoleRecord.exitCode)],
727
+ ["Duration", formatRunDuration(activeConsoleRecord?.durationMs)],
728
+ ["Runtime", activeConsoleRecord?.runtime || "—"],
729
+ ["Adapter", activeConsoleRecord?.adapter || "—"],
730
+ ["Run locality", activeConsoleRecord?.runLocality || "—"],
731
+ ["Tested", formatTimestamp(activeConsoleRecord?.ranAt)],
732
+ ["Finished", formatTimestamp(activeConsoleRecord?.finishedAt)]
733
+ ]}
734
+ />
735
+ </section>
736
+ <section className="dm-run-console__section">
737
+ <h3>Payload</h3>
738
+ <KeyValueBlock
739
+ entries={[
740
+ ["Object", payload.objectId || "—"],
741
+ ["Name", payload.name || row?.Name || "—"],
742
+ ["Runtime", payload.runtime || "—"],
743
+ ["Adapter", payload.adapter || "—"],
744
+ ["Version", payload.version || "—"],
745
+ ["Agent host", payload.agentHost || "—"],
746
+ ["Scheduler", payload.schedulerRegistryId || "—"],
747
+ ["Timeout", payload.timeoutMs ? `${payload.timeoutMs} ms` : "—"]
748
+ ]}
749
+ />
750
+ <CodeBlock label="Command" body={payload.command} />
751
+ <CodeBlock label="Instructions" body={payload.instructions} />
752
+ </section>
753
+ <SwarmSection swarm={swarmPayload} />
754
+ <InputsSection payload={payload} />
755
+ </div>
756
+ )}
757
+
758
+ {activeDetailTab === "detail" && (
759
+ <div className="dm-run-console__detail-body">
760
+ <section className="dm-run-console__section">
761
+ <h3>Export</h3>
762
+ <ExportActions
763
+ exports={activeConsoleRecord?.exports}
764
+ output={output}
765
+ record={activeConsoleRecord}
766
+ selectedLogNode={selectedLogNode}
767
+ />
768
+ </section>
769
+ <section className="dm-run-console__section">
770
+ <h3>Output</h3>
771
+ <CodeBlock label="Error" body={output.error} />
772
+ <CodeBlock label="Stdout" body={output.stdout || "—"} />
773
+ {output.normalizedOutput && output.normalizedOutput !== output.stdout && (
774
+ <CodeBlock label="Normalized output" body={output.normalizedOutput} />
775
+ )}
776
+ <CodeBlock label="Stderr" body={output.stderr} />
777
+ </section>
778
+ {selectedLogNode?.text ? (
779
+ <section className="dm-run-console__section">
780
+ <h3>{selectedLogNode.label}</h3>
781
+ <CodeBlock label={selectedLogNode.type || "log"} body={selectedLogNode.text} />
782
+ </section>
783
+ ) : null}
150
784
  </div>
151
785
  )}
152
786
 
153
- {(activeRecord?.stderr || rowTrace.stderr) && (
154
- <div className="dm-orchestration-trace__output">
155
- <span>Stderr</span>
156
- <pre>{redactSecretsFromText(activeRecord?.stderr || rowTrace.stderr)}</pre>
787
+ {activeDetailTab === "context" && (
788
+ <div className="dm-run-console__detail-body">
789
+ <section className="dm-run-console__section">
790
+ <h3>Environment</h3>
791
+ <KeyValueBlock
792
+ entries={[
793
+ ["Network allow", context.networkAllow ? "true" : "false"],
794
+ ["Allow list", context.allowList?.join(", ") || "—"],
795
+ ["Env refs resolved", context.envRefsResolved?.join(", ") || "—"],
796
+ ["Env refs missing", context.envRefsMissing?.join(", ") || "—"],
797
+ ["Source ID", activeConsoleRecord?.sourceId || row?.lastSourceId || "—"]
798
+ ]}
799
+ />
800
+ </section>
801
+ {context.adapterMeta && (
802
+ <section className="dm-run-console__section">
803
+ <h3>Adapter metadata</h3>
804
+ <CodeBlock
805
+ label="adapterMeta"
806
+ body={redactSecretsFromText(JSON.stringify(context.adapterMeta, null, 2))}
807
+ />
808
+ </section>
809
+ )}
810
+ {context.templateTrace && (
811
+ <section className="dm-run-console__section">
812
+ <h3>Template trace</h3>
813
+ <CodeBlock
814
+ label="templateTrace"
815
+ body={redactSecretsFromText(JSON.stringify(context.templateTrace, null, 2))}
816
+ />
817
+ </section>
818
+ )}
157
819
  </div>
158
820
  )}
159
821
  </div>