@smithers-orchestrator/cli 0.20.1 → 0.20.4

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/package.json +17 -19
  2. package/src/agent-detection.js +2 -2
  3. package/src/event-categories.js +5 -0
  4. package/src/find-db.js +6 -6
  5. package/src/index.js +117 -59
  6. package/src/mcp/semantic-tools.js +1 -2
  7. package/src/node-detail.js +1 -6
  8. package/src/watch.js +1 -2
  9. package/src/why-diagnosis.js +1 -2
  10. package/src/workflow-pack.js +23 -11
  11. package/src/tui/app.jsx +0 -139
  12. package/src/tui/app.tsx +0 -5
  13. package/src/tui/components/AskModal.jsx +0 -109
  14. package/src/tui/components/AskModal.tsx +0 -3
  15. package/src/tui/components/AttentionPane.jsx +0 -112
  16. package/src/tui/components/AttentionPane.tsx +0 -6
  17. package/src/tui/components/ChatPane.jsx +0 -57
  18. package/src/tui/components/ChatPane.tsx +0 -7
  19. package/src/tui/components/CronList.jsx +0 -87
  20. package/src/tui/components/CronList.tsx +0 -5
  21. package/src/tui/components/DetailsPane.jsx +0 -96
  22. package/src/tui/components/DetailsPane.tsx +0 -7
  23. package/src/tui/components/FramesPane.jsx +0 -147
  24. package/src/tui/components/FramesPane.tsx +0 -8
  25. package/src/tui/components/LogsPane.jsx +0 -46
  26. package/src/tui/components/LogsPane.tsx +0 -6
  27. package/src/tui/components/MetricsPane.jsx +0 -108
  28. package/src/tui/components/MetricsPane.tsx +0 -5
  29. package/src/tui/components/NodeDetailView.jsx +0 -284
  30. package/src/tui/components/NodeDetailView.tsx +0 -7
  31. package/src/tui/components/NodeInspector.jsx +0 -51
  32. package/src/tui/components/NodeInspector.tsx +0 -7
  33. package/src/tui/components/RunDetailView.jsx +0 -190
  34. package/src/tui/components/RunDetailView.tsx +0 -7
  35. package/src/tui/components/RunsList.jsx +0 -184
  36. package/src/tui/components/RunsList.tsx +0 -7
  37. package/src/tui/components/SqliteBrowser.jsx +0 -131
  38. package/src/tui/components/SqliteBrowser.tsx +0 -5
  39. package/src/tui/components/WorkflowLauncher.jsx +0 -63
  40. package/src/tui/components/WorkflowLauncher.tsx +0 -3
@@ -1,147 +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; focused: boolean; filterNodeId?: string; nodeAttempt?: any; }} value
8
- */
9
- export function FramesPane({ adapter, runId, focused, filterNodeId, nodeAttempt, }) {
10
- const [frames, setFrames] = useState([]);
11
- const [selectedIndex, setSelectedIndex] = useState(0);
12
- useEffect(() => {
13
- let mounted = true;
14
- async function fetchFrames() {
15
- if (!mounted)
16
- return;
17
- try {
18
- // limit(500) to ensure we get a chunk safely, reverse to ASCENDING chronological order
19
- const data = await adapter.listFrames(runId, 500);
20
- if (mounted && data.length > 0) {
21
- const ascData = data.slice().reverse();
22
- setFrames(ascData);
23
- if (frames.length === 0) {
24
- setSelectedIndex(ascData.length - 1); // default to newest frame (bottom)
25
- }
26
- }
27
- }
28
- catch (err) { }
29
- if (mounted)
30
- setTimeout(fetchFrames, 2000); // Polling for live updates
31
- }
32
- fetchFrames();
33
- return () => {
34
- mounted = false;
35
- };
36
- }, [adapter, runId]);
37
- let displayFrames = frames;
38
- if (filterNodeId && nodeAttempt && frames.length > 0) {
39
- const sMs = nodeAttempt.startedAtMs ?? 0;
40
- const fMs = nodeAttempt.finishedAtMs;
41
- const beforeF = frames.slice().reverse().find(f => f.createdAtMs <= sMs) || frames[0];
42
- const afterF = fMs ? (frames.find(f => f.createdAtMs >= fMs) || frames[frames.length - 1]) : frames[frames.length - 1];
43
- displayFrames = frames.map(f => {
44
- if (f.frameNo === beforeF.frameNo && f.frameNo === afterF.frameNo) {
45
- return { ...f, uiLabel: "Frame (Active)" };
46
- }
47
- if (f.frameNo === beforeF.frameNo)
48
- return { ...f, uiLabel: "Frame (Before)" };
49
- if (f.frameNo === afterF.frameNo)
50
- return { ...f, uiLabel: "Frame (After)" };
51
- return f;
52
- });
53
- }
54
- useKeyboard((key) => {
55
- if (!focused || displayFrames.length === 0)
56
- return;
57
- if (key.name === "up" || key.name === "k") {
58
- setSelectedIndex((s) => Math.max(0, s - 1));
59
- }
60
- if (key.name === "down" || key.name === "j") {
61
- setSelectedIndex((s) => Math.min(s + 1, displayFrames.length - 1));
62
- }
63
- });
64
- if (displayFrames.length === 0) {
65
- return <text style={{ paddingLeft: 1 }}>No frame history available...</text>;
66
- }
67
- // Bound check in case selectedIndex drifted
68
- const validIndex = Math.max(0, Math.min(selectedIndex, displayFrames.length - 1));
69
- const selectedFrame = displayFrames[validIndex];
70
- // Recursive formatter to convert `xmlJson` into a JSX code block
71
- /**
72
- * @param {any} node
73
- * @param {number} [indent]
74
- * @returns {string}
75
- */
76
- function formatJsxNode(node, indent = 0) {
77
- if (!node || typeof node !== "object")
78
- return "";
79
- const space = " ".repeat(indent);
80
- if (node.kind === "text") {
81
- const escaped = String(node.text || "").replace(/\n/g, `\n${space}`);
82
- return `${space}${escaped}`;
83
- }
84
- if (node.kind === "element" && typeof node.tag === "string") {
85
- let propsStr = "";
86
- if (node.props && typeof node.props === "object") {
87
- for (const [k, v] of Object.entries(node.props)) {
88
- if (typeof v === "string")
89
- propsStr += ` ${k}="${v.replace(/"/g, '&quot;')}"`;
90
- else
91
- propsStr += ` ${k}={${JSON.stringify(v)}}`;
92
- }
93
- }
94
- const tag = node.tag;
95
- const isTargetNode = filterNodeId && node.props?.id === filterNodeId;
96
- const colorPrefix = isTargetNode ? "👉 " : "";
97
- if (!Array.isArray(node.children) || node.children.length === 0) {
98
- return `${space}${colorPrefix}<${tag}${propsStr} />`;
99
- }
100
- let res = `${space}${colorPrefix}<${tag}${propsStr}>\n`;
101
- for (const child of node.children) {
102
- res += formatJsxNode(child, indent + 1) + "\n";
103
- }
104
- res += `${space}</${tag}>`;
105
- return res;
106
- }
107
- // Fallback if neither text nor element
108
- return `${space}${JSON.stringify(node)}`;
109
- }
110
- let xmlString = "Empty Frame";
111
- try {
112
- if (selectedFrame?.xmlJson) {
113
- const parsed = typeof selectedFrame.xmlJson === "string" ? JSON.parse(selectedFrame.xmlJson) : selectedFrame.xmlJson;
114
- xmlString = formatJsxNode(parsed);
115
- }
116
- }
117
- catch (err) {
118
- xmlString = `[Format Error: ${err?.message}]\n\n${selectedFrame?.xmlJson ?? "Parse error"}`;
119
- }
120
- return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "row" }}>
121
- {/* Left Sidebar: Frame List */}
122
- <box style={{
123
- width: 30,
124
- height: "100%",
125
- borderRight: true,
126
- borderColor: "#34d399",
127
- flexDirection: "column",
128
- }} title={`Timeline [Up/Down]`}>
129
- <scrollbox style={{ width: "100%", height: "100%", flexDirection: "column", paddingLeft: 1 }}>
130
- {displayFrames.map((frame, i) => {
131
- const isSelected = validIndex === i;
132
- const label = frame.uiLabel ? `${frame.uiLabel}` : `Frame ${frame.frameNo}`;
133
- return (<text key={frame.frameNo + label} style={{ color: isSelected ? "green" : "white" }}>
134
- {isSelected ? "▶ " : " "}{label}
135
- </text>);
136
- })}
137
- </scrollbox>
138
- </box>
139
-
140
- {/* Right Content: XML/JSX Dump */}
141
- <box style={{ flexGrow: 1, height: "100%", flexDirection: "column" }}>
142
- <scrollbox style={{ width: "100%", height: "100%", paddingLeft: 1 }}>
143
- <text style={{ color: "#d8b4e2" }}>{xmlString}</text>
144
- </scrollbox>
145
- </box>
146
- </box>);
147
- }
@@ -1,8 +0,0 @@
1
- import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
- export declare function FramesPane({ adapter, runId, focused, filterNodeId, nodeAttempt, }: {
3
- adapter: SmithersDb;
4
- runId: string;
5
- focused: boolean;
6
- filterNodeId?: string;
7
- nodeAttempt?: any;
8
- }): import("react/jsx-runtime").JSX.Element;
@@ -1,46 +0,0 @@
1
- // @ts-nocheck
2
- import React, { useEffect, useState } from "react";
3
- import { formatEventLine } from "../../format.js";
4
- /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
5
-
6
- /**
7
- * @param {{ adapter: SmithersDb; runId: string; focused: boolean; }} value
8
- */
9
- export function LogsPane({ adapter, runId, focused, }) {
10
- const [logs, setLogs] = useState([]);
11
- useEffect(() => {
12
- let mounted = true;
13
- let lastSeq = -1;
14
- async function fetchLogs() {
15
- if (!mounted)
16
- return;
17
- try {
18
- const events = await adapter.listEvents(runId, lastSeq, 200);
19
- if (mounted && events.length > 0) {
20
- const run = await adapter.getRun(runId);
21
- const baseMs = run?.startedAtMs ?? run?.createdAtMs ?? Date.now();
22
- const newLines = events.map((e) => formatEventLine(e, baseMs));
23
- lastSeq = events[events.length - 1].seq;
24
- setLogs((prev) => {
25
- const updated = [...prev, ...newLines];
26
- // keep the last 200 lines to avoid scrollbox lag
27
- return updated.slice(-200);
28
- });
29
- }
30
- }
31
- catch (err) { }
32
- if (mounted)
33
- setTimeout(fetchLogs, 500);
34
- }
35
- fetchLogs();
36
- return () => {
37
- mounted = false;
38
- };
39
- }, [adapter, runId]);
40
- return (<scrollbox focused={focused} style={{ width: "100%", height: "100%", paddingLeft: 1, paddingRight: 1 }}>
41
- <box flexDirection="column">
42
- {logs.map((log, index) => (<text key={index}>{log}</text>))}
43
- {logs.length === 0 && <text>Loading events...</text>}
44
- </box>
45
- </scrollbox>);
46
- }
@@ -1,6 +0,0 @@
1
- import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
- export declare function LogsPane({ adapter, runId, focused, }: {
3
- adapter: SmithersDb;
4
- runId: string;
5
- focused: boolean;
6
- }): import("react/jsx-runtime").JSX.Element;
@@ -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
- }