@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
package/src/tui/app.jsx
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState } from "react";
|
|
3
|
-
import { useKeyboard } from "@opentui/react";
|
|
4
|
-
import { RunsList } from "./components/RunsList.jsx";
|
|
5
|
-
import { WorkflowLauncher } from "./components/WorkflowLauncher.jsx";
|
|
6
|
-
import { RunDetailView } from "./components/RunDetailView.jsx";
|
|
7
|
-
import { NodeDetailView } from "./components/NodeDetailView.jsx";
|
|
8
|
-
import { AskModal } from "./components/AskModal.jsx";
|
|
9
|
-
import { SqliteBrowser } from "./components/SqliteBrowser.jsx";
|
|
10
|
-
import { CronList } from "./components/CronList.jsx";
|
|
11
|
-
import { MetricsPane } from "./components/MetricsPane.jsx";
|
|
12
|
-
/** @typedef {import("@smithers-orchestrator/db/adapter").SmithersDb} SmithersDb */
|
|
13
|
-
|
|
14
|
-
const TABS = [
|
|
15
|
-
{ id: "runs", label: "Runs", access: "r" },
|
|
16
|
-
{ id: "ask", label: "Agent Console", access: "a" },
|
|
17
|
-
{ id: "crons", label: "Triggers", access: "t" },
|
|
18
|
-
{ id: "metrics", label: "Telemetry", access: "m" },
|
|
19
|
-
{ id: "sqlite", label: "Data Grid", access: "s" },
|
|
20
|
-
];
|
|
21
|
-
/**
|
|
22
|
-
* @param {{ adapter: SmithersDb; onExit: () => void; }} value
|
|
23
|
-
*/
|
|
24
|
-
export function TuiApp({ adapter, onExit, }) {
|
|
25
|
-
const [view, setView] = useState("runs");
|
|
26
|
-
const [selectedRunId, setSelectedRunId] = useState(null);
|
|
27
|
-
const [selectedNodeId, setSelectedNodeId] = useState(null);
|
|
28
|
-
const activeTabId = (view === "detail" || view === "node" || view === "launcher") ? "runs" : view;
|
|
29
|
-
useKeyboard(async (key) => {
|
|
30
|
-
// Tab switching
|
|
31
|
-
if (key.name === "left" || key.name === "right") {
|
|
32
|
-
// only accept left/right at the root view so we don't clobber text inputs
|
|
33
|
-
if (view === "runs" || view === "crons" || view === "metrics") {
|
|
34
|
-
const currentIndex = TABS.findIndex((t) => t.id === activeTabId);
|
|
35
|
-
if (key.name === "right") {
|
|
36
|
-
const next = TABS[(currentIndex + 1) % TABS.length];
|
|
37
|
-
setView(next.id);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
const prev = TABS[(currentIndex - 1 + TABS.length) % TABS.length];
|
|
41
|
-
setView(prev.id);
|
|
42
|
-
}
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (key.name === "s" && view !== "sqlite" && view !== "ask") {
|
|
47
|
-
setView("sqlite");
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (key.name === "t" && view !== "crons" && view !== "ask") {
|
|
51
|
-
setView("crons");
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (key.name === "m" && view !== "metrics" && view !== "ask") {
|
|
55
|
-
setView("metrics");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (view === "runs") {
|
|
59
|
-
if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
|
|
60
|
-
onExit();
|
|
61
|
-
}
|
|
62
|
-
if (key.name === "n") {
|
|
63
|
-
setView("launcher");
|
|
64
|
-
}
|
|
65
|
-
if (key.name === "a") {
|
|
66
|
-
setView("ask");
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else if (view === "detail") {
|
|
70
|
-
if (key.name === "escape") {
|
|
71
|
-
setView("runs");
|
|
72
|
-
}
|
|
73
|
-
else if (key.name === "c" && key.ctrl) {
|
|
74
|
-
onExit();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
else if (view === "node") {
|
|
78
|
-
if (key.name === "escape") {
|
|
79
|
-
setView("detail");
|
|
80
|
-
}
|
|
81
|
-
else if (key.name === "c" && key.ctrl) {
|
|
82
|
-
onExit();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
else if (view === "launcher") {
|
|
86
|
-
if (key.name === "escape") {
|
|
87
|
-
setView("runs");
|
|
88
|
-
}
|
|
89
|
-
else if (key.name === "c" && key.ctrl) {
|
|
90
|
-
onExit();
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
else if (view === "ask") {
|
|
94
|
-
if (key.name === "escape") {
|
|
95
|
-
setView("runs");
|
|
96
|
-
}
|
|
97
|
-
else if (key.name === "c" && key.ctrl) {
|
|
98
|
-
onExit();
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
return (<box style={{ flexGrow: 1, width: "100%", height: "100%", flexDirection: "column" }}>
|
|
103
|
-
{/* Global Tab Header */}
|
|
104
|
-
<box style={{ width: "100%", height: 3, borderBottom: true, borderColor: "gray", flexDirection: "row", paddingLeft: 1 }}>
|
|
105
|
-
{TABS.map((tab) => {
|
|
106
|
-
const isActive = activeTabId === tab.id;
|
|
107
|
-
return (<text key={tab.id} style={{ color: isActive ? "#a7f3d0" : "gray", marginRight: 3 }}>
|
|
108
|
-
{isActive ? "▶ " : " "}[{tab.access.toUpperCase()}] {tab.label}
|
|
109
|
-
</text>);
|
|
110
|
-
})}
|
|
111
|
-
</box>
|
|
112
|
-
|
|
113
|
-
{view === "runs" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers Runs - [Enter] View Details | [N] New Run | [Esc] Exit">
|
|
114
|
-
<RunsList adapter={adapter} focused={view === "runs"} onChange={setSelectedRunId} onSubmit={(runId) => {
|
|
115
|
-
setSelectedRunId(runId);
|
|
116
|
-
setView("detail");
|
|
117
|
-
}}/>
|
|
118
|
-
</box>)}
|
|
119
|
-
|
|
120
|
-
{view === "detail" && selectedRunId && (<RunDetailView adapter={adapter} runId={selectedRunId} onBack={() => setView("runs")} onSelectNode={(nodeId) => {
|
|
121
|
-
setSelectedNodeId(nodeId);
|
|
122
|
-
setView("node");
|
|
123
|
-
}}/>)}
|
|
124
|
-
|
|
125
|
-
{view === "node" && selectedRunId && (<NodeDetailView adapter={adapter} runId={selectedRunId} nodeId={selectedNodeId} onBack={() => setView("detail")}/>)}
|
|
126
|
-
|
|
127
|
-
{view === "launcher" && (<WorkflowLauncher onClose={() => setView("runs")}/>)}
|
|
128
|
-
{view === "ask" && (<AskModal onClose={() => setView("runs")}/>)}
|
|
129
|
-
{view === "sqlite" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers DB - [Esc] Return to Runs | [Tab] Switch Panes | [Up/Down] Query Table">
|
|
130
|
-
<SqliteBrowser adapter={adapter} onBack={() => setView("runs")}/>
|
|
131
|
-
</box>)}
|
|
132
|
-
{view === "crons" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers Schedule Triggers - [Esc] Return to Runs | [Up/Down] Select | [Del] Remove">
|
|
133
|
-
<CronList adapter={adapter} onBack={() => setView("runs")}/>
|
|
134
|
-
</box>)}
|
|
135
|
-
{view === "metrics" && (<box style={{ flexGrow: 1, width: "100%", height: "100%", border: true, borderColor: "#34d399", flexDirection: "column" }} title="Smithers Telemetry (Prometheus Rollup) - [Esc] Return to Runs">
|
|
136
|
-
<MetricsPane adapter={adapter} onBack={() => setView("runs")}/>
|
|
137
|
-
</box>)}
|
|
138
|
-
</box>);
|
|
139
|
-
}
|
package/src/tui/app.tsx
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useState, useEffect } from "react";
|
|
3
|
-
import { useKeyboard } from "@opentui/react";
|
|
4
|
-
/**
|
|
5
|
-
* @param {{ onClose: () => void }} value
|
|
6
|
-
*/
|
|
7
|
-
export function AskModal({ onClose }) {
|
|
8
|
-
const [question, setQuestion] = useState("");
|
|
9
|
-
const [answer, setAnswer] = useState("");
|
|
10
|
-
const [status, setStatus] = useState("input");
|
|
11
|
-
useKeyboard((key) => {
|
|
12
|
-
if (key.name === "escape" || (key.name === "c" && key.ctrl)) {
|
|
13
|
-
if (status !== "streaming") { // Or allow cancel mid-stream if we kill the proc? Keep simple for now
|
|
14
|
-
onClose();
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
if (status === "input") {
|
|
19
|
-
if (key.name === "backspace") {
|
|
20
|
-
setQuestion((q) => q.slice(0, -1));
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (key.name === "enter" || key.name === "return") {
|
|
24
|
-
if (question.trim().length > 0) {
|
|
25
|
-
startAsk();
|
|
26
|
-
}
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
// Basic typing capture (sequence is the literal ansi char)
|
|
30
|
-
if (!key.ctrl && !key.meta && key.sequence && key.sequence.length === 1 && key.name !== "up" && key.name !== "down" && key.name !== "left" && key.name !== "right") {
|
|
31
|
-
setQuestion((q) => q + key.sequence);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
async function startAsk() {
|
|
36
|
-
setStatus("streaming");
|
|
37
|
-
setAnswer("");
|
|
38
|
-
try {
|
|
39
|
-
const proc = Bun.spawn(["bun", "run", "src/index.js", "ask", question], {
|
|
40
|
-
stdout: "pipe",
|
|
41
|
-
stderr: "pipe",
|
|
42
|
-
});
|
|
43
|
-
// Stream stdout async
|
|
44
|
-
(async () => {
|
|
45
|
-
try {
|
|
46
|
-
const stream = proc.stdout;
|
|
47
|
-
const reader = stream.getReader();
|
|
48
|
-
const decoder = new TextDecoder();
|
|
49
|
-
while (true) {
|
|
50
|
-
const { done, value } = await reader.read();
|
|
51
|
-
if (done)
|
|
52
|
-
break;
|
|
53
|
-
const text = decoder.decode(value);
|
|
54
|
-
setAnswer((a) => a + text);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
catch { }
|
|
58
|
-
})();
|
|
59
|
-
// Stream stderr async (in case the agent prints progress or errors there)
|
|
60
|
-
(async () => {
|
|
61
|
-
try {
|
|
62
|
-
const stream = proc.stderr;
|
|
63
|
-
const reader = stream.getReader();
|
|
64
|
-
const decoder = new TextDecoder();
|
|
65
|
-
while (true) {
|
|
66
|
-
const { done, value } = await reader.read();
|
|
67
|
-
if (done)
|
|
68
|
-
break;
|
|
69
|
-
const text = decoder.decode(value);
|
|
70
|
-
setAnswer((a) => a + text);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch { }
|
|
74
|
-
})();
|
|
75
|
-
await proc.exited;
|
|
76
|
-
if (proc.exitCode !== 0 && answer.trim().length === 0) {
|
|
77
|
-
setAnswer("Failed to run ask command. Is your agent installed?");
|
|
78
|
-
setStatus("error");
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
setStatus("done");
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
setAnswer(`Spawn error: ${err.message}`);
|
|
86
|
-
setStatus("error");
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// Auto scroll logic in OpenTUI: scrollboxes generally stay at the top unless navigated?
|
|
90
|
-
// For streaming, we'll just append text.
|
|
91
|
-
return (<box style={{
|
|
92
|
-
flexGrow: 1,
|
|
93
|
-
width: "100%",
|
|
94
|
-
height: "100%",
|
|
95
|
-
border: true,
|
|
96
|
-
borderColor: "magenta",
|
|
97
|
-
flexDirection: "column",
|
|
98
|
-
}} title={`Ask Smithers ${status === "input" ? "[Type Question, Enter to Submit, Esc to Close]" : "[Streaming... Esc to Close]"}`}>
|
|
99
|
-
{status === "input" ? (<box style={{ flexDirection: "column", paddingLeft: 1, paddingTop: 1 }}>
|
|
100
|
-
<text style={{ color: "cyan" }}>What would you like to know about the Smithers orchestrator?</text>
|
|
101
|
-
<text style={{ color: "white", marginTop: 1 }}>{"> "}{question}█</text>
|
|
102
|
-
</box>) : (<scrollbox style={{ width: "100%", height: "100%", flexDirection: "column", paddingLeft: 1 }}>
|
|
103
|
-
<text style={{ color: "cyan", marginBottom: 1 }}>Q: {question}</text>
|
|
104
|
-
<text style={{ color: "white" }}>{answer}</text>
|
|
105
|
-
{status === "done" && <text style={{ color: "green", marginTop: 1 }}>[ Agent finished. Press Esc to close. ]</text>}
|
|
106
|
-
{status === "error" && <text style={{ color: "red", marginTop: 1 }}>[ Agent failed. Press Esc to close. ]</text>}
|
|
107
|
-
</scrollbox>)}
|
|
108
|
-
</box>);
|
|
109
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import React, { useEffect, useState, useCallback } 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; focused: boolean; onSelectRun?: (runId: string) => void; }} value
|
|
9
|
-
*/
|
|
10
|
-
export function AttentionPane({ adapter, focused, onSelectRun, }) {
|
|
11
|
-
const [items, setItems] = useState([]);
|
|
12
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
13
|
-
useEffect(() => {
|
|
14
|
-
let mounted = true;
|
|
15
|
-
async function poll() {
|
|
16
|
-
if (!mounted)
|
|
17
|
-
return;
|
|
18
|
-
try {
|
|
19
|
-
const result = [];
|
|
20
|
-
// Active alerts
|
|
21
|
-
const alerts = await adapter.listAlerts(100, ["firing", "acknowledged"]);
|
|
22
|
-
for (const alert of alerts) {
|
|
23
|
-
result.push({
|
|
24
|
-
kind: "alert",
|
|
25
|
-
id: alert.alertId,
|
|
26
|
-
severity: alert.severity,
|
|
27
|
-
status: alert.status,
|
|
28
|
-
runId: alert.runId ?? null,
|
|
29
|
-
nodeId: alert.nodeId ?? null,
|
|
30
|
-
message: alert.message,
|
|
31
|
-
firedAtMs: alert.firedAtMs ?? null,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
// Pending approvals
|
|
35
|
-
const runs = await adapter.listRuns(100);
|
|
36
|
-
for (const run of runs) {
|
|
37
|
-
const pending = await adapter.listPendingApprovals(run.runId);
|
|
38
|
-
for (const ap of pending) {
|
|
39
|
-
result.push({
|
|
40
|
-
kind: "approval",
|
|
41
|
-
id: `${ap.runId}:${ap.nodeId}:${ap.iteration ?? 0}`,
|
|
42
|
-
severity: "info",
|
|
43
|
-
status: "pending",
|
|
44
|
-
runId: ap.runId,
|
|
45
|
-
nodeId: ap.nodeId,
|
|
46
|
-
message: ap.note ?? `Approval for ${ap.nodeId}`,
|
|
47
|
-
firedAtMs: ap.requestedAtMs ?? null,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// Sort: critical first, then warning, then info
|
|
52
|
-
const order = { critical: 0, warning: 1, info: 2 };
|
|
53
|
-
result.sort((a, b) => (order[a.severity] ?? 3) - (order[b.severity] ?? 3));
|
|
54
|
-
if (mounted)
|
|
55
|
-
setItems(result);
|
|
56
|
-
}
|
|
57
|
-
catch { }
|
|
58
|
-
if (mounted)
|
|
59
|
-
setTimeout(poll, 2000);
|
|
60
|
-
}
|
|
61
|
-
poll();
|
|
62
|
-
return () => { mounted = false; };
|
|
63
|
-
}, [adapter]);
|
|
64
|
-
const selected = items[selectedIndex];
|
|
65
|
-
useKeyboard(focused, useCallback((key) => {
|
|
66
|
-
if (key === "up" || key === "k") {
|
|
67
|
-
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
68
|
-
}
|
|
69
|
-
else if (key === "down" || key === "j") {
|
|
70
|
-
setSelectedIndex((i) => Math.min(items.length - 1, i + 1));
|
|
71
|
-
}
|
|
72
|
-
else if (key === "a" && selected?.kind === "alert" && selected.status === "firing") {
|
|
73
|
-
// Ack
|
|
74
|
-
void adapter.acknowledgeAlert(selected.id, Date.now());
|
|
75
|
-
}
|
|
76
|
-
else if (key === "r" && selected?.kind === "alert") {
|
|
77
|
-
// Resolve
|
|
78
|
-
void adapter.resolveAlert(selected.id, Date.now());
|
|
79
|
-
}
|
|
80
|
-
else if (key === "s" && selected?.kind === "alert") {
|
|
81
|
-
// Silence for 1h
|
|
82
|
-
void adapter.silenceAlert(selected.id, Date.now() + 3_600_000);
|
|
83
|
-
}
|
|
84
|
-
else if (key === "enter" && selected?.runId && onSelectRun) {
|
|
85
|
-
onSelectRun(selected.runId);
|
|
86
|
-
}
|
|
87
|
-
}, [items, selectedIndex, selected, adapter, onSelectRun]));
|
|
88
|
-
const severityCounts = {
|
|
89
|
-
critical: items.filter((i) => i.severity === "critical").length,
|
|
90
|
-
warning: items.filter((i) => i.severity === "warning").length,
|
|
91
|
-
info: items.filter((i) => i.severity === "info").length,
|
|
92
|
-
};
|
|
93
|
-
const header = [
|
|
94
|
-
`Attention (${items.length})`,
|
|
95
|
-
severityCounts.critical > 0 ? ` 🔴${severityCounts.critical}` : "",
|
|
96
|
-
severityCounts.warning > 0 ? ` 🟡${severityCounts.warning}` : "",
|
|
97
|
-
severityCounts.info > 0 ? ` 🔵${severityCounts.info}` : "",
|
|
98
|
-
].join("");
|
|
99
|
-
return (<box flexDirection="column">
|
|
100
|
-
<text bold>{header}</text>
|
|
101
|
-
<text dimColor> [a]ck [r]esolve [s]ilence [Enter] open run</text>
|
|
102
|
-
{items.length === 0 ? (<text dimColor> All clear — no attention items.</text>) : (items.map((item, i) => {
|
|
103
|
-
const isSelected = i === selectedIndex && focused;
|
|
104
|
-
const sev = item.severity === "critical" ? "🔴" : item.severity === "warning" ? "🟡" : "🔵";
|
|
105
|
-
const age = item.firedAtMs ? formatAge(item.firedAtMs) : "";
|
|
106
|
-
const prefix = isSelected ? "▸ " : " ";
|
|
107
|
-
return (<text key={item.id} inverse={isSelected}>
|
|
108
|
-
{prefix}{sev} [{item.kind}] {item.message} ({item.status}) {age}
|
|
109
|
-
</text>);
|
|
110
|
-
}))}
|
|
111
|
-
</box>);
|
|
112
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
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;
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
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;
|
|
@@ -1,87 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,96 +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; 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
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
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;
|