@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.
- package/package.json +17 -19
- package/src/agent-detection.js +2 -2
- package/src/event-categories.js +5 -0
- package/src/find-db.js +6 -6
- package/src/index.js +117 -59
- package/src/mcp/semantic-tools.js +1 -2
- package/src/node-detail.js +1 -6
- package/src/watch.js +1 -2
- package/src/why-diagnosis.js +1 -2
- package/src/workflow-pack.js +23 -11
- package/src/tui/app.jsx +0 -139
- package/src/tui/app.tsx +0 -5
- package/src/tui/components/AskModal.jsx +0 -109
- package/src/tui/components/AskModal.tsx +0 -3
- package/src/tui/components/AttentionPane.jsx +0 -112
- package/src/tui/components/AttentionPane.tsx +0 -6
- package/src/tui/components/ChatPane.jsx +0 -57
- package/src/tui/components/ChatPane.tsx +0 -7
- package/src/tui/components/CronList.jsx +0 -87
- package/src/tui/components/CronList.tsx +0 -5
- package/src/tui/components/DetailsPane.jsx +0 -96
- package/src/tui/components/DetailsPane.tsx +0 -7
- package/src/tui/components/FramesPane.jsx +0 -147
- package/src/tui/components/FramesPane.tsx +0 -8
- package/src/tui/components/LogsPane.jsx +0 -46
- package/src/tui/components/LogsPane.tsx +0 -6
- package/src/tui/components/MetricsPane.jsx +0 -108
- package/src/tui/components/MetricsPane.tsx +0 -5
- package/src/tui/components/NodeDetailView.jsx +0 -284
- package/src/tui/components/NodeDetailView.tsx +0 -7
- package/src/tui/components/NodeInspector.jsx +0 -51
- package/src/tui/components/NodeInspector.tsx +0 -7
- package/src/tui/components/RunDetailView.jsx +0 -190
- package/src/tui/components/RunDetailView.tsx +0 -7
- package/src/tui/components/RunsList.jsx +0 -184
- package/src/tui/components/RunsList.tsx +0 -7
- package/src/tui/components/SqliteBrowser.jsx +0 -131
- package/src/tui/components/SqliteBrowser.tsx +0 -5
- package/src/tui/components/WorkflowLauncher.jsx +0 -63
- 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, '"')}"`;
|
|
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,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,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
|
-
}
|