@smithers-orchestrator/cli 0.20.0 → 0.20.3

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 (37) hide show
  1. package/dist/agent-detection.d.ts +20 -3
  2. package/package.json +17 -19
  3. package/src/AgentAvailability.ts +3 -0
  4. package/src/agent-detection.js +226 -14
  5. package/src/ask.js +4 -6
  6. package/src/index.js +89 -45
  7. package/src/workflow-pack.js +48 -10
  8. package/src/tui/app.jsx +0 -139
  9. package/src/tui/app.tsx +0 -5
  10. package/src/tui/components/AskModal.jsx +0 -109
  11. package/src/tui/components/AskModal.tsx +0 -3
  12. package/src/tui/components/AttentionPane.jsx +0 -112
  13. package/src/tui/components/AttentionPane.tsx +0 -6
  14. package/src/tui/components/ChatPane.jsx +0 -57
  15. package/src/tui/components/ChatPane.tsx +0 -7
  16. package/src/tui/components/CronList.jsx +0 -87
  17. package/src/tui/components/CronList.tsx +0 -5
  18. package/src/tui/components/DetailsPane.jsx +0 -96
  19. package/src/tui/components/DetailsPane.tsx +0 -7
  20. package/src/tui/components/FramesPane.jsx +0 -147
  21. package/src/tui/components/FramesPane.tsx +0 -8
  22. package/src/tui/components/LogsPane.jsx +0 -46
  23. package/src/tui/components/LogsPane.tsx +0 -6
  24. package/src/tui/components/MetricsPane.jsx +0 -108
  25. package/src/tui/components/MetricsPane.tsx +0 -5
  26. package/src/tui/components/NodeDetailView.jsx +0 -284
  27. package/src/tui/components/NodeDetailView.tsx +0 -7
  28. package/src/tui/components/NodeInspector.jsx +0 -51
  29. package/src/tui/components/NodeInspector.tsx +0 -7
  30. package/src/tui/components/RunDetailView.jsx +0 -190
  31. package/src/tui/components/RunDetailView.tsx +0 -7
  32. package/src/tui/components/RunsList.jsx +0 -184
  33. package/src/tui/components/RunsList.tsx +0 -7
  34. package/src/tui/components/SqliteBrowser.jsx +0 -131
  35. package/src/tui/components/SqliteBrowser.tsx +0 -5
  36. package/src/tui/components/WorkflowLauncher.jsx +0 -63
  37. package/src/tui/components/WorkflowLauncher.tsx +0 -3
@@ -1,108 +0,0 @@
1
- // @ts-nocheck
2
- import React, { useEffect, useState } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
5
-
6
- /**
7
- * @param {{ adapter: SmithersDb; onBack: () => void; }} value
8
- */
9
- export function MetricsPane({ adapter, onBack, }) {
10
- const [stats, setStats] = useState({
11
- runsTotal: 0,
12
- runsFinished: 0,
13
- nodesTotal: 0,
14
- tokensIn: 0,
15
- tokensOut: 0,
16
- tokensCache: 0,
17
- series: []
18
- });
19
- useEffect(() => {
20
- let mounted = true;
21
- async function fetchStats() {
22
- try {
23
- const [runStats] = await adapter.rawQuery(`SELECT count(*) as total, sum(case when status='finished' then 1 else 0 end) as finished FROM _smithers_runs`);
24
- const [nodeStats] = await adapter.rawQuery(`SELECT count(*) as total FROM _smithers_nodes`);
25
- const [tokenStats] = await adapter.rawQuery(`
26
- SELECT
27
- sum(cast(json_extract(payload_json, '$.inputTokens') as integer)) as tIn,
28
- sum(cast(json_extract(payload_json, '$.outputTokens') as integer)) as tOut,
29
- sum(cast(json_extract(payload_json, '$.cacheReadTokens') as integer)) as tCache
30
- FROM _smithers_events
31
- WHERE type = 'TokenUsageReported'
32
- `);
33
- // Last 24hr timeseries
34
- const nowMs = Date.now();
35
- const oneDayAgo = nowMs - (24 * 60 * 60 * 1000);
36
- const series = await adapter.rawQuery(`
37
- SELECT
38
- strftime('%H:00', datetime(timestamp_ms/1000, 'unixepoch', 'localtime')) as hr,
39
- sum(cast(json_extract(payload_json, '$.inputTokens') as integer) + cast(json_extract(payload_json, '$.outputTokens') as integer)) as totalTokens
40
- FROM _smithers_events
41
- WHERE type = 'TokenUsageReported' AND timestamp_ms > ${oneDayAgo}
42
- GROUP BY hr
43
- ORDER BY timestamp_ms ASC
44
- LIMIT 24
45
- `);
46
- if (mounted) {
47
- setStats({
48
- runsTotal: runStats?.total || 0,
49
- runsFinished: runStats?.finished || 0,
50
- nodesTotal: nodeStats?.total || 0,
51
- tokensIn: tokenStats?.tIn || 0,
52
- tokensOut: tokenStats?.tOut || 0,
53
- tokensCache: tokenStats?.tCache || 0,
54
- series: series || []
55
- });
56
- }
57
- }
58
- catch (err) {
59
- // fail silently for telemetry
60
- }
61
- }
62
- fetchStats();
63
- const interval = setInterval(fetchStats, 5000);
64
- return () => {
65
- mounted = false;
66
- clearInterval(interval);
67
- };
68
- }, [adapter]);
69
- useKeyboard((key) => {
70
- if (key.name === "escape") {
71
- onBack();
72
- }
73
- });
74
- // Render Sparkline
75
- const maxTokens = Math.max(1, ...stats.series.map((s) => s.totalTokens || 0));
76
- const blocks = [' ', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
77
- const sparkline = stats.series.map((s) => {
78
- const val = s.totalTokens || 0;
79
- const idx = Math.floor((val / maxTokens) * (blocks.length - 1));
80
- return blocks[idx];
81
- }).join("");
82
- const labels = stats.series.map((s) => s.hr).join(" ");
83
- return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "column", paddingLeft: 1 }}>
84
- <text style={{ color: "cyan", marginBottom: 1 }}> 📊 Smithers Global Telemetry (Prometheus Rollup) </text>
85
-
86
- <box style={{ flexDirection: "row", marginBottom: 2 }}>
87
- <box style={{ width: 30, flexDirection: "column", borderRight: true, borderColor: "gray" }}>
88
- <text style={{ color: "white" }}> Lifetime Runs: </text>
89
- <text style={{ color: "green" }}> {stats.runsTotal} ({stats.runsFinished} completed) </text>
90
- </box>
91
- <box style={{ width: 30, flexDirection: "column", borderRight: true, borderColor: "gray", paddingLeft: 1 }}>
92
- <text style={{ color: "white" }}> Total Nodes Executed: </text>
93
- <text style={{ color: "yellow" }}> {stats.nodesTotal} tasks </text>
94
- </box>
95
- <box style={{ width: 40, flexDirection: "column", paddingLeft: 1 }}>
96
- <text style={{ color: "white" }}> LLM Token Throughput: </text>
97
- <text style={{ color: "magenta" }}> IN: {stats.tokensIn} | OUT: {stats.tokensOut} </text>
98
- </box>
99
- </box>
100
-
101
- <text style={{ color: "gray", marginTop: 1 }}> Token Usage (Last 24 Hours) </text>
102
- <box style={{ height: 6, width: "100%", flexDirection: "column", marginTop: 1, border: true, borderColor: "#34d399", paddingLeft: 1 }}>
103
- <text style={{ color: "cyan", marginTop: 1 }}> {sparkline || "No token telemetry to graph"} </text>
104
- <text style={{ color: "gray" }}> {labels} </text>
105
- </box>
106
-
107
- </box>);
108
- }
@@ -1,5 +0,0 @@
1
- import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
- export declare function MetricsPane({ adapter, onBack, }: {
3
- adapter: SmithersDb;
4
- onBack: () => void;
5
- }): import("react/jsx-runtime").JSX.Element;
@@ -1,284 +0,0 @@
1
- // @ts-nocheck
2
- import React, { useEffect, useState } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- import { ChatPane } from "./ChatPane.jsx";
5
- import { LogsPane } from "./LogsPane.jsx";
6
- import { FramesPane } from "./FramesPane.jsx";
7
- /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
8
-
9
- /**
10
- * @param {{ adapter: SmithersDb; runId: string; nodeId: string | null; // null means "Global Run" onBack: () => void; }} value
11
- */
12
- export function NodeDetailView({ adapter, runId, nodeId, onBack, }) {
13
- const [runData, setRunData] = useState(null);
14
- const [nodeData, setNodeData] = useState(null);
15
- const [attempts, setAttempts] = useState([]);
16
- const [events, setEvents] = useState([]);
17
- const [rawOutput, setRawOutput] = useState(null);
18
- const [scorerResults, setScorerResults] = useState([]);
19
- const [tab, setTab] = useState("output");
20
- useEffect(() => {
21
- let mounted = true;
22
- let timeout;
23
- async function fetchDetails() {
24
- if (!mounted)
25
- return;
26
- try {
27
- const run = await adapter.getRun(runId);
28
- const fetchedAttempts = await adapter.listAttemptsForRun(runId);
29
- const fetchedEvents = await adapter.listEvents(runId, -1, 10000);
30
- let fetchedNode = null;
31
- let fetchedRawOutput = null;
32
- if (nodeId) {
33
- const nodes = await adapter.listNodes(runId);
34
- fetchedNode = nodes.find(n => n.nodeId === nodeId) || null;
35
- if (fetchedNode?.outputTable) {
36
- fetchedRawOutput = await adapter.getRawNodeOutput(fetchedNode.outputTable, runId, nodeId);
37
- }
38
- }
39
- let fetchedScores = [];
40
- try {
41
- fetchedScores = await adapter.listScorerResults(runId, nodeId ?? undefined);
42
- }
43
- catch { }
44
- if (mounted) {
45
- setRunData(run);
46
- setNodeData(fetchedNode);
47
- setAttempts(fetchedAttempts);
48
- setEvents(fetchedEvents);
49
- setRawOutput(fetchedRawOutput);
50
- setScorerResults(fetchedScores ?? []);
51
- }
52
- }
53
- catch (err) { }
54
- if (mounted)
55
- timeout = setTimeout(fetchDetails, 1000);
56
- }
57
- fetchDetails();
58
- return () => {
59
- mounted = false;
60
- clearTimeout(timeout);
61
- };
62
- }, [adapter, runId, nodeId]);
63
- const isGlobal = nodeId === null;
64
- useKeyboard((key) => {
65
- if (key.name === "escape" || (key.name === "c" && key.ctrl) || key.name === "backspace") {
66
- onBack();
67
- return;
68
- }
69
- if (key.name === "r" && !isGlobal && latestAttempt) {
70
- Bun.spawn([
71
- "bun", "run", "src/index.js", "revert",
72
- "--runId", runId,
73
- "--nodeId", nodeId,
74
- "--attempt", latestAttempt.attempt.toString(),
75
- "--iteration", latestAttempt.iteration.toString()
76
- ], { stdout: "ignore", stderr: "ignore" }).unref();
77
- return;
78
- }
79
- const tabList = ["input", "output", "frames", "chat", "logs", "scores"];
80
- if (key.name === "left" || key.name === "h") {
81
- setTab((prev) => tabList[(tabList.indexOf(prev) - 1 + tabList.length) % tabList.length]);
82
- return;
83
- }
84
- if (key.name === "right" || key.name === "l") {
85
- setTab((prev) => tabList[(tabList.indexOf(prev) + 1) % tabList.length]);
86
- return;
87
- }
88
- if (key.name === "1")
89
- setTab("input");
90
- if (key.name === "2")
91
- setTab("output");
92
- if (key.name === "3")
93
- setTab("frames");
94
- if (key.name === "4")
95
- setTab("chat");
96
- if (key.name === "5")
97
- setTab("logs");
98
- if (key.name === "6")
99
- setTab("scores");
100
- });
101
- if (!runData) {
102
- return <text style={{ margin: 1 }}>Loading inspection data...</text>;
103
- }
104
- /**
105
- * @param {string} [jsonStr]
106
- */
107
- function safePretty(jsonStr) {
108
- if (!jsonStr)
109
- return "None";
110
- try {
111
- return JSON.stringify(JSON.parse(jsonStr), null, 2);
112
- }
113
- catch {
114
- return jsonStr.substring(0, 10000); // RAW String output
115
- }
116
- }
117
- const targetAttempts = isGlobal ? [] : attempts.filter((a) => a.nodeId === nodeId).sort((a, b) => b.attempt - a.attempt);
118
- const latestAttempt = targetAttempts.length > 0 ? targetAttempts[0] : null;
119
- let inputData = isGlobal ? runData.configJson : "Inputs are dynamically constructed. No static properties were captured for this task frame.";
120
- if (!isGlobal && latestAttempt?.metaJson) {
121
- try {
122
- const meta = JSON.parse(latestAttempt.metaJson);
123
- const { inputPrompt, systemPrompt, agentId, model, config, approvalMode, ...rest } = meta;
124
- let str = "[ Agent Configuration ]\n";
125
- if (agentId || model)
126
- str += `Agent: ${agentId ?? "unknown"} | Model: ${model ?? "default"}\n`;
127
- if (approvalMode)
128
- str += `Approval Mode: ${approvalMode}\n`;
129
- if (config) {
130
- try {
131
- str += `Config: ${JSON.stringify(config)}\n`;
132
- }
133
- catch {
134
- str += `Config: ${String(config)}\n`;
135
- }
136
- }
137
- if (systemPrompt)
138
- str += `\n[ System Prompt ]\n${systemPrompt}\n`;
139
- if (inputPrompt)
140
- str += `\n[ Input Prompt ]\n${inputPrompt}\n`;
141
- if (Object.keys(rest).length)
142
- str += `\n[ Other Meta Options ]\n${JSON.stringify(rest, null, 2)}`;
143
- inputData = str;
144
- }
145
- catch (err) {
146
- inputData = `Failed to parse metadata: ${err.message}\nRaw JSON:\n${latestAttempt.metaJson}`;
147
- }
148
- }
149
- let outputData = "No output text available yet.";
150
- if (isGlobal) {
151
- if (runData.errorJson) {
152
- outputData = `ERROR:\n${runData.errorJson}`;
153
- }
154
- else if (runData.status === "finished") {
155
- outputData = "Run completed successfully (no global errorJson stacktrace was captured).\n\nPress [Enter] to inspect individual task payloads.";
156
- }
157
- else if (runData.status === "failed") {
158
- outputData = "Workflow failed (no global errorJson stacktrace was captured).\n\nPress [Enter] to inspect and determine which individual task node crashed.";
159
- }
160
- else {
161
- outputData = "Workflow is still active or pending execution...";
162
- }
163
- }
164
- else {
165
- if (latestAttempt?.errorJson) {
166
- outputData = `ERROR:\n${latestAttempt.errorJson}`;
167
- }
168
- else if (rawOutput) {
169
- try {
170
- const cleanOutput = { ...rawOutput };
171
- delete cleanOutput.run_id;
172
- delete cleanOutput.node_id;
173
- delete cleanOutput.iteration;
174
- // Attempt to parse internal stringified JSON fields for display
175
- for (const [k, v] of Object.entries(cleanOutput)) {
176
- if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
177
- try {
178
- cleanOutput[k] = JSON.parse(v);
179
- }
180
- catch { }
181
- }
182
- }
183
- outputData = JSON.stringify(cleanOutput);
184
- }
185
- catch {
186
- outputData = JSON.stringify(rawOutput);
187
- }
188
- }
189
- else {
190
- outputData = latestAttempt?.responseText ?? "No output text available yet.";
191
- }
192
- }
193
- let tokensStr = "";
194
- if (!isGlobal && nodeId) {
195
- const usageEvent = events.find((e) => e.type === "TokenUsageReported" && e.nodeId === nodeId && e.attempt === latestAttempt?.attempt);
196
- if (usageEvent) {
197
- try {
198
- const payload = JSON.parse(usageEvent.payloadJson);
199
- tokensStr = ` \n Tokens: ${payload.inputTokens} IN | ${payload.outputTokens} OUT | ${payload.cacheReadTokens ?? 0} CACHE`;
200
- }
201
- catch { }
202
- }
203
- }
204
- else if (isGlobal) {
205
- let tIn = 0, tOut = 0, tCache = 0;
206
- for (const e of events) {
207
- if (e.type === "TokenUsageReported") {
208
- try {
209
- const p = JSON.parse(e.payloadJson);
210
- tIn += (p.inputTokens ?? 0);
211
- tOut += (p.outputTokens ?? 0);
212
- tCache += (p.cacheReadTokens ?? 0);
213
- }
214
- catch { }
215
- }
216
- }
217
- tokensStr = ` \n Total Run Tokens: ${tIn} IN | ${tOut} OUT | ${tCache} CACHE`;
218
- }
219
- let bodyContent = null;
220
- if (tab === "input") {
221
- bodyContent = (<scrollbox style={{ width: "100%", height: "100%", paddingLeft: 1 }}>
222
- <text>{safePretty(inputData)}</text>
223
- </scrollbox>);
224
- }
225
- else if (tab === "output") {
226
- bodyContent = (<box style={{ width: "100%", height: "100%", flexDirection: "column" }}>
227
- <scrollbox style={{ flexGrow: 1, width: "100%", paddingLeft: 1 }}>
228
- <text>{safePretty(outputData)}</text>
229
- </scrollbox>
230
- {tokensStr && (<box style={{ width: "100%", height: 2, borderTop: true, borderColor: "gray" }}>
231
- <text style={{ color: "yellow" }}>{tokensStr}</text>
232
- </box>)}
233
- </box>);
234
- }
235
- else if (tab === "frames") {
236
- bodyContent = (<FramesPane adapter={adapter} runId={runId} focused={true} filterNodeId={nodeId ?? undefined} nodeAttempt={latestAttempt ?? undefined}/>);
237
- }
238
- else if (tab === "chat") {
239
- bodyContent = (<ChatPane adapter={adapter} runId={runId} focused={true} filterNodeId={nodeId ?? undefined}/>);
240
- }
241
- else if (tab === "logs") {
242
- bodyContent = (<LogsPane adapter={adapter} runId={runId} focused={true}/>);
243
- }
244
- else if (tab === "scores") {
245
- let scoresText = "No scorer results available.";
246
- if (scorerResults.length > 0) {
247
- const lines = scorerResults.map((r) => {
248
- const scoreVal = typeof r.score === "number" ? r.score.toFixed(2) : String(r.score);
249
- return ` ${r.scorerName ?? r.scorer_name ?? "unknown"}: ${scoreVal} ${r.reason ?? ""}`;
250
- });
251
- scoresText = `Scorer Results (${scorerResults.length}):\n\n${lines.join("\n")}`;
252
- }
253
- bodyContent = (<scrollbox style={{ width: "100%", height: "100%", paddingLeft: 1 }}>
254
- <text>{scoresText}</text>
255
- </scrollbox>);
256
- }
257
- /**
258
- * @param {string} num
259
- * @param {string} label
260
- * @param {string} expectedTab
261
- */
262
- const getTabLabel = (num, label, expectedTab) => {
263
- return tab === expectedTab ? `[(${num}) ${label}]` : ` (${num}) ${label} `;
264
- };
265
- const header = `Task Inspector: ${isGlobal ? "Entire Run" : nodeId}`;
266
- const tabs = `${getTabLabel("1", "Input", "input")} | ${getTabLabel("2", "Output", "output")} | ${getTabLabel("3", "Frames", "frames")} | ${getTabLabel("4", "Chat", "chat")} | ${getTabLabel("5", "Logs", "logs")} | ${getTabLabel("6", "Scores", "scores")}`;
267
- const escLabel = !isGlobal && latestAttempt ? "[R] Revert State | [Esc] Back" : "[Esc] Back";
268
- try {
269
- return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "column" }}>
270
- <box style={{ width: "100%", height: 3, border: true, borderColor: "#3b82f6", flexDirection: "row", justifyContent: "space-between" }}>
271
- <text style={{ color: "white", paddingLeft: 1, fontWeight: "bold" }}>{header}</text>
272
- <text style={{ color: "#93c5fd", paddingRight: 1 }}>{tabs} {escLabel}</text>
273
- </box>
274
-
275
- <box style={{ flexGrow: 1, width: "100%", border: true, borderColor: "#60a5fa", flexDirection: "column" }}>
276
- {bodyContent}
277
- </box>
278
- </box>);
279
- }
280
- catch (err) {
281
- require("fs").writeFileSync("/tmp/tui-crash.log", err?.stack || err?.message || String(err));
282
- throw err;
283
- }
284
- }
@@ -1,7 +0,0 @@
1
- import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
- export declare function NodeDetailView({ adapter, runId, nodeId, onBack, }: {
3
- adapter: SmithersDb;
4
- runId: string;
5
- nodeId: string | null;
6
- onBack: () => void;
7
- }): import("react/jsx-runtime").JSX.Element;
@@ -1,51 +0,0 @@
1
- // @ts-nocheck
2
- import React, { useEffect, useState } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- import { ChatPane } from "./ChatPane.jsx";
5
- /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
6
-
7
- /**
8
- * @param {{ adapter: SmithersDb; runId: string; node: any; onClose: () => void; }} value
9
- */
10
- export function NodeInspector({ adapter, runId, node, onClose, }) {
11
- const [tab, setTab] = useState("snapshot");
12
- useKeyboard((key) => {
13
- if (key.name === "escape" || (key.name === "c" && key.ctrl) || key.name === "backspace") {
14
- onClose();
15
- }
16
- if (key.name === "1")
17
- setTab("snapshot");
18
- if (key.name === "2")
19
- setTab("chat");
20
- });
21
- return (<box style={{
22
- width: "90%",
23
- height: "90%",
24
- border: true,
25
- borderColor: "magenta",
26
- position: "absolute",
27
- top: "5%",
28
- left: "5%",
29
- flexDirection: "column",
30
- backgroundColor: "black",
31
- }} title={`[Esc to Close] Node Inspector: ${node.nodeId} | ${tab === "snapshot" ? "[(1) Snapshot] (2) Chat" : " (1) Snapshot [(2) Chat]"}`}>
32
- {tab === "snapshot" ? (<scrollbox style={{ width: "100%", height: "100%", flexDirection: "column", padding: 1 }}>
33
- <text style={{ color: "yellow" }}>
34
- <strong>Input Data:</strong>
35
- </text>
36
- <text>{node.inputData ? JSON.stringify(JSON.parse(node.inputData), null, 2) : "None"}</text>
37
- <box style={{ height: 1 }}/>
38
- <text style={{ color: "yellow" }}>
39
- <strong>Output Data:</strong>
40
- </text>
41
- <text>{node.outputData ? JSON.stringify(JSON.parse(node.outputData), null, 2) : "None"}</text>
42
- <box style={{ height: 1 }}/>
43
- <text style={{ color: "yellow" }}>
44
- <strong>Metadata:</strong>
45
- </text>
46
- <text>{`Iteration: ${node.iteration} | Attempts: ${node.attempts ?? 0} | State: ${node.state}`}</text>
47
- </scrollbox>) : (<box style={{ flexGrow: 1, width: "100%", height: "100%" }}>
48
- <ChatPane adapter={adapter} runId={runId} focused={true} filterNodeId={node.nodeId}/>
49
- </box>)}
50
- </box>);
51
- }
@@ -1,7 +0,0 @@
1
- import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
- export declare function NodeInspector({ adapter, runId, node, onClose, }: {
3
- adapter: SmithersDb;
4
- runId: string;
5
- node: any;
6
- onClose: () => void;
7
- }): import("react/jsx-runtime").JSX.Element;
@@ -1,190 +0,0 @@
1
- // @ts-nocheck
2
- import React, { useEffect, useState } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
5
-
6
- /**
7
- * @param {{ adapter: SmithersDb; runId: string; onBack: () => void; onSelectNode: (nodeId: string | null) => void; }} value
8
- */
9
- export function RunDetailView({ adapter, runId, onBack, onSelectNode, }) {
10
- const [runData, setRunData] = useState(null);
11
- const [nodes, setNodes] = useState([]);
12
- const [attempts, setAttempts] = useState([]);
13
- const [events, setEvents] = useState([]);
14
- const [branchInfo, setBranchInfo] = useState(null);
15
- const [childBranches, setChildBranches] = useState([]);
16
- const [selectedIndex, setSelectedIndex] = useState(0); // 0 = Run itself, 1+ = nodes
17
- useEffect(() => {
18
- let mounted = true;
19
- let timeout;
20
- async function fetchDetails() {
21
- if (!mounted)
22
- return;
23
- try {
24
- const run = await adapter.getRun(runId);
25
- const fetchedNodes = await adapter.listNodes(runId);
26
- const fetchedAttempts = await adapter.listAttemptsForRun(runId);
27
- const fetchedEvents = await adapter.listEvents(runId, -1, 10000);
28
- // Time Travel: fetch branch info
29
- let fetchedBranchInfo = null;
30
- let fetchedChildBranches = [];
31
- try {
32
- const { getBranchInfo, listBranches } = await import("../../../time-travel/fork.js");
33
- fetchedBranchInfo = await getBranchInfo(adapter, runId) ?? null;
34
- fetchedChildBranches = await listBranches(adapter, runId) ?? [];
35
- }
36
- catch { }
37
- if (mounted) {
38
- setRunData(run);
39
- setNodes(fetchedNodes);
40
- setAttempts(fetchedAttempts);
41
- setEvents(fetchedEvents);
42
- setBranchInfo(fetchedBranchInfo);
43
- setChildBranches(fetchedChildBranches);
44
- setSelectedIndex((prev) => Math.min(prev, fetchedNodes.length));
45
- }
46
- }
47
- catch (err) { }
48
- if (mounted)
49
- timeout = setTimeout(fetchDetails, 1000);
50
- }
51
- fetchDetails();
52
- return () => {
53
- mounted = false;
54
- clearTimeout(timeout);
55
- };
56
- }, [adapter, runId]);
57
- useKeyboard((key) => {
58
- if (key.name === "escape" || (key.name === "c" && key.ctrl) || key.name === "backspace") {
59
- onBack();
60
- return;
61
- }
62
- if (key.name === "down" || key.name === "j") {
63
- setSelectedIndex((s) => Math.min(s + 1, nodes.length));
64
- }
65
- if (key.name === "up" || key.name === "k") {
66
- setSelectedIndex((s) => Math.max(0, s - 1));
67
- }
68
- if (key.name === "enter" || key.name === "return") {
69
- onSelectNode(selectedIndex === 0 ? null : nodes[selectedIndex - 1]?.nodeId);
70
- }
71
- if (key.name === "h" && selectedIndex > 0) {
72
- const targetNode = nodes[selectedIndex - 1]?.nodeId;
73
- if (targetNode && (runData?.status === "running" || runData?.status === "waiting-approval")) {
74
- Bun.spawn([
75
- "smithers-ctl",
76
- "terminal",
77
- "--cwd",
78
- process.cwd(),
79
- "--command",
80
- `bun run src/index.js hijack ${runId} --target ${targetNode}`,
81
- ], { stdout: "ignore", stderr: "ignore" }).unref();
82
- }
83
- }
84
- });
85
- if (!runData) {
86
- return <text style={{ margin: 1 }}>Loading run details for {runId}...</text>;
87
- }
88
- const isGlobal = selectedIndex === 0;
89
- const selectedNode = isGlobal ? null : nodes[selectedIndex - 1];
90
- let outputData = "No output text available yet.";
91
- if (isGlobal) {
92
- if (runData.errorJson) {
93
- outputData = `ERROR:\n${runData.errorJson}`;
94
- }
95
- else if (runData.status === "finished") {
96
- outputData = "Run completed successfully (no global errorJson stacktrace was captured).\n\nPress [Enter] to inspect individual task payloads.";
97
- }
98
- else if (runData.status === "failed") {
99
- outputData = "Workflow failed (no global errorJson stacktrace was captured).\n\nPress [Enter] to inspect and determine which individual task node crashed.";
100
- }
101
- else {
102
- outputData = "Workflow is still active or pending execution...";
103
- }
104
- }
105
- else {
106
- const nodeAttempts = attempts.filter((a) => a.nodeId === selectedNode?.nodeId).sort((a, b) => b.attempt - a.attempt);
107
- const latestAttempt = nodeAttempts[0];
108
- outputData = latestAttempt?.errorJson ? `ERROR:\n${latestAttempt.errorJson}` : latestAttempt?.responseText ?? "No output text available yet.";
109
- }
110
- let truncatedOutput = outputData;
111
- if (truncatedOutput.length > 500) {
112
- const head = truncatedOutput.substring(0, 200);
113
- const tail = truncatedOutput.substring(truncatedOutput.length - 200);
114
- truncatedOutput = `${head}\n\n... [ TRUNCATED ${truncatedOutput.length - 400} CHARACTERS ] ...\n\n${tail}`;
115
- }
116
- // Calculate tokens
117
- let tokensStr = "";
118
- if (!isGlobal && selectedNode) {
119
- const usageAttempts = attempts.filter((a) => a.nodeId === selectedNode.nodeId).sort((a, b) => b.attempt - a.attempt);
120
- const usageAttempt = usageAttempts[0];
121
- const usageEvent = events.find((e) => e.type === "TokenUsageReported" && e.nodeId === selectedNode.nodeId && e.attempt === usageAttempt?.attempt);
122
- if (usageEvent) {
123
- try {
124
- const payload = JSON.parse(usageEvent.payloadJson);
125
- tokensStr = `Tokens: ${payload.inputTokens} IN | ${payload.outputTokens} OUT | ${payload.cacheReadTokens ?? 0} CACHE`;
126
- }
127
- catch { }
128
- }
129
- }
130
- else if (isGlobal) {
131
- let tIn = 0, tOut = 0, tCache = 0;
132
- for (const e of events) {
133
- if (e.type === "TokenUsageReported") {
134
- try {
135
- const p = JSON.parse(e.payloadJson);
136
- tIn += (p.inputTokens ?? 0);
137
- tOut += (p.outputTokens ?? 0);
138
- tCache += (p.cacheReadTokens ?? 0);
139
- }
140
- catch { }
141
- }
142
- }
143
- tokensStr = `Total Run Tokens: ${tIn} IN | ${tOut} OUT | ${tCache} CACHE`;
144
- }
145
- return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "row" }}>
146
- {/* Left Sidebar: Nodes Tree */}
147
- <box style={{
148
- width: 40,
149
- height: "100%",
150
- border: true,
151
- borderColor: "#34d399",
152
- flexDirection: "column",
153
- }} title={`Run Tasks [Esc to Return]`}>
154
- <scrollbox style={{ width: "100%", height: "100%", flexDirection: "column", paddingLeft: 1 }}>
155
- <text style={{ color: isGlobal ? "green" : "white" }}>
156
- {isGlobal ? "▶ " : " "}[ Entire Run ]
157
- </text>
158
- {nodes.map((node, i) => {
159
- const isSelected = selectedIndex === i + 1;
160
- return (<text key={`${node.runId}-${node.nodeId}-${node.iteration}`} style={{ color: isSelected ? "green" : "white" }}>
161
- {isSelected ? "▶ " : " "}{node.nodeId}: {node.state}
162
- </text>);
163
- })}
164
- </scrollbox>
165
- </box>
166
-
167
- {/* Right Area: Preview Pane */}
168
- <box style={{
169
- flexGrow: 1,
170
- height: "100%",
171
- border: true,
172
- borderColor: "#4bc5a3",
173
- flexDirection: "column",
174
- paddingLeft: 1
175
- }} title={`Preview: ${isGlobal ? "Entire Run" : selectedNode?.nodeId} [Hit Enter to Deep Inspect]`}>
176
- <text style={{ color: "yellow" }}>State: {isGlobal ? runData.status : selectedNode?.state}</text>
177
- {isGlobal && branchInfo && (<text style={{ color: "magenta" }}>
178
- Fork: from {branchInfo.parentRunId?.slice(0, 12)} frame {branchInfo.parentFrameNo}{branchInfo.branchLabel ? ` [${branchInfo.branchLabel}]` : ""}
179
- </text>)}
180
- {isGlobal && childBranches.length > 0 && (<text style={{ color: "cyan" }}>
181
- Branches: {childBranches.length} fork{childBranches.length !== 1 ? "s" : ""} ({childBranches.map((b) => b.branchLabel || b.runId?.slice(0, 8)).join(", ")})
182
- </text>)}
183
- {tokensStr && <text style={{ color: "cyan" }}>{tokensStr}</text>}
184
- <text style={{ color: "gray", marginTop: 1 }}>--- Terminal Output Snippet ---</text>
185
- <scrollbox style={{ flexGrow: 1, width: "100%", marginTop: 1 }}>
186
- <text>{truncatedOutput}</text>
187
- </scrollbox>
188
- </box>
189
- </box>);
190
- }
@@ -1,7 +0,0 @@
1
- import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
- export declare function RunDetailView({ adapter, runId, onBack, onSelectNode, }: {
3
- adapter: SmithersDb;
4
- runId: string;
5
- onBack: () => void;
6
- onSelectNode: (nodeId: string | null) => void;
7
- }): import("react/jsx-runtime").JSX.Element;