@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
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,5 +0,0 @@
1
- import type { SmithersDb } from "@smithers-orchestrator/db/adapter";
2
- export declare function TuiApp({ adapter, onExit, }: {
3
- adapter: SmithersDb;
4
- onExit: () => void;
5
- }): import("react/jsx-runtime").JSX.Element;
@@ -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,3 +0,0 @@
1
- export declare function AskModal({ onClose }: {
2
- onClose: () => void;
3
- }): import("react/jsx-runtime").JSX.Element;
@@ -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,5 +0,0 @@
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;
@@ -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;