@smithers-orchestrator/cli 0.16.0

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 (110) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +55 -0
  3. package/src/AgentAvailability.ts +13 -0
  4. package/src/AgentAvailabilityStatus.ts +5 -0
  5. package/src/AggregateNodeDetailParams.ts +5 -0
  6. package/src/AskOptions.ts +12 -0
  7. package/src/ChatAttemptMeta.ts +7 -0
  8. package/src/ChatAttemptRow.ts +12 -0
  9. package/src/ChatOutputEvent.ts +6 -0
  10. package/src/DiffBundleLike.ts +6 -0
  11. package/src/DiscoveredWorkflow.ts +9 -0
  12. package/src/EnrichedNodeDetail.ts +60 -0
  13. package/src/EventCategory.ts +18 -0
  14. package/src/FindDbWaitOptions.ts +4 -0
  15. package/src/FormatEventLineOptions.ts +4 -0
  16. package/src/HijackCandidate.ts +11 -0
  17. package/src/HijackLaunchSpec.ts +6 -0
  18. package/src/InitWorkflowPackOptions.ts +4 -0
  19. package/src/InitWorkflowPackResult.ts +6 -0
  20. package/src/NativeHijackEngine.ts +8 -0
  21. package/src/NodeDetailAttempt.ts +22 -0
  22. package/src/NodeDetailTokenUsage.ts +11 -0
  23. package/src/NodeDetailToolCall.ts +12 -0
  24. package/src/ParsedNodeOutputEvent.ts +9 -0
  25. package/src/RenderNodeDetailOptions.ts +4 -0
  26. package/src/RunAutoResumeSkipReason.ts +4 -0
  27. package/src/RunDiffCommandInput.ts +13 -0
  28. package/src/RunDiffCommandResult.ts +3 -0
  29. package/src/RunOutputCommandInput.ts +12 -0
  30. package/src/RunOutputCommandResult.ts +3 -0
  31. package/src/RunRewindCommandInput.ts +14 -0
  32. package/src/RunRewindCommandResult.ts +3 -0
  33. package/src/RunTreeCommandInput.ts +14 -0
  34. package/src/RunTreeCommandResult.ts +3 -0
  35. package/src/SmithersEventType.ts +3 -0
  36. package/src/SupervisorOptions.ts +33 -0
  37. package/src/SupervisorPollSummary.ts +6 -0
  38. package/src/TreeRenderOptions.ts +5 -0
  39. package/src/WatchLoopOptions.ts +9 -0
  40. package/src/WatchLoopResult.ts +8 -0
  41. package/src/WatchRenderContext.ts +4 -0
  42. package/src/WhyBlocker.ts +17 -0
  43. package/src/WhyBlockerKind.ts +9 -0
  44. package/src/WhyDiagnosis.ts +10 -0
  45. package/src/WorkflowCta.ts +4 -0
  46. package/src/WorkflowSourceType.ts +1 -0
  47. package/src/agent-detection.js +257 -0
  48. package/src/ask.js +491 -0
  49. package/src/chat.js +226 -0
  50. package/src/diff.js +221 -0
  51. package/src/event-categories.js +141 -0
  52. package/src/find-db.js +93 -0
  53. package/src/format.js +272 -0
  54. package/src/hijack-session.js +207 -0
  55. package/src/hijack.js +226 -0
  56. package/src/index.d.ts +1 -0
  57. package/src/index.js +4868 -0
  58. package/src/mcp/SemanticMcpServerOptions.ts +4 -0
  59. package/src/mcp/SemanticToolCallResult.ts +14 -0
  60. package/src/mcp/SemanticToolContext.ts +6 -0
  61. package/src/mcp/SemanticToolDefinition.ts +13 -0
  62. package/src/mcp/SemanticToolError.ts +6 -0
  63. package/src/mcp/semantic-server.js +41 -0
  64. package/src/mcp/semantic-tools.js +1242 -0
  65. package/src/node-detail.js +682 -0
  66. package/src/output.js +111 -0
  67. package/src/resume-detached.js +37 -0
  68. package/src/rewind.js +88 -0
  69. package/src/scheduler.js +112 -0
  70. package/src/smithersRuntime.js +63 -0
  71. package/src/supervisor.js +418 -0
  72. package/src/tree.js +307 -0
  73. package/src/tui/app.jsx +139 -0
  74. package/src/tui/app.tsx +5 -0
  75. package/src/tui/components/AskModal.jsx +109 -0
  76. package/src/tui/components/AskModal.tsx +3 -0
  77. package/src/tui/components/AttentionPane.jsx +112 -0
  78. package/src/tui/components/AttentionPane.tsx +6 -0
  79. package/src/tui/components/ChatPane.jsx +57 -0
  80. package/src/tui/components/ChatPane.tsx +7 -0
  81. package/src/tui/components/CronList.jsx +87 -0
  82. package/src/tui/components/CronList.tsx +5 -0
  83. package/src/tui/components/DetailsPane.jsx +96 -0
  84. package/src/tui/components/DetailsPane.tsx +7 -0
  85. package/src/tui/components/FramesPane.jsx +147 -0
  86. package/src/tui/components/FramesPane.tsx +8 -0
  87. package/src/tui/components/LogsPane.jsx +46 -0
  88. package/src/tui/components/LogsPane.tsx +6 -0
  89. package/src/tui/components/MetricsPane.jsx +108 -0
  90. package/src/tui/components/MetricsPane.tsx +5 -0
  91. package/src/tui/components/NodeDetailView.jsx +284 -0
  92. package/src/tui/components/NodeDetailView.tsx +7 -0
  93. package/src/tui/components/NodeInspector.jsx +51 -0
  94. package/src/tui/components/NodeInspector.tsx +7 -0
  95. package/src/tui/components/RunDetailView.jsx +190 -0
  96. package/src/tui/components/RunDetailView.tsx +7 -0
  97. package/src/tui/components/RunsList.jsx +184 -0
  98. package/src/tui/components/RunsList.tsx +7 -0
  99. package/src/tui/components/SqliteBrowser.jsx +131 -0
  100. package/src/tui/components/SqliteBrowser.tsx +5 -0
  101. package/src/tui/components/WorkflowLauncher.jsx +63 -0
  102. package/src/tui/components/WorkflowLauncher.tsx +3 -0
  103. package/src/util/CliErrorMapping.ts +7 -0
  104. package/src/util/CliExitCode.ts +10 -0
  105. package/src/util/errorMessage.js +212 -0
  106. package/src/util/exitCodes.js +18 -0
  107. package/src/watch.js +128 -0
  108. package/src/why-diagnosis.js +1000 -0
  109. package/src/workflow-pack.js +2151 -0
  110. package/src/workflows.js +122 -0
@@ -0,0 +1,6 @@
1
+ import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
+ export declare function AttentionPane({ adapter, focused, onSelectRun, }: {
3
+ adapter: SmithersDb;
4
+ focused: boolean;
5
+ onSelectRun?: (runId: string) => void;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,57 @@
1
+ // @ts-nocheck
2
+ import React, { useEffect, useState } from "react";
3
+ import { formatChatBlock, parseChatAttemptMeta, selectChatAttempts } from "../../chat.js";
4
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
5
+
6
+ /**
7
+ * @param {{ adapter: SmithersDb; runId: string; focused: boolean; filterNodeId?: string; }} value
8
+ */
9
+ export function ChatPane({ adapter, runId, focused, filterNodeId, }) {
10
+ const [chatLines, setChatLines] = useState([]);
11
+ useEffect(() => {
12
+ let mounted = true;
13
+ async function fetchChat() {
14
+ if (!mounted)
15
+ return;
16
+ try {
17
+ const attempts = await adapter.listAttemptsForRun(runId);
18
+ let lines = [];
19
+ for (const attempt of attempts) {
20
+ if (filterNodeId && attempt.nodeId !== filterNodeId)
21
+ continue;
22
+ const meta = parseChatAttemptMeta(attempt.metaJson ?? "");
23
+ if (!meta.prompt && !attempt.responseText)
24
+ continue; // Skip empty attempts
25
+ if (lines.length > 0)
26
+ lines.push("");
27
+ lines.push(`=== ${attempt.nodeId} (Attempt ${attempt.attempt}, Iteration ${attempt.iteration}) ===`);
28
+ if (meta.prompt) {
29
+ lines.push(`[USER]`);
30
+ lines.push(...String(meta.prompt).trim().split("\n").map(l => ` ${l}`));
31
+ lines.push("");
32
+ }
33
+ if (attempt.responseText) {
34
+ lines.push(`[ASSISTANT]`);
35
+ lines.push(...String(attempt.responseText).trim().split("\n").map(l => ` ${l}`));
36
+ }
37
+ }
38
+ if (mounted) {
39
+ setChatLines(lines);
40
+ }
41
+ }
42
+ catch (err) { }
43
+ if (mounted)
44
+ setTimeout(fetchChat, 1000);
45
+ }
46
+ fetchChat();
47
+ return () => {
48
+ mounted = false;
49
+ };
50
+ }, [adapter, runId]);
51
+ return (<scrollbox focused={focused} style={{ width: "100%", height: "100%", paddingLeft: 1, paddingRight: 1 }}>
52
+ <box flexDirection="column">
53
+ {chatLines.map((line, index) => (<text key={index}>{line}</text>))}
54
+ {chatLines.length === 0 && <text>No chat history available.</text>}
55
+ </box>
56
+ </scrollbox>);
57
+ }
@@ -0,0 +1,7 @@
1
+ import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
+ export declare function ChatPane({ adapter, runId, focused, filterNodeId, }: {
3
+ adapter: SmithersDb;
4
+ runId: string;
5
+ focused: boolean;
6
+ filterNodeId?: string;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,87 @@
1
+ // @ts-nocheck
2
+ import React, { useEffect, useState } from "react";
3
+ import { useKeyboard } from "@opentui/react";
4
+ import { formatAge } from "../../format.js";
5
+ /** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
6
+
7
+ /**
8
+ * @param {{ adapter: SmithersDb; onBack: () => void; }} value
9
+ */
10
+ export function CronList({ adapter, onBack, }) {
11
+ const [crons, setCrons] = useState([]);
12
+ const [selectedIndex, setSelectedIndex] = useState(0);
13
+ useEffect(() => {
14
+ let mounted = true;
15
+ async function poll() {
16
+ try {
17
+ const jobs = await adapter.listCrons(false);
18
+ if (mounted)
19
+ setCrons(jobs);
20
+ }
21
+ catch { }
22
+ if (mounted)
23
+ setTimeout(poll, 2000);
24
+ }
25
+ poll();
26
+ return () => { mounted = false; };
27
+ }, [adapter]);
28
+ useKeyboard(async (key) => {
29
+ if (key.name === "escape") {
30
+ onBack();
31
+ return;
32
+ }
33
+ if (crons.length > 0) {
34
+ if (key.name === "down" || key.name === "j") {
35
+ setSelectedIndex(Math.min(crons.length - 1, selectedIndex + 1));
36
+ }
37
+ else if (key.name === "up" || key.name === "k") {
38
+ setSelectedIndex(Math.max(0, selectedIndex - 1));
39
+ }
40
+ else if (key.name === "backspace" || key.name === "delete") {
41
+ const id = crons[selectedIndex].cronId;
42
+ await adapter.deleteCron(id);
43
+ const next = await adapter.listCrons(false);
44
+ setCrons(next);
45
+ setSelectedIndex(Math.max(0, Math.min(selectedIndex, next.length - 1)));
46
+ }
47
+ }
48
+ });
49
+ const selectedJob = crons[selectedIndex];
50
+ return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "row" }}>
51
+ {/* Left List */}
52
+ <box style={{ width: 45, height: "100%", borderRight: true, borderColor: "#34d399", flexDirection: "column" }}>
53
+ <text style={{ color: "gray", marginBottom: 1 }}> Active Cron Triggers: {crons.length} </text>
54
+ {crons.map((c, i) => (<text key={c.cronId} style={{ color: i === selectedIndex ? "#a7f3d0" : "white" }}>
55
+ {i === selectedIndex ? "▶ " : " "}{c.workflowPath.slice(0, 30)}
56
+ </text>))}
57
+ </box>
58
+
59
+ {/* Right Details */}
60
+ <box style={{ flexGrow: 1, height: "100%", flexDirection: "column", paddingLeft: 1 }}>
61
+ {selectedJob ? (<box style={{ flexDirection: "column" }}>
62
+ <text style={{ color: "yellow" }}> Workflow: {selectedJob.workflowPath} </text>
63
+ <text style={{ color: "#93c5fd" }}> Setup: {selectedJob.pattern} </text>
64
+ <text style={{ color: "white", marginTop: 1 }}>
65
+ Status: {selectedJob.enabled ? "ACTIVE" : "PAUSED"}
66
+ </text>
67
+ <text style={{ color: "white" }}>
68
+ Registered: {formatAge(selectedJob.createdAtMs)}
69
+ </text>
70
+ <text style={{ color: "white" }}>
71
+ Last Pired: {selectedJob.lastRunAtMs ? formatAge(selectedJob.lastRunAtMs) : "Never"}
72
+ </text>
73
+ <text style={{ color: "cyan" }}>
74
+ Next Fire: {selectedJob.nextRunAtMs ? formatAge(selectedJob.nextRunAtMs) : "Pending"}
75
+ </text>
76
+
77
+ <box style={{ marginTop: 2, flexDirection: "column", borderTop: true, borderColor: "gray", paddingTop: 1 }}>
78
+ <text style={{ color: "red" }}>[Backspace] Kill Trigger</text>
79
+ </box>
80
+
81
+ {selectedJob.errorJson && (<text style={{ color: "red", marginTop: 1 }}>
82
+ Last Error: {selectedJob.errorJson}
83
+ </text>)}
84
+ </box>) : (<text style={{ color: "gray" }}>No schedules found. Run `smithers cron add`.</text>)}
85
+ </box>
86
+ </box>);
87
+ }
@@ -0,0 +1,5 @@
1
+ import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
+ export declare function CronList({ adapter, onBack, }: {
3
+ adapter: SmithersDb;
4
+ onBack: () => void;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,96 @@
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; onInspectNode?: (node: any) => void; }} value
8
+ */
9
+ export function DetailsPane({ adapter, runId, focused, onInspectNode, }) {
10
+ const [nodes, setNodes] = useState([]);
11
+ const [runData, setRunData] = useState(null);
12
+ const [selectedIndex, setSelectedIndex] = useState(0);
13
+ useEffect(() => {
14
+ let mounted = true;
15
+ let timeout;
16
+ async function fetchDetails() {
17
+ if (!mounted)
18
+ return;
19
+ try {
20
+ const run = await adapter.getRun(runId);
21
+ const fetchedNodes = await adapter.listNodes(runId);
22
+ if (mounted) {
23
+ setRunData(run);
24
+ setNodes(fetchedNodes);
25
+ setSelectedIndex((prev) => Math.min(prev, Math.max(0, fetchedNodes.length - 1)));
26
+ }
27
+ }
28
+ catch (err) { }
29
+ if (mounted)
30
+ timeout = setTimeout(fetchDetails, 1000);
31
+ }
32
+ fetchDetails();
33
+ return () => {
34
+ mounted = false;
35
+ clearTimeout(timeout);
36
+ };
37
+ }, [adapter, runId]);
38
+ useKeyboard((key) => {
39
+ if (!focused)
40
+ return;
41
+ if (key.name === "down" || key.name === "j") {
42
+ setSelectedIndex((s) => Math.min(s + 1, Math.max(0, nodes.length - 1)));
43
+ }
44
+ if (key.name === "up" || key.name === "k") {
45
+ setSelectedIndex((s) => Math.max(0, s - 1));
46
+ }
47
+ if (key.name === "enter" || key.name === "return") {
48
+ if (nodes[selectedIndex] && onInspectNode) {
49
+ onInspectNode(nodes[selectedIndex]);
50
+ }
51
+ }
52
+ });
53
+ if (!runData) {
54
+ return <text style={{ margin: 1 }}>Loading details...</text>;
55
+ }
56
+ return (<scrollbox focused={focused} style={{ width: "100%", height: "100%", padding: 1 }}>
57
+ <box flexDirection="column">
58
+ <text>
59
+ <strong>Status:</strong>{" "}
60
+ <span fg={runData.status === "finished"
61
+ ? "green"
62
+ : runData.status === "failed"
63
+ ? "red"
64
+ : runData.status === "running"
65
+ ? "#34d399"
66
+ : "yellow"}>
67
+ {runData.status}
68
+ </span>
69
+ </text>
70
+ <text>
71
+ <strong>Input Payload:</strong>
72
+ </text>
73
+ <text>{runData.inputData ? runData.inputData.substring(0, 100) + "..." : "None"}</text>
74
+
75
+ {runData.outputData && (<>
76
+ <box style={{ height: 1 }}/>
77
+ <text>
78
+ <strong>Final Output:</strong>
79
+ </text>
80
+ <text>{runData.outputData.substring(0, 100) + "..."}</text>
81
+ </>)}
82
+
83
+ <box style={{ height: 1 }}/>
84
+ <text style={{ color: focused ? "yellow" : "white" }}>
85
+ <strong>Nodes [Press Enter on Node to Inspect]:</strong>
86
+ </text>
87
+
88
+ {nodes.map((node, i) => {
89
+ const isSelected = focused && selectedIndex === i;
90
+ return (<text key={`${node.runId}-${node.nodeId}-${node.iteration}`} style={{ color: isSelected ? "green" : "white" }}>
91
+ {isSelected ? "▶ " : " "}{node.nodeId}: {node.state} (Att: {node.attempts ?? 0}, Iter: {node.iteration})
92
+ </text>);
93
+ })}
94
+ </box>
95
+ </scrollbox>);
96
+ }
@@ -0,0 +1,7 @@
1
+ import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
+ export declare function DetailsPane({ adapter, runId, focused, onInspectNode, }: {
3
+ adapter: SmithersDb;
4
+ runId: string;
5
+ focused: boolean;
6
+ onInspectNode?: (node: any) => void;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,147 @@
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
+ }
@@ -0,0 +1,8 @@
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;
@@ -0,0 +1,46 @@
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
+ }
@@ -0,0 +1,6 @@
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;
@@ -0,0 +1,108 @@
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
+ }
@@ -0,0 +1,5 @@
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;