loop-task 1.0.2 → 1.3.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.
- package/README.md +132 -50
- package/dist/board/App.js +202 -0
- package/dist/board/board-log.js +18 -0
- package/dist/board/components/ActionButtons.js +25 -0
- package/dist/board/components/BoardButton.js +58 -0
- package/dist/board/components/ConfirmModal.js +36 -0
- package/dist/board/components/CreateForm.js +155 -0
- package/dist/board/components/DetailView.js +25 -0
- package/dist/board/components/FilterBar.js +15 -0
- package/dist/board/components/Footer.js +39 -0
- package/dist/board/components/Header.js +16 -0
- package/dist/board/components/HelpModal.js +35 -0
- package/dist/board/components/Inspector.js +12 -0
- package/dist/board/components/LogModal.js +27 -0
- package/dist/board/components/Navigator.js +68 -0
- package/dist/board/components/RunHistory.js +44 -0
- package/dist/board/components/Timeline.js +5 -0
- package/dist/board/daemon.js +61 -0
- package/dist/board/format.js +95 -0
- package/dist/board/hooks/useBoardKeybindings.js +191 -0
- package/dist/board/hooks/useBreakpoint.js +6 -0
- package/dist/board/hooks/useHoverState.js +11 -0
- package/dist/board/hooks/useLogStream.js +32 -0
- package/dist/board/hooks/useLoopPolling.js +28 -0
- package/dist/board/index.js +32 -0
- package/dist/board/state.js +95 -0
- package/dist/board/toast.js +65 -0
- package/dist/board/types.js +1 -0
- package/dist/cli.js +80 -91
- package/dist/cli.js.map +1 -1
- package/dist/client/commands.d.ts +11 -0
- package/dist/client/commands.d.ts.map +1 -0
- package/dist/client/commands.js +161 -0
- package/dist/client/commands.js.map +1 -0
- package/dist/client/ipc.d.ts +5 -0
- package/dist/client/ipc.d.ts.map +1 -0
- package/dist/client/ipc.js +96 -0
- package/dist/client/ipc.js.map +1 -0
- package/dist/config/constants.js +14 -0
- package/dist/config/paths.js +43 -0
- package/dist/core/command-runner.js +80 -0
- package/dist/core/foreground-loop.js +78 -0
- package/dist/core/log-parser.js +29 -0
- package/dist/core/log-rotator.js +25 -0
- package/dist/core/loop-controller.js +251 -0
- package/dist/daemon/daemon-log.js +11 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +40 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/manager.d.ts +17 -0
- package/dist/daemon/manager.d.ts.map +1 -0
- package/dist/daemon/manager.js +181 -0
- package/dist/daemon/manager.js.map +1 -0
- package/dist/daemon/server.d.ts +15 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +174 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/spawner.d.ts +3 -0
- package/dist/daemon/spawner.d.ts.map +1 -0
- package/dist/daemon/spawner.js +82 -0
- package/dist/daemon/spawner.js.map +1 -0
- package/dist/daemon/state.d.ts +15 -0
- package/dist/daemon/state.d.ts.map +1 -0
- package/dist/daemon/state.js +120 -0
- package/dist/daemon/state.js.map +1 -0
- package/dist/duration.js +4 -4
- package/dist/entry.js +11 -0
- package/dist/esm-loader.js +36 -0
- package/dist/i18n/en.json +267 -0
- package/dist/i18n/index.js +11 -0
- package/dist/ipc/handlers/logs-stream.js +65 -0
- package/dist/ipc/send.js +5 -0
- package/dist/logger.js +0 -1
- package/dist/loop-config.d.ts +13 -0
- package/dist/loop-config.d.ts.map +1 -0
- package/dist/loop-config.js +74 -0
- package/dist/loop-config.js.map +1 -0
- package/dist/loop.d.ts +43 -2
- package/dist/loop.d.ts.map +1 -1
- package/dist/loop.js +250 -5
- package/dist/loop.js.map +1 -1
- package/dist/open-board.d.ts +2 -0
- package/dist/open-board.d.ts.map +1 -0
- package/dist/open-board.js +19 -0
- package/dist/open-board.js.map +1 -0
- package/dist/shared/fs-utils.js +11 -0
- package/dist/shared/sleep.js +17 -0
- package/dist/shared/tail.js +4 -0
- package/dist/tui/app.d.ts +57 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +167 -0
- package/dist/tui/app.js.map +1 -0
- package/dist/tui/create-loop.d.ts +3 -0
- package/dist/tui/create-loop.d.ts.map +1 -0
- package/dist/tui/create-loop.js +90 -0
- package/dist/tui/create-loop.js.map +1 -0
- package/dist/tui/dashboard.d.ts +2 -0
- package/dist/tui/dashboard.d.ts.map +1 -0
- package/dist/tui/dashboard.js +45 -0
- package/dist/tui/dashboard.js.map +1 -0
- package/dist/tui/events.d.ts +3 -0
- package/dist/tui/events.d.ts.map +1 -0
- package/dist/tui/events.js +244 -0
- package/dist/tui/events.js.map +1 -0
- package/dist/tui/popup.d.ts +3 -0
- package/dist/tui/popup.d.ts.map +1 -0
- package/dist/tui/popup.js +58 -0
- package/dist/tui/popup.js.map +1 -0
- package/dist/tui/render.d.ts +9 -0
- package/dist/tui/render.d.ts.map +1 -0
- package/dist/tui/render.js +305 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -1
- package/package.json +64 -55
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { buildLoopOptions, parseCommandLine } from "../../loop-config.js";
|
|
6
|
+
import { t } from "../../i18n/index.js";
|
|
7
|
+
import { commandLine } from "../format.js";
|
|
8
|
+
import { createLoop, updateLoop } from "../daemon.js";
|
|
9
|
+
import { useHoverState } from "../hooks/useHoverState.js";
|
|
10
|
+
import { HOVER_BG } from "../../config/constants.js";
|
|
11
|
+
export const createFields = ["interval", "command", "description", "cwd", "runNow", "maxRuns"];
|
|
12
|
+
export function createInitialValues(loop) {
|
|
13
|
+
if (!loop) {
|
|
14
|
+
return {
|
|
15
|
+
interval: "30m",
|
|
16
|
+
command: "",
|
|
17
|
+
description: "",
|
|
18
|
+
cwd: process.cwd(),
|
|
19
|
+
runNow: "n",
|
|
20
|
+
maxRuns: "",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
interval: loop.intervalHuman,
|
|
25
|
+
command: commandLine(loop.command, loop.commandArgs),
|
|
26
|
+
description: loop.description ?? "",
|
|
27
|
+
cwd: loop.cwd ?? "",
|
|
28
|
+
runNow: loop.immediate ? "y" : "n",
|
|
29
|
+
maxRuns: loop.maxRuns !== null ? String(loop.maxRuns) : "",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function CreateView(props) {
|
|
33
|
+
const fields = props.mode === "edit"
|
|
34
|
+
? createFields.filter((field) => field !== "runNow")
|
|
35
|
+
: createFields;
|
|
36
|
+
const saveIndex = fields.length;
|
|
37
|
+
const cancelIndex = fields.length + 1;
|
|
38
|
+
const labels = {
|
|
39
|
+
interval: t("board.labelInterval"),
|
|
40
|
+
command: t("board.labelCommand"),
|
|
41
|
+
description: t("board.labelDescription"),
|
|
42
|
+
cwd: t("board.labelCwd"),
|
|
43
|
+
runNow: t("board.labelRunNow"),
|
|
44
|
+
maxRuns: t("board.labelMaxRuns"),
|
|
45
|
+
};
|
|
46
|
+
const hints = {
|
|
47
|
+
interval: t("board.hintInterval"),
|
|
48
|
+
command: t("board.hintCommand"),
|
|
49
|
+
description: t("board.hintDescription"),
|
|
50
|
+
cwd: t("board.hintCwd"),
|
|
51
|
+
runNow: t("board.hintRunNow"),
|
|
52
|
+
maxRuns: t("board.hintMaxRuns"),
|
|
53
|
+
};
|
|
54
|
+
const examples = {
|
|
55
|
+
interval: t("board.exampleInterval"),
|
|
56
|
+
command: t("board.exampleCommand"),
|
|
57
|
+
description: t("board.exampleDescription"),
|
|
58
|
+
cwd: "",
|
|
59
|
+
runNow: "",
|
|
60
|
+
maxRuns: "",
|
|
61
|
+
};
|
|
62
|
+
const runNowOptions = [
|
|
63
|
+
{ name: t("board.runNowNo"), description: "", value: "n" },
|
|
64
|
+
{ name: t("board.runNowYes"), description: "", value: "y" },
|
|
65
|
+
];
|
|
66
|
+
const [values, setValues] = useState(props.initial);
|
|
67
|
+
const valuesRef = useRef(values);
|
|
68
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
69
|
+
const [error, setError] = useState("");
|
|
70
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
71
|
+
const { width: termWidth } = useTerminalDimensions();
|
|
72
|
+
const btnWidth = Math.max(10, Math.min(14, Math.floor(termWidth / 6)));
|
|
73
|
+
function updateValues(next) {
|
|
74
|
+
valuesRef.current = next;
|
|
75
|
+
setValues(next);
|
|
76
|
+
}
|
|
77
|
+
useKeyboard((key) => {
|
|
78
|
+
if (key.name === "tab") {
|
|
79
|
+
setFocusIndex((i) => {
|
|
80
|
+
const next = key.shift ? i - 1 : i + 1;
|
|
81
|
+
return Math.max(0, Math.min(cancelIndex, next));
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if ((key.name === "left" || key.name === "right") && focusIndex >= saveIndex) {
|
|
86
|
+
setFocusIndex(key.name === "left" ? saveIndex : cancelIndex);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.name === "return" || key.name === "enter") {
|
|
90
|
+
if (focusIndex === saveIndex) {
|
|
91
|
+
void submit(valuesRef.current);
|
|
92
|
+
}
|
|
93
|
+
else if (focusIndex === cancelIndex) {
|
|
94
|
+
props.onCancel();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
async function submit(current) {
|
|
99
|
+
if (isSubmitting) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
setIsSubmitting(true);
|
|
104
|
+
setError("");
|
|
105
|
+
const cwd = current.cwd.trim();
|
|
106
|
+
if (cwd && !fs.existsSync(cwd)) {
|
|
107
|
+
setError(t("board.cwdMissing", { cwd }));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const tokens = parseCommandLine(current.command.trim());
|
|
111
|
+
const [command, ...commandArgs] = tokens;
|
|
112
|
+
const built = buildLoopOptions(current.interval.trim(), command ?? "", commandArgs, {
|
|
113
|
+
now: props.mode === "create" && current.runNow === "y",
|
|
114
|
+
maxRuns: current.maxRuns.trim() || null,
|
|
115
|
+
verbose: false,
|
|
116
|
+
cwd,
|
|
117
|
+
description: current.description.trim(),
|
|
118
|
+
});
|
|
119
|
+
const request = props.mode === "edit" && props.editId
|
|
120
|
+
? updateLoop(props.editId, built.options, built.intervalHuman)
|
|
121
|
+
: createLoop(built.options, built.intervalHuman);
|
|
122
|
+
const id = await request;
|
|
123
|
+
props.onDone(props.mode === "edit", id);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
setIsSubmitting(false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const title = props.mode === "edit" ? t("board.editTitle") : t("board.createTitle");
|
|
133
|
+
return (_jsxs("box", { title: title, border: true, style: { flexDirection: "column", flexGrow: 1, padding: 1, backgroundColor: "#0b0b0b" }, children: [_jsxs("box", { style: { flexDirection: "column", marginBottom: 1 }, children: [_jsx("text", { fg: "#9ca3af", children: t("board.exampleHeading") }), _jsx("text", { fg: "#d1d5db", children: t("board.exampleFull") })] }), _jsx("box", { style: { flexDirection: "column", flexGrow: 1 }, children: Array.from({ length: Math.ceil(fields.length / 2) }, (_, row) => {
|
|
134
|
+
const leftField = fields[row * 2];
|
|
135
|
+
const rightField = row * 2 + 1 < fields.length ? fields[row * 2 + 1] : null;
|
|
136
|
+
return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(FormField, { field: leftField, index: row * 2, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, runNowOptions: runNowOptions, fields: fields, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(FormField, { field: rightField, index: row * 2 + 1, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, runNowOptions: runNowOptions, fields: fields, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
|
|
137
|
+
}) }), _jsxs("box", { style: { flexDirection: "row", height: 3, marginBottom: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(HoverButton, { label: isSubmitting ? t("board.saving") : props.mode === "edit" ? t("board.save") : t("board.create"), onMouseDown: () => void submit(valuesRef.current), selected: focusIndex === saveIndex, width: btnWidth, marginRight: 1 }), _jsx(HoverButton, { label: t("board.cancel"), onMouseDown: props.onCancel, selected: focusIndex === cancelIndex, width: btnWidth })] }), _jsx("text", { fg: "#9ca3af", children: t("board.formNav") }), error ? _jsx("text", { fg: "#f87171", children: error }) : null] }));
|
|
138
|
+
}
|
|
139
|
+
function HoverButton(props) {
|
|
140
|
+
const { isHovered, hoverProps } = useHoverState();
|
|
141
|
+
const bg = props.selected ? "#1e3a8a" : isHovered ? HOVER_BG : undefined;
|
|
142
|
+
const borderColor = props.selected ? "#38bdf8" : undefined;
|
|
143
|
+
return (_jsx("box", { border: true, onMouseDown: props.onMouseDown, borderColor: borderColor, style: { width: props.width, justifyContent: "center", alignItems: "center", marginRight: props.marginRight, backgroundColor: bg }, ...hoverProps, children: _jsx("text", { fg: "#e5e7eb", children: _jsx("strong", { children: props.label }) }) }));
|
|
144
|
+
}
|
|
145
|
+
function FormField(props) {
|
|
146
|
+
const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, examples, runNowOptions, fields, style } = props;
|
|
147
|
+
return (_jsxs("box", { style: { flexDirection: "column", ...style }, children: [_jsx("text", { fg: focusIndex === index ? "#38bdf8" : "#e5e7eb", children: labels[field] }), _jsx("text", { fg: "#6b7280", children: hints[field] }), field === "runNow" ? (_jsx("box", { border: true, borderColor: focusIndex === index ? "#38bdf8" : undefined, style: { height: runNowOptions.length + 2, backgroundColor: "#0b0b0b" }, children: _jsx("select", { focused: focusIndex === index, options: runNowOptions, selectedIndex: values.runNow === "y" ? 1 : 0, showDescription: false, style: { flexGrow: 1 }, onChange: (_index, option) => updateValues({ ...valuesRef.current, runNow: option?.value ?? "n" }) }) })) : (_jsx("box", { border: true, borderColor: focusIndex === index ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b" }, children: _jsx("input", { focused: focusIndex === index, value: values[field], placeholder: examples[field] ? t("board.placeholderExample", { example: examples[field] }) : t("board.placeholderBlank"), onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
|
|
148
|
+
if (index < fields.length - 1) {
|
|
149
|
+
setFocusIndex(index + 1);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
void submit(valuesRef.current);
|
|
153
|
+
}
|
|
154
|
+
} }) }))] }));
|
|
155
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
import { commandLine, describeLoop, statusColor, statusLabel, timingLabel } from "../format.js";
|
|
5
|
+
import { RunHistory } from "./RunHistory.js";
|
|
6
|
+
export function DetailView(props) {
|
|
7
|
+
const { loop, selectedRunIndex, onSelectRun, onOpenRun } = props;
|
|
8
|
+
const [, setTick] = useState(0);
|
|
9
|
+
const cmd = commandLine(loop.command, loop.commandArgs);
|
|
10
|
+
const maxRuns = loop.maxRuns !== null ? String(loop.maxRuns) : t("board.unlimited");
|
|
11
|
+
const summary = [
|
|
12
|
+
`${t("board.detailSummaryStatus")} ${statusLabel(loop.status)}`,
|
|
13
|
+
`${t("board.detailSummaryTiming")} ${timingLabel(loop)}`,
|
|
14
|
+
`${t("board.detailSummaryInterval")} ${loop.intervalHuman}`,
|
|
15
|
+
`${t("board.detailSummaryRuns")} ${loop.runCount}/${maxRuns}`,
|
|
16
|
+
`${t("board.detailSummaryExit")} ${loop.lastExitCode ?? t("format.dash")}`,
|
|
17
|
+
].join(" | ");
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const timer = setInterval(() => {
|
|
20
|
+
setTick((tick) => tick + 1);
|
|
21
|
+
}, 1000);
|
|
22
|
+
return () => clearInterval(timer);
|
|
23
|
+
}, []);
|
|
24
|
+
return (_jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsxs("box", { title: t("board.detailTitle"), border: true, style: { flexDirection: "column", backgroundColor: "#0b0b0b" }, children: [_jsx("text", { fg: "#9ca3af", children: summary }), _jsx("text", { children: " " }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldId") }), loop.id] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldDesc") }), describeLoop(loop)] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldCommand") }), cmd] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldDir") }), loop.cwd || t("board.inherit")] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldInterval") }), loop.intervalHuman] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldStatus") }), _jsx("span", { fg: statusColor(loop.status), children: statusLabel(loop.status) })] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldRuns") }), loop.runCount, " / ", maxRuns] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldCreated") }), loop.createdAt] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldLastRun") }), loop.lastRunAt ?? t("format.dash")] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldLastExit") }), loop.lastExitCode ?? t("format.dash")] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldNextRun") }), loop.nextRunAt ?? t("format.dash")] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.detailFieldPid") }), loop.pid] })] }), _jsx(RunHistory, { loop: loop, selectedRunIndex: selectedRunIndex, focused: true, onSelectRun: onSelectRun, onOpenRun: onOpenRun })] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { t } from "../../i18n/index.js";
|
|
3
|
+
import { useHoverState } from "../hooks/useHoverState.js";
|
|
4
|
+
import { HOVER_BG } from "../../config/constants.js";
|
|
5
|
+
export function FilterBar(props) {
|
|
6
|
+
const { filters, sort, searchActive, focusedPanel, onSearch, onStatusCycle, onSortCycle, onNewLoop } = props;
|
|
7
|
+
const statusDisplay = filters.status === "waiting" ? "waiting" : filters.status;
|
|
8
|
+
return (_jsxs("box", { style: { flexDirection: "row", height: 3, paddingLeft: 1, paddingRight: 1, backgroundColor: "#0b0b0b" }, children: [_jsx("box", { title: t("board.searchTitle"), border: true, borderColor: focusedPanel === "search" ? "#38bdf8" : undefined, style: { flexGrow: 2, height: 3, marginRight: 1, paddingLeft: 1, backgroundColor: focusedPanel === "search" ? "#1e2a4a" : "#0b0b0b" }, children: searchActive ? (_jsx("input", { focused: true, placeholder: t("board.searchPlaceholder"), onInput: onSearch })) : (_jsx("text", { fg: filters.query ? "#e5e7eb" : "#6b7280", children: filters.query || t("board.searchEmpty") })) }), _jsx(ClickableBadge, { title: t("board.statusFilterTitle"), text: statusDisplay, textColor: "#38bdf8", focused: focusedPanel === "status", onMouseDown: onStatusCycle, marginRight: 1 }), _jsx(ClickableBadge, { title: t("board.sortTitle"), text: sort, textColor: "#a3e635", focused: focusedPanel === "sort", onMouseDown: onSortCycle, marginRight: 1 }), _jsx(ClickableBadge, { title: t("board.newLoopTitle"), text: t("board.newLoopLabel"), textColor: "#4ade80", focused: focusedPanel === "new", onMouseDown: onNewLoop, narrow: true })] }));
|
|
9
|
+
}
|
|
10
|
+
function ClickableBadge(props) {
|
|
11
|
+
const { isHovered, hoverProps } = useHoverState();
|
|
12
|
+
const bg = props.focused ? "#1e2a4a" : isHovered ? HOVER_BG : "#0b0b0b";
|
|
13
|
+
const borderColor = props.focused ? "#38bdf8" : undefined;
|
|
14
|
+
return (_jsx("box", { title: props.title, border: true, borderColor: borderColor, onMouseDown: props.onMouseDown, style: { flexGrow: props.narrow ? 0 : 1, height: 3, marginRight: props.marginRight, paddingLeft: 1, backgroundColor: bg }, ...hoverProps, children: _jsx("text", { fg: props.textColor, children: props.text }) }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { t } from "../../i18n/index.js";
|
|
3
|
+
export function Footer(props) {
|
|
4
|
+
const { mode } = props;
|
|
5
|
+
const badge = {
|
|
6
|
+
normal: { label: t("board.badgeNormal"), bg: "#4ade80" },
|
|
7
|
+
search: { label: t("board.badgeSearch"), bg: "#38bdf8" },
|
|
8
|
+
create: { label: t("board.badgeCreate"), bg: "#a3e635" },
|
|
9
|
+
help: { label: t("board.badgeHelp"), bg: "#facc15" },
|
|
10
|
+
confirm: { label: t("board.badgeConfirm"), bg: "#f87171" },
|
|
11
|
+
};
|
|
12
|
+
const hints = {
|
|
13
|
+
normal: [
|
|
14
|
+
["←/→", t("board.hintSwitchPanel")],
|
|
15
|
+
[t("board.hintKeyEnter"), t("board.hintOpenRun")],
|
|
16
|
+
[t("board.hintKeySlash"), t("board.hintSearch")],
|
|
17
|
+
[t("board.hintKeyH"), t("board.hintHelp")],
|
|
18
|
+
[t("board.hintKeyEsc"), t("board.hintQuit")],
|
|
19
|
+
],
|
|
20
|
+
search: [
|
|
21
|
+
[t("board.hintKeyEnter"), t("board.hintApply")],
|
|
22
|
+
[t("board.hintKeyEsc"), t("board.hintCancel")],
|
|
23
|
+
],
|
|
24
|
+
create: [
|
|
25
|
+
[t("board.hintKeyTab"), t("board.hintNextField")],
|
|
26
|
+
[t("board.hintKeyEnter"), t("board.hintCreate")],
|
|
27
|
+
[t("board.hintKeyEsc"), t("board.hintCancel")],
|
|
28
|
+
],
|
|
29
|
+
help: [[t("board.hintKeyHelpEsc"), t("board.hintBack")]],
|
|
30
|
+
confirm: [
|
|
31
|
+
[t("board.hintKeyArrows"), t("board.hintChoose")],
|
|
32
|
+
[t("board.hintKeyEnter"), t("board.hintConfirm")],
|
|
33
|
+
[t("board.hintKeyYN"), t("board.hintYesNo")],
|
|
34
|
+
[t("board.hintKeyEsc"), t("board.hintCancel")],
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
const current = badge[mode];
|
|
38
|
+
return (_jsxs("box", { style: { flexDirection: "row", height: 1 }, children: [_jsx("box", { style: { backgroundColor: current.bg, paddingLeft: 1, paddingRight: 1 }, children: _jsx("text", { fg: "#0b0b0b", children: _jsx("strong", { children: current.label }) }) }), _jsx("box", { style: { flexGrow: 1, paddingLeft: 1, flexDirection: "row" }, children: _jsx("text", { children: hints[mode].map(([k, a], i) => (_jsxs("span", { children: [i > 0 ? _jsx("span", { fg: "#374151", children: " " }) : null, _jsx("span", { fg: "#38bdf8", children: k }), _jsxs("span", { fg: "#6b7280", children: [":", a] })] }, k))) }) })] }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import { HEADER_COMPACT_WIDTH } from "../../config/constants.js";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
export function Header(props) {
|
|
6
|
+
const { daemonStatus, counts } = props;
|
|
7
|
+
const { width } = useTerminalDimensions();
|
|
8
|
+
const compact = width < HEADER_COMPACT_WIDTH;
|
|
9
|
+
const color = daemonStatus === "connected"
|
|
10
|
+
? "#4ade80"
|
|
11
|
+
: daemonStatus === "error"
|
|
12
|
+
? "#f87171"
|
|
13
|
+
: "#facc15";
|
|
14
|
+
const symbol = daemonStatus === "connected" ? "✓" : daemonStatus === "error" ? "✗" : "…";
|
|
15
|
+
return (_jsxs("box", { style: { flexDirection: "column" }, children: [_jsx("box", { style: { flexDirection: "row", paddingLeft: 1, paddingRight: 1 }, children: _jsxs("text", { children: [_jsx("strong", { fg: "#a3e635", children: t("board.appName") }), compact ? null : _jsx("span", { fg: "#6b7280", children: t("board.appTagline") })] }) }), _jsx("box", { style: { flexDirection: "row", paddingLeft: 1, paddingRight: 1 }, children: _jsxs("text", { children: [_jsx("span", { fg: "#6b7280", children: t("board.daemonLabel") }), _jsxs("span", { fg: color, children: [symbol, " ", daemonStatus] }), compact ? null : (_jsxs(_Fragment, { children: [_jsx("span", { fg: "#6b7280", children: t("board.loopsLabel") }), _jsx("span", { fg: "#e5e7eb", children: counts.total }), _jsx("span", { fg: "#6b7280", children: t("board.runningLabel") }), _jsx("span", { fg: "#4ade80", children: counts.running }), _jsx("span", { fg: "#6b7280", children: t("board.waitingLabel") }), _jsx("span", { fg: "#38bdf8", children: counts.waiting }), _jsx("span", { fg: "#6b7280", children: t("board.pausedLabel") }), _jsx("span", { fg: "#facc15", children: counts.paused })] }))] }) }), _jsx("box", { style: { height: 1, paddingLeft: 1, paddingRight: 1 }, children: _jsx("text", { fg: "#374151", children: "─".repeat(Math.max(0, width - 2)) }) })] }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
export function HelpModal() {
|
|
5
|
+
const { width } = useTerminalDimensions();
|
|
6
|
+
const rows = [
|
|
7
|
+
[t("board.helpKeyMove"), t("board.helpMoveSelection")],
|
|
8
|
+
[t("board.helpKeyEnter"), t("board.helpEdit")],
|
|
9
|
+
[t("board.helpKeyN"), t("board.helpCreate")],
|
|
10
|
+
["p", t("board.helpPauseResume")],
|
|
11
|
+
["r", t("board.helpForceRun")],
|
|
12
|
+
["del", t("board.helpDelete")],
|
|
13
|
+
["←/→", t("board.helpSwitchPanel")],
|
|
14
|
+
[t("board.helpKeySlash"), t("board.helpSearch")],
|
|
15
|
+
[t("board.helpKeyF"), t("board.helpCycleFilter")],
|
|
16
|
+
[t("board.helpKeyS"), t("board.helpCycleSort")],
|
|
17
|
+
[t("board.helpKeyH"), t("board.helpToggleHelp")],
|
|
18
|
+
[t("board.helpKeyEsc"), t("board.helpQuit")],
|
|
19
|
+
];
|
|
20
|
+
return (_jsx("box", { style: {
|
|
21
|
+
position: "absolute",
|
|
22
|
+
top: 0,
|
|
23
|
+
left: 0,
|
|
24
|
+
width: "100%",
|
|
25
|
+
height: "100%",
|
|
26
|
+
justifyContent: "center",
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
zIndex: 90,
|
|
29
|
+
}, children: _jsx("box", { title: t("board.helpTitle"), border: true, style: {
|
|
30
|
+
flexDirection: "column",
|
|
31
|
+
padding: 1,
|
|
32
|
+
minWidth: Math.min(52, width - 4),
|
|
33
|
+
backgroundColor: "#111827",
|
|
34
|
+
}, children: rows.map(([keys, desc]) => (_jsxs("text", { children: [_jsx("span", { fg: "#38bdf8", children: keys.padEnd(16) }), desc] }, keys))) }) }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { t } from "../../i18n/index.js";
|
|
3
|
+
import { commandLine, describeLoop, statusColor, statusLabel, timeAgo } from "../format.js";
|
|
4
|
+
export function Inspector(props) {
|
|
5
|
+
const { loop } = props;
|
|
6
|
+
if (!loop) {
|
|
7
|
+
return (_jsx("box", { title: t("board.inspectorTitle"), border: true, style: { backgroundColor: "#0b0b0b" }, children: _jsx("text", { fg: "#9ca3af", children: t("board.inspectorEmpty") }) }));
|
|
8
|
+
}
|
|
9
|
+
const cmd = commandLine(loop.command, loop.commandArgs);
|
|
10
|
+
const maxRuns = loop.maxRuns !== null ? String(loop.maxRuns) : t("board.unlimited");
|
|
11
|
+
return (_jsxs("box", { title: t("board.inspectorTitle"), border: true, style: { flexDirection: "column", backgroundColor: "#0b0b0b" }, children: [_jsxs("text", { children: [_jsx("strong", { children: t("board.fieldId") }), " ", loop.id] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldDesc") }), " ", describeLoop(loop)] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldCommand") }), " ", cmd] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldDir") }), " ", loop.cwd || t("board.inherit")] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldInterval") }), " ", loop.intervalHuman] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldStatus") }), " ", _jsx("span", { fg: statusColor(loop.status), children: statusLabel(loop.status) })] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldRuns") }), " ", loop.runCount, " / ", maxRuns] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldLastRun") }), " ", timeAgo(loop.lastRunAt)] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldLastExit") }), " ", loop.lastExitCode !== null ? String(loop.lastExitCode) : t("format.dash")] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldNextRun") }), " ", loop.nextRunAt ? timeAgo(loop.nextRunAt) : t("format.dash")] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.fieldPid") }), " ", loop.pid] })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import { t } from "../../i18n/index.js";
|
|
4
|
+
import { formatRunDuration, formatRunTime } from "../format.js";
|
|
5
|
+
export function LogModal(props) {
|
|
6
|
+
const { run, logLines, loading } = props;
|
|
7
|
+
const { width, height } = useTerminalDimensions();
|
|
8
|
+
const success = run.exitCode === 0;
|
|
9
|
+
const icon = success ? "✓" : "✗";
|
|
10
|
+
const iconColor = success ? "#4ade80" : "#f87171";
|
|
11
|
+
return (_jsx("box", { style: {
|
|
12
|
+
position: "absolute",
|
|
13
|
+
top: 0,
|
|
14
|
+
left: 0,
|
|
15
|
+
width: "100%",
|
|
16
|
+
height: "100%",
|
|
17
|
+
justifyContent: "center",
|
|
18
|
+
alignItems: "center",
|
|
19
|
+
zIndex: 100,
|
|
20
|
+
}, children: _jsxs("box", { title: t("board.logModalTitle", { icon, time: formatRunTime(run.startedAt) }), border: true, style: {
|
|
21
|
+
flexDirection: "column",
|
|
22
|
+
minWidth: Math.min(60, width - 4),
|
|
23
|
+
width: Math.min(width - 4, 100),
|
|
24
|
+
height: Math.min(height - 4, 30),
|
|
25
|
+
backgroundColor: "#111827",
|
|
26
|
+
}, children: [_jsx("scrollbox", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: loading ? (_jsx("text", { fg: "#9ca3af", children: t("board.logModalLoading") })) : logLines.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: t("board.logModalEmpty") })) : (logLines.map((line, i) => _jsx("text", { children: line }, i))) }), _jsxs("box", { style: { flexDirection: "row", justifyContent: "space-between", backgroundColor: "#111827" }, children: [_jsxs("text", { children: [_jsx("span", { fg: iconColor, children: icon }), " ", _jsx("span", { fg: "#9ca3af", children: formatRunDuration(run.duration) }), " ", _jsxs("span", { fg: success ? "#4ade80" : "#f87171", children: ["exit ", run.exitCode] })] }), _jsx("text", { fg: "#6b7280", children: t("board.logModalEscClose") })] })] }) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { describeLoop, statusColor, statusLabel, timingLabel, truncate } from "../format.js";
|
|
6
|
+
import { useHoverState } from "../hooks/useHoverState.js";
|
|
7
|
+
import { HOVER_BG } from "../../config/constants.js";
|
|
8
|
+
function fit(text, width) {
|
|
9
|
+
if (width <= 0)
|
|
10
|
+
return "";
|
|
11
|
+
if (text.length <= width)
|
|
12
|
+
return text.padEnd(width);
|
|
13
|
+
return truncate(text, width);
|
|
14
|
+
}
|
|
15
|
+
export function Navigator(props) {
|
|
16
|
+
const { visible, total, selectedIndex, filters, sort, breakpoint, focused, onSelect, onActivate } = props;
|
|
17
|
+
const { width, height } = useTerminalDimensions();
|
|
18
|
+
const scrollRef = useRef(null);
|
|
19
|
+
const [, setTick] = useState(0);
|
|
20
|
+
const panelWidth = width * (breakpoint === "narrow" ? 1 : 0.55) - 4;
|
|
21
|
+
const panelHeight = height - (breakpoint === "narrow" ? 10 : 7);
|
|
22
|
+
const statusW = 8;
|
|
23
|
+
const exitW = 4;
|
|
24
|
+
const runsW = 5;
|
|
25
|
+
const fixedOverhead = 2 + statusW + 1 + 1 + exitW + 1 + runsW;
|
|
26
|
+
const timingW = Math.max(6, Math.min(12, Math.floor((panelWidth - fixedOverhead) * 0.3)));
|
|
27
|
+
const descW = Math.max(4, panelWidth - fixedOverhead - timingW);
|
|
28
|
+
const header = " " +
|
|
29
|
+
fit(t("board.headerStatus"), statusW) +
|
|
30
|
+
" " +
|
|
31
|
+
fit(t("board.headerDescription"), descW) +
|
|
32
|
+
" " +
|
|
33
|
+
fit(t("board.headerTiming"), timingW) +
|
|
34
|
+
" " +
|
|
35
|
+
fit(t("board.headerExitRuns"), exitW + 1 + runsW);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const id = `nav-row-${selectedIndex}`;
|
|
38
|
+
scrollRef.current?.scrollChildIntoView(id);
|
|
39
|
+
}, [selectedIndex]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const timer = setInterval(() => {
|
|
42
|
+
setTick((tick) => tick + 1);
|
|
43
|
+
}, 1000);
|
|
44
|
+
return () => clearInterval(timer);
|
|
45
|
+
}, []);
|
|
46
|
+
return (_jsxs("box", { title: t("board.navigatorTitle", { visible: visible.length, total, sort, status: filters.status }), border: true, borderColor: focused ? "#38bdf8" : undefined, style: { width: breakpoint === "narrow" ? "100%" : "55%", flexShrink: 0, flexDirection: "column", backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx("text", { fg: "#6b7280", children: header }), visible.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: t("board.noMatch") })) : (_jsx("scrollbox", { ref: scrollRef, style: { flexGrow: 1, maxHeight: panelHeight, backgroundColor: "#0b0b0b" }, children: visible.map((loop, index) => {
|
|
47
|
+
const isSelected = index === selectedIndex;
|
|
48
|
+
const exit = loop.lastExitCode === null ? "-" : String(loop.lastExitCode);
|
|
49
|
+
return (_jsx(NavigatorRow, { id: `nav-row-${index}`, loop: loop, index: index, isSelected: isSelected, focused: focused, exit: exit, statusW: statusW, descW: descW, timingW: timingW, exitW: exitW, runsW: runsW, onSelect: onSelect, onActivate: onActivate }, loop.id));
|
|
50
|
+
}) }))] }));
|
|
51
|
+
}
|
|
52
|
+
function NavigatorRow(props) {
|
|
53
|
+
const { id, loop, index, isSelected, focused, exit, statusW, descW, timingW, exitW, runsW, onSelect, onActivate } = props;
|
|
54
|
+
const { isHovered, hoverProps } = useHoverState();
|
|
55
|
+
const bg = isSelected ? (focused ? "#1e3a8a" : "#1e2a4a") : isHovered ? HOVER_BG : undefined;
|
|
56
|
+
const lastClickRef = useRef(0);
|
|
57
|
+
function handleClick() {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
if (now - lastClickRef.current < 400) {
|
|
60
|
+
onActivate(index);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
onSelect(index);
|
|
64
|
+
}
|
|
65
|
+
lastClickRef.current = now;
|
|
66
|
+
}
|
|
67
|
+
return (_jsx("box", { id: id, onMouseDown: handleClick, backgroundColor: bg, ...hoverProps, children: _jsxs("text", { children: [isSelected ? "›" : " ", " ", _jsx("span", { fg: statusColor(loop.status), children: fit(statusLabel(loop.status), statusW) }), " ", fit(truncate(describeLoop(loop), descW), descW), " ", fit(timingLabel(loop), timingW), " ", fit(exit, exitW), " #", String(loop.runCount).padStart(runsW)] }) }));
|
|
68
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { formatFileSize, formatRunDuration, formatRunTime } from "../format.js";
|
|
6
|
+
import { useHoverState } from "../hooks/useHoverState.js";
|
|
7
|
+
import { HOVER_BG } from "../../config/constants.js";
|
|
8
|
+
function fit(text, width) {
|
|
9
|
+
if (width <= 0)
|
|
10
|
+
return "";
|
|
11
|
+
if (text.length <= width)
|
|
12
|
+
return text.padEnd(width);
|
|
13
|
+
return text.length > width ? text.slice(0, width - 3) + "..." : text;
|
|
14
|
+
}
|
|
15
|
+
export function RunHistory(props) {
|
|
16
|
+
const { loop, selectedRunIndex, focused, onSelectRun, onOpenRun } = props;
|
|
17
|
+
const { height } = useTerminalDimensions();
|
|
18
|
+
const scrollRef = useRef(null);
|
|
19
|
+
const runs = loop?.runHistory ?? [];
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const id = `run-row-${selectedRunIndex}`;
|
|
22
|
+
scrollRef.current?.scrollChildIntoView(id);
|
|
23
|
+
}, [selectedRunIndex]);
|
|
24
|
+
const panelHeight = Math.max(4, Math.floor((height - 10) * 0.45));
|
|
25
|
+
const timeW = 8;
|
|
26
|
+
const durW = 7;
|
|
27
|
+
const sizeW = 8;
|
|
28
|
+
const header = " " +
|
|
29
|
+
fit(t("board.runHistoryTime"), timeW) +
|
|
30
|
+
" " +
|
|
31
|
+
fit(t("board.runHistoryDuration"), durW) +
|
|
32
|
+
" " +
|
|
33
|
+
fit(t("board.runHistorySize"), sizeW);
|
|
34
|
+
return (_jsxs("box", { title: t("board.runHistoryTitle"), border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx("text", { fg: "#6b7280", children: header }), runs.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: t("board.runHistoryEmpty") })) : (_jsx("scrollbox", { ref: scrollRef, style: { flexGrow: 1, maxHeight: panelHeight, backgroundColor: "#0b0b0b" }, children: [...runs].reverse().map((run, revIndex) => (_jsx(RunRow, { id: `run-row-${revIndex}`, run: run, isSelected: revIndex === selectedRunIndex, focused: focused, timeW: timeW, durW: durW, sizeW: sizeW, onSelect: onSelectRun, onOpen: onOpenRun }, run.runNumber))) }, `runs-${loop?.id}-${runs.length}`))] }));
|
|
35
|
+
}
|
|
36
|
+
function RunRow(props) {
|
|
37
|
+
const { id, run, isSelected, focused, timeW, durW, sizeW, onOpen } = props;
|
|
38
|
+
const { isHovered, hoverProps } = useHoverState();
|
|
39
|
+
const bg = isSelected ? (focused ? "#1e3a8a" : "#1e2a4a") : isHovered ? HOVER_BG : undefined;
|
|
40
|
+
const success = run.exitCode === 0;
|
|
41
|
+
const icon = success ? "✓" : "✗";
|
|
42
|
+
const iconColor = success ? "#4ade80" : "#f87171";
|
|
43
|
+
return (_jsx("box", { id: id, onMouseDown: () => onOpen(run), backgroundColor: bg, ...hoverProps, children: _jsxs("text", { children: [" ", _jsx("span", { fg: "#9ca3af", children: fit(formatRunTime(run.startedAt), timeW) }), " ", _jsx("span", { fg: iconColor, children: icon }), " ", fit(formatRunDuration(run.duration), durW), " ", fit(formatFileSize(run.logSize), sizeW)] }) }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { t } from "../../i18n/index.js";
|
|
3
|
+
export function Timeline(props) {
|
|
4
|
+
return (_jsx("scrollbox", { title: t("board.timelineTitle"), border: true, style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, stickyScroll: true, stickyStart: "bottom", children: props.logLines.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: t("board.timelineEmpty") })) : (props.logLines.map((line, index) => _jsx("text", { children: line }, index))) }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { sendRequest, streamRequest } from "../client/ipc.js";
|
|
2
|
+
import { t } from "../i18n/index.js";
|
|
3
|
+
import { LOG_TAIL_DEFAULT } from "../config/constants.js";
|
|
4
|
+
function expectOk(message, type) {
|
|
5
|
+
if (type !== "ok") {
|
|
6
|
+
throw new Error(message);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function listLoops() {
|
|
10
|
+
const response = await sendRequest({ type: "list" });
|
|
11
|
+
if (response.type !== "ok") {
|
|
12
|
+
throw new Error(response.message);
|
|
13
|
+
}
|
|
14
|
+
return response.data;
|
|
15
|
+
}
|
|
16
|
+
export async function pauseLoop(id) {
|
|
17
|
+
const response = await sendRequest({ type: "pause", payload: { id } });
|
|
18
|
+
expectOk(response.message ?? t("errors.pauseFailed"), response.type);
|
|
19
|
+
}
|
|
20
|
+
export async function resumeLoop(id) {
|
|
21
|
+
const response = await sendRequest({ type: "resume", payload: { id } });
|
|
22
|
+
expectOk(response.message ?? t("errors.resumeFailed"), response.type);
|
|
23
|
+
}
|
|
24
|
+
export async function triggerLoop(id) {
|
|
25
|
+
const response = await sendRequest({ type: "trigger", payload: { id } });
|
|
26
|
+
expectOk(response.message ?? t("errors.forceRunFailed"), response.type);
|
|
27
|
+
}
|
|
28
|
+
export async function deleteLoop(id) {
|
|
29
|
+
const response = await sendRequest({ type: "delete", payload: { id } });
|
|
30
|
+
expectOk(response.message ?? t("errors.deleteFailed"), response.type);
|
|
31
|
+
}
|
|
32
|
+
export async function createLoop(options, intervalHuman) {
|
|
33
|
+
const response = await sendRequest({
|
|
34
|
+
type: "start",
|
|
35
|
+
payload: { ...options, intervalHuman },
|
|
36
|
+
});
|
|
37
|
+
if (response.type !== "ok") {
|
|
38
|
+
throw new Error(response.message);
|
|
39
|
+
}
|
|
40
|
+
return response.data.id;
|
|
41
|
+
}
|
|
42
|
+
export async function updateLoop(id, options, intervalHuman) {
|
|
43
|
+
const response = await sendRequest({
|
|
44
|
+
type: "update",
|
|
45
|
+
payload: { id, ...options, intervalHuman },
|
|
46
|
+
});
|
|
47
|
+
if (response.type !== "ok") {
|
|
48
|
+
throw new Error(response.message);
|
|
49
|
+
}
|
|
50
|
+
return response.data.id;
|
|
51
|
+
}
|
|
52
|
+
export function streamLogs(id, onLine, onError) {
|
|
53
|
+
return streamRequest({ type: "logs", payload: { id, follow: true, tail: LOG_TAIL_DEFAULT } }, onLine, () => { }, onError);
|
|
54
|
+
}
|
|
55
|
+
export async function fetchRunLog(id, runNumber) {
|
|
56
|
+
const response = await sendRequest({ type: "run-log", payload: { id, runNumber } });
|
|
57
|
+
if (response.type !== "ok") {
|
|
58
|
+
throw new Error(response.message ?? "Failed to fetch run log");
|
|
59
|
+
}
|
|
60
|
+
return response.data ?? "";
|
|
61
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { t } from "../i18n/index.js";
|
|
2
|
+
export function quoteArg(arg) {
|
|
3
|
+
return /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
4
|
+
}
|
|
5
|
+
export function commandLine(command, args) {
|
|
6
|
+
return [command, ...args.map(quoteArg)].join(" ").trim();
|
|
7
|
+
}
|
|
8
|
+
export function formatCmd(command, args, max = 24) {
|
|
9
|
+
const full = commandLine(command, args);
|
|
10
|
+
return full.length > max ? full.slice(0, max - 3) + "..." : full;
|
|
11
|
+
}
|
|
12
|
+
export function describeLoop(loop) {
|
|
13
|
+
return loop.description?.trim() || commandLine(loop.command, loop.commandArgs);
|
|
14
|
+
}
|
|
15
|
+
export function truncate(text, max) {
|
|
16
|
+
return text.length > max ? text.slice(0, max - 3) + "..." : text;
|
|
17
|
+
}
|
|
18
|
+
export function timeAgo(iso) {
|
|
19
|
+
if (!iso)
|
|
20
|
+
return t("format.dash");
|
|
21
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
22
|
+
const secs = Math.floor(diff / 1000);
|
|
23
|
+
if (secs < 5)
|
|
24
|
+
return t("format.justNow");
|
|
25
|
+
if (secs < 60)
|
|
26
|
+
return t("format.secsAgo", { secs });
|
|
27
|
+
const mins = Math.floor(secs / 60);
|
|
28
|
+
if (mins < 60)
|
|
29
|
+
return t("format.minsAgo", { mins });
|
|
30
|
+
const hrs = Math.floor(mins / 60);
|
|
31
|
+
if (hrs < 24)
|
|
32
|
+
return t("format.hrsAgo", { hrs });
|
|
33
|
+
return t("format.daysAgo", { days: Math.floor(hrs / 24) });
|
|
34
|
+
}
|
|
35
|
+
export function timeUntil(iso) {
|
|
36
|
+
if (!iso)
|
|
37
|
+
return t("format.dash");
|
|
38
|
+
const diff = Math.max(0, new Date(iso).getTime() - Date.now());
|
|
39
|
+
const secs = Math.floor(diff / 1000);
|
|
40
|
+
if (secs < 5)
|
|
41
|
+
return t("format.justNow");
|
|
42
|
+
if (secs < 60)
|
|
43
|
+
return t("format.secsAhead", { secs });
|
|
44
|
+
const mins = Math.floor(secs / 60);
|
|
45
|
+
if (mins < 60)
|
|
46
|
+
return t("format.minsAhead", { mins });
|
|
47
|
+
const hrs = Math.floor(mins / 60);
|
|
48
|
+
if (hrs < 24)
|
|
49
|
+
return t("format.hrsAhead", { hrs });
|
|
50
|
+
return t("format.daysAhead", { days: Math.floor(hrs / 24) });
|
|
51
|
+
}
|
|
52
|
+
export function statusColor(status) {
|
|
53
|
+
switch (status) {
|
|
54
|
+
case "running":
|
|
55
|
+
return "#4ade80";
|
|
56
|
+
case "paused":
|
|
57
|
+
return "#facc15";
|
|
58
|
+
case "waiting":
|
|
59
|
+
return "#38bdf8";
|
|
60
|
+
case "stopped":
|
|
61
|
+
return "#f87171";
|
|
62
|
+
default:
|
|
63
|
+
return "#ffffff";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function timingLabel(loop) {
|
|
67
|
+
if (loop.status === "paused")
|
|
68
|
+
return t("format.timingPaused");
|
|
69
|
+
if (loop.nextRunAt)
|
|
70
|
+
return t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) });
|
|
71
|
+
if (loop.lastRunAt)
|
|
72
|
+
return t("format.timingLast", { timeAgo: timeAgo(loop.lastRunAt) });
|
|
73
|
+
return t("format.timingNew");
|
|
74
|
+
}
|
|
75
|
+
export function statusLabel(status) {
|
|
76
|
+
return status === "waiting" ? "waiting" : status;
|
|
77
|
+
}
|
|
78
|
+
export function formatFileSize(bytes) {
|
|
79
|
+
if (bytes < 1024)
|
|
80
|
+
return `${bytes} B`;
|
|
81
|
+
if (bytes < 1024 * 1024)
|
|
82
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
83
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
84
|
+
}
|
|
85
|
+
export function formatRunDuration(ms) {
|
|
86
|
+
if (ms < 1000)
|
|
87
|
+
return `${ms}ms`;
|
|
88
|
+
if (ms < 60000)
|
|
89
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
90
|
+
return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
|
|
91
|
+
}
|
|
92
|
+
export function formatRunTime(iso) {
|
|
93
|
+
const d = new Date(iso);
|
|
94
|
+
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
|
|
95
|
+
}
|