loop-task 1.1.0 → 1.4.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 +250 -113
- package/dist/board/App.js +340 -0
- package/dist/board/board-log.js +18 -0
- package/dist/board/components/ActionButtons.js +45 -0
- package/dist/board/components/BoardButton.js +58 -0
- package/dist/board/components/ConfirmModal.js +36 -0
- package/dist/board/components/CreateForm.js +309 -0
- package/dist/board/components/CreateProjectModal.js +104 -0
- package/dist/board/components/DeleteProjectConfirm.js +53 -0
- package/dist/board/components/DetailView.js +25 -0
- package/dist/board/components/EditProjectModal.js +106 -0
- package/dist/board/components/FilterBar.js +16 -0
- package/dist/board/components/Footer.js +52 -0
- package/dist/board/components/Header.js +46 -0
- package/dist/board/components/HelpModal.js +36 -0
- package/dist/board/components/Inspector.js +12 -0
- package/dist/board/components/LogModal.js +58 -0
- package/dist/board/components/Navigator.js +84 -0
- package/dist/board/components/ProjectsModal.js +70 -0
- package/dist/board/components/ProjectsPage.js +105 -0
- package/dist/board/components/RunHistory.js +83 -0
- package/dist/board/components/TaskBrowser.js +95 -0
- package/dist/board/components/TaskFilterBar.js +6 -0
- package/dist/board/components/TaskForm.js +177 -0
- package/dist/board/components/Timeline.js +5 -0
- package/dist/board/daemon.js +117 -0
- package/dist/board/format.js +102 -0
- package/dist/board/hooks/useBoardKeybindings.js +299 -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/hooks/useTaskKeybindings.js +114 -0
- package/dist/board/index.js +32 -0
- package/dist/board/router.js +16 -0
- package/dist/board/state.js +99 -0
- package/dist/board/toast.js +65 -0
- package/dist/board/types.js +1 -0
- package/dist/cli.js +134 -93
- 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 +317 -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 +29 -0
- package/dist/config/paths.js +49 -0
- package/dist/core/command-runner.js +82 -0
- package/dist/core/foreground-loop.js +81 -0
- package/dist/core/log-parser.js +29 -0
- package/dist/core/log-rotator.js +25 -0
- package/dist/core/loop-controller.js +429 -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 +43 -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 +260 -0
- package/dist/daemon/manager.js.map +1 -0
- package/dist/daemon/projects.js +104 -0
- package/dist/daemon/server.d.ts +15 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +345 -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 +151 -0
- package/dist/daemon/state.js.map +1 -0
- package/dist/daemon/task-manager.js +59 -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 +386 -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 +83 -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 +247 -2
- 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/clipboard.js +19 -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 +74 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -1
- package/package.json +64 -55
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { t } from "../../i18n/index.js";
|
|
3
|
+
import { ENTITY_COLORS } from "../../config/constants.js";
|
|
4
|
+
export function Footer(props) {
|
|
5
|
+
const { mode } = props;
|
|
6
|
+
const badge = {
|
|
7
|
+
normal: { label: t("board.badgeNormal"), bg: ENTITY_COLORS.loop },
|
|
8
|
+
search: { label: t("board.badgeSearch"), bg: "#38bdf8" },
|
|
9
|
+
create: { label: t("board.badgeCreate"), bg: ENTITY_COLORS.loop },
|
|
10
|
+
task: { label: t("board.badgeTask"), bg: ENTITY_COLORS.task },
|
|
11
|
+
help: { label: t("board.badgeHelp"), bg: "#facc15" },
|
|
12
|
+
confirm: { label: t("board.badgeConfirm"), bg: "#f87171" },
|
|
13
|
+
projects: { label: t("project.projectsLabel"), bg: ENTITY_COLORS.project },
|
|
14
|
+
};
|
|
15
|
+
const hints = {
|
|
16
|
+
normal: [
|
|
17
|
+
["←/→", t("board.hintSwitchPanel")],
|
|
18
|
+
["/", t("board.hintSearch")],
|
|
19
|
+
["h", t("board.hintHelp")],
|
|
20
|
+
["esc", t("board.hintQuit")],
|
|
21
|
+
],
|
|
22
|
+
search: [
|
|
23
|
+
[t("board.hintKeyEnter"), t("board.hintApply")],
|
|
24
|
+
[t("board.hintKeyEsc"), t("board.hintCancel")],
|
|
25
|
+
],
|
|
26
|
+
create: [
|
|
27
|
+
[t("board.hintKeyTab"), t("board.hintNextField")],
|
|
28
|
+
[t("board.hintKeyEnter"), t("board.hintCreate")],
|
|
29
|
+
[t("board.hintKeyEsc"), t("board.hintCancel")],
|
|
30
|
+
],
|
|
31
|
+
task: [
|
|
32
|
+
[t("board.hintKeyTab"), t("board.hintNextField")],
|
|
33
|
+
[t("board.hintKeyEnter"), t("board.hintCreate")],
|
|
34
|
+
[t("board.hintKeyEsc"), t("board.hintCancel")],
|
|
35
|
+
],
|
|
36
|
+
help: [[t("board.hintKeyHelpEsc"), t("board.hintBack")]],
|
|
37
|
+
confirm: [
|
|
38
|
+
[t("board.hintKeyArrows"), t("board.hintChoose")],
|
|
39
|
+
[t("board.hintKeyEnter"), t("board.hintConfirm")],
|
|
40
|
+
[t("board.hintKeyYN"), t("board.hintYesNo")],
|
|
41
|
+
[t("board.hintKeyEsc"), t("board.hintCancel")],
|
|
42
|
+
],
|
|
43
|
+
projects: [
|
|
44
|
+
["n", t("project.keyNewHint")],
|
|
45
|
+
["e", t("project.keyEditHint")],
|
|
46
|
+
["d", t("project.keyDeleteHint")],
|
|
47
|
+
[t("board.hintKeyEsc"), t("board.hintBack")],
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
const current = badge[mode];
|
|
51
|
+
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))) }) })] }));
|
|
52
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
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, ENTITY_COLORS } from "../../config/constants.js";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { useHoverState } from "../hooks/useHoverState.js";
|
|
6
|
+
import { HOVER_BG } from "../../config/constants.js";
|
|
7
|
+
function ActionButton(props) {
|
|
8
|
+
const { isHovered, hoverProps } = useHoverState();
|
|
9
|
+
const bg = props.focused ? "#1e2a4a" : isHovered ? HOVER_BG : undefined;
|
|
10
|
+
return (_jsx("box", { onMouseDown: props.onMouseDown, style: { flexGrow: 0, paddingLeft: 1, paddingRight: 1, marginLeft: 1, backgroundColor: bg }, ...hoverProps, children: _jsx("text", { fg: props.textColor, children: props.label }) }));
|
|
11
|
+
}
|
|
12
|
+
export function Header(props) {
|
|
13
|
+
const { daemonStatus, counts, view, focusedPanel, onViewLoops, onViewTasks, onViewProjects, onAddLoop, onAddTask, onAddProject } = props;
|
|
14
|
+
const { width } = useTerminalDimensions();
|
|
15
|
+
const compact = width < HEADER_COMPACT_WIDTH;
|
|
16
|
+
const color = daemonStatus === "connected"
|
|
17
|
+
? "#4ade80"
|
|
18
|
+
: daemonStatus === "error"
|
|
19
|
+
? "#f87171"
|
|
20
|
+
: "#facc15";
|
|
21
|
+
const symbol = daemonStatus === "connected" ? "✓" : daemonStatus === "error" ? "✗" : "…";
|
|
22
|
+
let buttons;
|
|
23
|
+
if (view === "projects") {
|
|
24
|
+
buttons = [
|
|
25
|
+
{ label: t("board.viewLoopsLabel"), color: ENTITY_COLORS.loop, panel: "header-tasks", action: onViewLoops },
|
|
26
|
+
{ label: t("board.viewTasksLabel"), color: ENTITY_COLORS.task, panel: "header-projects", action: onViewTasks },
|
|
27
|
+
{ label: t("project.newProjectLabel"), color: ENTITY_COLORS.project, panel: "header-new", action: onAddProject },
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
else if (view === "task-list") {
|
|
31
|
+
buttons = [
|
|
32
|
+
{ label: t("project.manageLabel"), color: ENTITY_COLORS.project, panel: "header-tasks", action: onViewProjects },
|
|
33
|
+
{ label: t("board.viewLoopsLabel"), color: ENTITY_COLORS.loop, panel: "header-projects", action: onViewLoops },
|
|
34
|
+
{ label: t("board.taskActionNew"), color: ENTITY_COLORS.task, panel: "header-new", action: onAddTask },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// board (default)
|
|
39
|
+
buttons = [
|
|
40
|
+
{ label: t("project.manageLabel"), color: ENTITY_COLORS.project, panel: "header-tasks", action: onViewProjects },
|
|
41
|
+
{ label: t("board.viewTasksLabel"), color: ENTITY_COLORS.task, panel: "header-projects", action: onViewTasks },
|
|
42
|
+
{ label: t("board.newLoopLabel"), color: ENTITY_COLORS.loop, panel: "header-new", action: onAddLoop },
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
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") })] }) }), _jsxs("box", { style: { flexDirection: "row", paddingLeft: 1, paddingRight: 1, alignItems: "center" }, children: [_jsx("box", { style: { flexGrow: 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("span", { fg: "#6b7280", children: t("board.idleLabel") }), _jsx("span", { fg: "#fb923c", children: counts.idle })] }))] }) }), buttons.map((btn) => btn.action ? (_jsx(ActionButton, { label: btn.label, textColor: btn.color, focused: focusedPanel === btn.panel, onMouseDown: btn.action }, btn.panel)) : null)] }), _jsx("box", { style: { height: 1, paddingLeft: 1, paddingRight: 1 }, children: _jsx("text", { fg: "#374151", children: "─".repeat(Math.max(0, width - 2)) }) })] }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
["e", "edit loop"],
|
|
8
|
+
["d/del", "delete loop"],
|
|
9
|
+
["f", "force run"],
|
|
10
|
+
["p", "pause/play (contextual)"],
|
|
11
|
+
["s", "stop loop (resets schedule)"],
|
|
12
|
+
[t("board.helpKeyEnter"), t("board.helpEdit")],
|
|
13
|
+
["n", t("board.helpCreate")],
|
|
14
|
+
["t", t("board.helpCreateTask")],
|
|
15
|
+
["o", t("board.helpCycleSort")],
|
|
16
|
+
["←/→", t("board.helpSwitchPanel")],
|
|
17
|
+
["/", t("board.helpSearch")],
|
|
18
|
+
["h", t("board.helpToggleHelp")],
|
|
19
|
+
["esc", t("board.helpQuit")],
|
|
20
|
+
];
|
|
21
|
+
return (_jsx("box", { style: {
|
|
22
|
+
position: "absolute",
|
|
23
|
+
top: 0,
|
|
24
|
+
left: 0,
|
|
25
|
+
width: "100%",
|
|
26
|
+
height: "100%",
|
|
27
|
+
justifyContent: "center",
|
|
28
|
+
alignItems: "center",
|
|
29
|
+
zIndex: 90,
|
|
30
|
+
}, children: _jsx("box", { title: t("board.helpTitle"), border: true, style: {
|
|
31
|
+
flexDirection: "column",
|
|
32
|
+
padding: 1,
|
|
33
|
+
minWidth: Math.min(52, width - 4),
|
|
34
|
+
backgroundColor: "#111827",
|
|
35
|
+
}, children: rows.map(([keys, desc]) => (_jsxs("text", { children: [_jsx("span", { fg: "#38bdf8", children: keys.padEnd(16) }), desc] }, keys))) }) }));
|
|
36
|
+
}
|
|
@@ -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.fieldTask") }), " ", loop.taskId] }), _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,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { formatRunDuration, formatRunTime } from "../format.js";
|
|
6
|
+
import { streamRunLog } from "../daemon.js";
|
|
7
|
+
import { LOG_LINES_MAX } from "../../config/constants.js";
|
|
8
|
+
import { copyToClipboard } from "../../shared/clipboard.js";
|
|
9
|
+
export function LogModal(props) {
|
|
10
|
+
const { loopId, run, logLines: staticLines, loading } = props;
|
|
11
|
+
const { width, height } = useTerminalDimensions();
|
|
12
|
+
const isRunning = run.status === "running";
|
|
13
|
+
const [streamLines, setStreamLines] = useState([]);
|
|
14
|
+
const socketRef = useRef(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!isRunning || !loopId)
|
|
17
|
+
return;
|
|
18
|
+
setStreamLines([t("board.logWaiting")]);
|
|
19
|
+
const socket = streamRunLog(loopId, run.runNumber, (line) => setStreamLines((prev) => {
|
|
20
|
+
const next = prev[0] === t("board.logWaiting") ? [] : prev;
|
|
21
|
+
return [...next, line].slice(-LOG_LINES_MAX);
|
|
22
|
+
}), () => { }, () => { });
|
|
23
|
+
socketRef.current = socket;
|
|
24
|
+
return () => {
|
|
25
|
+
socket.destroy();
|
|
26
|
+
if (socketRef.current === socket) {
|
|
27
|
+
socketRef.current = null;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}, [isRunning, loopId, run.runNumber]);
|
|
31
|
+
useKeyboard((key) => {
|
|
32
|
+
if (key.ctrl && key.name === "c") {
|
|
33
|
+
const all = (isRunning ? streamLines : staticLines).join("\n");
|
|
34
|
+
copyToClipboard(all);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
const logLines = isRunning ? streamLines : staticLines;
|
|
38
|
+
const success = isRunning ? true : run.exitCode === 0;
|
|
39
|
+
const icon = isRunning ? "⟳" : success ? "✓" : "✗";
|
|
40
|
+
const iconColor = isRunning ? "#facc15" : success ? "#4ade80" : "#f87171";
|
|
41
|
+
const chainLabel = run.chainName ? ` → ${run.chainName}` : "";
|
|
42
|
+
return (_jsx("box", { style: {
|
|
43
|
+
position: "absolute",
|
|
44
|
+
top: 0,
|
|
45
|
+
left: 0,
|
|
46
|
+
width: "100%",
|
|
47
|
+
height: "100%",
|
|
48
|
+
justifyContent: "center",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
zIndex: 100,
|
|
51
|
+
}, children: _jsxs("box", { title: `${t("board.logModalTitle", { icon, time: formatRunTime(run.startedAt) })}${chainLabel}`, border: true, style: {
|
|
52
|
+
flexDirection: "column",
|
|
53
|
+
minWidth: Math.min(60, width - 4),
|
|
54
|
+
width: Math.min(width - 4, 100),
|
|
55
|
+
height: Math.min(height - 4, 30),
|
|
56
|
+
backgroundColor: "#111827",
|
|
57
|
+
}, children: [_jsx("scrollbox", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: loading && !isRunning ? (_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 }), " ", isRunning ? (_jsx("span", { fg: "#facc15", children: t("board.logModalRunning") })) : (_jsxs(_Fragment, { children: [_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") })] })] }) }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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, sinceLabel, statusColor, statusLabel, timingLabel, truncate } from "../format.js";
|
|
6
|
+
import { useHoverState } from "../hooks/useHoverState.js";
|
|
7
|
+
import { HOVER_BG, ENTITY_COLORS } 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, projects, 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.605) - 4;
|
|
21
|
+
const panelHeight = height - (breakpoint === "narrow" ? 10 : 7);
|
|
22
|
+
const statusW = 8;
|
|
23
|
+
const exitW = 2;
|
|
24
|
+
const runsW = 5;
|
|
25
|
+
const skpW = 4;
|
|
26
|
+
const bulletW = 2;
|
|
27
|
+
const nonVar = 2 + 1 + statusW + 1 + 1 + 1 + exitW + 1 + 1 + runsW + 1 + skpW;
|
|
28
|
+
const avail = Math.floor(panelWidth) - nonVar - bulletW;
|
|
29
|
+
const descW = Math.min(22, Math.max(6, Math.round(avail * 0.30)));
|
|
30
|
+
const sinceW = Math.min(14, Math.max(8, Math.round(avail * 0.35)));
|
|
31
|
+
const timingW = Math.max(6, avail - descW - sinceW);
|
|
32
|
+
const header = " " +
|
|
33
|
+
fit("", bulletW) +
|
|
34
|
+
fit(t("board.headerDescription"), descW) +
|
|
35
|
+
" " +
|
|
36
|
+
fit(t("board.headerStatus"), statusW) +
|
|
37
|
+
" " +
|
|
38
|
+
fit(t("board.headerSince"), sinceW) +
|
|
39
|
+
" " +
|
|
40
|
+
fit(t("board.headerTiming"), timingW) +
|
|
41
|
+
" " +
|
|
42
|
+
fit("EX", exitW) +
|
|
43
|
+
" " +
|
|
44
|
+
"#" + fit(t("board.headerRuns"), runsW) +
|
|
45
|
+
" " +
|
|
46
|
+
fit(t("board.headerSkipped"), skpW);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const id = `nav-row-${selectedIndex}`;
|
|
49
|
+
scrollRef.current?.scrollChildIntoView(id);
|
|
50
|
+
}, [selectedIndex]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const timer = setInterval(() => {
|
|
53
|
+
setTick((tick) => tick + 1);
|
|
54
|
+
}, 1000);
|
|
55
|
+
return () => clearInterval(timer);
|
|
56
|
+
}, []);
|
|
57
|
+
return (_jsxs("box", { title: t("board.navigatorTitle", { visible: visible.length, total, sort, status: filters.status }), border: true, borderColor: focused ? ENTITY_COLORS.loop : "#1e3a4a", style: { width: breakpoint === "narrow" ? "100%" : "60.5%", flexShrink: 0, flexDirection: "column", backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx("text", { fg: "#6b7280", style: { height: 1, overflow: "hidden" }, 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) => {
|
|
58
|
+
const isSelected = index === selectedIndex;
|
|
59
|
+
const exit = loop.lastExitCode === null ? "-" : String(loop.lastExitCode);
|
|
60
|
+
const proj = projects?.find((p) => p.id === (loop.projectId ?? "default"));
|
|
61
|
+
const bulletColor = proj?.color ?? "#ffffff";
|
|
62
|
+
return (_jsx(NavigatorRow, { id: `nav-row-${index}`, loop: loop, index: index, isSelected: isSelected, focused: focused, exit: exit, statusW: statusW, sinceW: sinceW, descW: descW, timingW: timingW, exitW: exitW, runsW: runsW, skpW: skpW, bulletColor: bulletColor, onSelect: onSelect, onActivate: onActivate }, loop.id));
|
|
63
|
+
}) }))] }));
|
|
64
|
+
}
|
|
65
|
+
function NavigatorRow(props) {
|
|
66
|
+
const { id, loop, index, isSelected, focused, exit, statusW, sinceW, descW, timingW, exitW, runsW, skpW, bulletColor, onSelect, onActivate } = props;
|
|
67
|
+
const { isHovered, hoverProps } = useHoverState();
|
|
68
|
+
const bg = isSelected ? (focused ? "#1e3a8a" : "#1e2a4a") : isHovered ? HOVER_BG : undefined;
|
|
69
|
+
const lastClickRef = useRef(0);
|
|
70
|
+
function handleClick() {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
if (now - lastClickRef.current < 400) {
|
|
73
|
+
onActivate(index);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
onSelect(index);
|
|
77
|
+
}
|
|
78
|
+
lastClickRef.current = now;
|
|
79
|
+
}
|
|
80
|
+
const descText = fit(truncate(describeLoop(loop), descW), descW);
|
|
81
|
+
const afterDesc = ` ${fit(statusLabel(loop.status), statusW)} ${fit(sinceLabel(loop), sinceW)} ${fit(timingLabel(loop), timingW)} ${fit(exit, exitW)} #${String(loop.runCount).padStart(runsW)} ${fit(loop.skippedCount > 0 ? String(loop.skippedCount) : "-", skpW)}`;
|
|
82
|
+
const statusFg = statusColor(loop.status);
|
|
83
|
+
return (_jsx("box", { id: id, onMouseDown: handleClick, backgroundColor: bg, style: { height: 1, overflow: "hidden" }, ...hoverProps, children: _jsxs("text", { fg: "#e5e7eb", children: [isSelected ? "› " : " ", _jsx("span", { fg: bulletColor, children: "● " }), descText, " ", _jsx("span", { fg: statusFg, children: afterDesc.slice(1, 1 + statusW) }), afterDesc.slice(1 + statusW)] }) }));
|
|
84
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
const ALL_ID = "all";
|
|
6
|
+
export function ProjectsModal(props) {
|
|
7
|
+
const { projects, loops, currentProjectId, onSelect, onClose } = props;
|
|
8
|
+
const { width } = useTerminalDimensions();
|
|
9
|
+
const [query, setQuery] = useState("");
|
|
10
|
+
// "Show All" synthetic entry always first, then real projects sorted alphabetically
|
|
11
|
+
const sorted = [...projects].sort((a, b) => a.name.localeCompare(b.name));
|
|
12
|
+
const allEntry = { id: ALL_ID, name: t("project.showAll"), color: "#6b7280", isSystem: false, isDefault: false, createdAt: "" };
|
|
13
|
+
const entries = [allEntry, ...sorted];
|
|
14
|
+
const filtered = query
|
|
15
|
+
? entries.filter((p) => p.id === ALL_ID || p.name.toLowerCase().startsWith(query.toLowerCase()))
|
|
16
|
+
: entries;
|
|
17
|
+
const initialIndex = Math.max(0, filtered.findIndex((p) => p.id === currentProjectId));
|
|
18
|
+
const [selectedIndex, setSelectedIndex] = useState(initialIndex);
|
|
19
|
+
const clampedIndex = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
|
|
20
|
+
useKeyboard((key) => {
|
|
21
|
+
if (key.name === "up") {
|
|
22
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (key.name === "down") {
|
|
26
|
+
setSelectedIndex((i) => Math.min(filtered.length - 1, i + 1));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (key.name === "return" || key.name === "enter") {
|
|
30
|
+
const entry = filtered[clampedIndex];
|
|
31
|
+
if (entry)
|
|
32
|
+
onSelect(entry.id);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (key.name === "escape") {
|
|
36
|
+
onClose();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (key.name === "backspace") {
|
|
40
|
+
setQuery((q) => q.slice(0, -1));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
|
|
44
|
+
setQuery((q) => q + key.sequence);
|
|
45
|
+
setSelectedIndex(0);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return (_jsx("box", { style: {
|
|
49
|
+
position: "absolute",
|
|
50
|
+
top: 0,
|
|
51
|
+
left: 0,
|
|
52
|
+
width: "100%",
|
|
53
|
+
height: "100%",
|
|
54
|
+
justifyContent: "center",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
zIndex: 100,
|
|
57
|
+
}, children: _jsxs("box", { title: t("project.selectProject"), border: true, style: {
|
|
58
|
+
flexDirection: "column",
|
|
59
|
+
padding: 1,
|
|
60
|
+
minWidth: Math.min(50, width - 4),
|
|
61
|
+
backgroundColor: "#111827",
|
|
62
|
+
}, children: [_jsx("box", { border: true, style: { height: 3, marginBottom: 1, backgroundColor: "#0b0b0b" }, children: _jsxs("text", { fg: query ? "#e5e7eb" : "#6b7280", children: [query || "type to filter...", "\u258E"] }) }), filtered.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: "No projects match" })) : (filtered.map((entry, index) => {
|
|
63
|
+
const isSelected = index === clampedIndex;
|
|
64
|
+
const bg = isSelected ? "#1e3a8a" : undefined;
|
|
65
|
+
const loopCount = entry.id === ALL_ID
|
|
66
|
+
? loops.length
|
|
67
|
+
: loops.filter((l) => (l.projectId ?? "default") === entry.id).length;
|
|
68
|
+
return (_jsx("box", { backgroundColor: bg, onMouseDown: () => onSelect(entry.id), style: { height: 1 }, children: _jsxs("text", { children: [isSelected ? "› " : " ", _jsx("span", { fg: entry.color, children: "\u25CF" }), ` ${entry.name} `, _jsx("span", { fg: "#6b7280", children: `(${loopCount})` })] }) }, entry.id));
|
|
69
|
+
}))] }) }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useKeyboard } from "@opentui/react";
|
|
4
|
+
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { useHoverState } from "../hooks/useHoverState.js";
|
|
6
|
+
import { HOVER_BG, ENTITY_COLORS } from "../../config/constants.js";
|
|
7
|
+
import { CreateProjectModal } from "./CreateProjectModal.js";
|
|
8
|
+
import { EditProjectModal } from "./EditProjectModal.js";
|
|
9
|
+
import { DeleteProjectConfirm } from "./DeleteProjectConfirm.js";
|
|
10
|
+
const PROJECT_ACTION_COUNT = 2;
|
|
11
|
+
function ProjectActionButton(props) {
|
|
12
|
+
const { isHovered, hoverProps } = useHoverState();
|
|
13
|
+
const bg = props.selected ? "#1e3a8a" : isHovered ? HOVER_BG : undefined;
|
|
14
|
+
const fg = props.selected ? "#ffffff" : isHovered ? "#e5e7eb" : "#9ca3af";
|
|
15
|
+
return (_jsx("box", { onMouseDown: props.onMouseDown, style: { backgroundColor: bg, paddingLeft: 1, paddingRight: 1, marginRight: 1 }, ...hoverProps, children: _jsx("text", { fg: fg, children: _jsx("strong", { children: props.label }) }) }));
|
|
16
|
+
}
|
|
17
|
+
export function ProjectsPage(props) {
|
|
18
|
+
const { projects, loops, onClose, onRefresh, onOpenCreate } = props;
|
|
19
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
20
|
+
const [subModal, setSubModal] = useState("none");
|
|
21
|
+
const [focusedPanel, setFocusedPanel] = useState("list");
|
|
22
|
+
const [selectedAction, setSelectedAction] = useState(0);
|
|
23
|
+
// Expose create trigger to parent via callback
|
|
24
|
+
useState(() => { onOpenCreate?.(() => setSubModal("create")); });
|
|
25
|
+
const clampedIndex = Math.min(selectedIndex, Math.max(0, projects.length - 1));
|
|
26
|
+
const selectedProject = projects[clampedIndex] ?? null;
|
|
27
|
+
const loopCount = (projectId) => loops.filter((l) => (l.projectId ?? "default") === projectId).length;
|
|
28
|
+
function runAction(actionIndex) {
|
|
29
|
+
if (!selectedProject || selectedProject.isSystem)
|
|
30
|
+
return;
|
|
31
|
+
if (actionIndex === 0)
|
|
32
|
+
setSubModal("edit");
|
|
33
|
+
else if (actionIndex === 1)
|
|
34
|
+
setSubModal("delete");
|
|
35
|
+
}
|
|
36
|
+
useKeyboard((key) => {
|
|
37
|
+
if (subModal !== "none")
|
|
38
|
+
return;
|
|
39
|
+
if (key.name === "up") {
|
|
40
|
+
if (focusedPanel === "list")
|
|
41
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
42
|
+
else
|
|
43
|
+
setSelectedAction((a) => Math.max(0, a - 1));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (key.name === "down") {
|
|
47
|
+
if (focusedPanel === "list")
|
|
48
|
+
setSelectedIndex((i) => Math.min(projects.length - 1, i + 1));
|
|
49
|
+
else
|
|
50
|
+
setSelectedAction((a) => Math.min(PROJECT_ACTION_COUNT - 1, a + 1));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (key.name === "left" || key.name === "right") {
|
|
54
|
+
setFocusedPanel((p) => (p === "list" ? "actions" : "list"));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.name === "return" || key.name === "enter") {
|
|
58
|
+
if (focusedPanel === "list") {
|
|
59
|
+
setFocusedPanel("actions");
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
runAction(selectedAction);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (key.name === "n") {
|
|
67
|
+
setSubModal("create");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (key.name === "e" && selectedProject && !selectedProject.isSystem) {
|
|
71
|
+
setSubModal("edit");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (key.name === "d" && selectedProject && !selectedProject.isSystem) {
|
|
75
|
+
setSubModal("delete");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (key.name === "escape") {
|
|
79
|
+
onClose();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
const actions = [
|
|
83
|
+
{ key: "edit", label: t("project.editProjectLabel") },
|
|
84
|
+
{ key: "delete", label: t("project.deleteProjectLabel") },
|
|
85
|
+
];
|
|
86
|
+
return (_jsxs("box", { style: { flexDirection: "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsxs("box", { title: t("project.projectsTitle"), border: true, borderColor: focusedPanel === "list" ? ENTITY_COLORS.project : "#1e3a2a", style: { width: "40%", flexDirection: "column", backgroundColor: "#0b0b0b", flexShrink: 0 }, children: [_jsxs("text", { fg: "#9ca3af", children: [t("project.keyNewHint"), " | ", t("project.keyEditHint"), " | ", t("project.keyDeleteHint")] }), projects.map((project, index) => {
|
|
87
|
+
const isSelected = index === clampedIndex;
|
|
88
|
+
const bg = isSelected ? "#1e3a8a" : undefined;
|
|
89
|
+
const count = loopCount(project.id);
|
|
90
|
+
return (_jsx("box", { backgroundColor: bg, onMouseDown: () => { setSelectedIndex(index); setFocusedPanel("list"); }, style: { height: 1 }, children: _jsxs("text", { children: [isSelected ? "› " : " ", _jsx("span", { fg: project.color, children: "\u25CF" }), ` ${project.name} `, _jsx("span", { fg: "#6b7280", children: `(${count})` })] }) }, project.id));
|
|
91
|
+
})] }), _jsxs("box", { style: { flexGrow: 1, flexDirection: "column", backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx("box", { title: t("board.inspectorTitle"), border: true, style: { flexGrow: 1, flexDirection: "column", backgroundColor: "#0b0b0b", padding: 1 }, children: selectedProject ? (_jsxs(_Fragment, { children: [_jsxs("text", { children: [_jsx("span", { fg: selectedProject.color, children: "\u25CF" }), ` `, _jsx("strong", { children: selectedProject.name })] }), _jsx("text", { fg: "#6b7280", children: t("project.loopCount", { count: String(loopCount(selectedProject.id)) }) }), selectedProject.isSystem ? (_jsx("text", { fg: "#9ca3af", children: t("project.systemLabel") })) : null] })) : (_jsx("text", { fg: "#9ca3af", children: t("project.noLoops") })) }), _jsx("box", { border: true, borderColor: focusedPanel === "actions" ? ENTITY_COLORS.project : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, paddingLeft: 1, backgroundColor: "#0b0b0b", alignItems: "center", justifyContent: selectedProject?.isSystem ? "center" : "flex-start" }, children: selectedProject && !selectedProject.isSystem ? (actions.map((action, i) => (_jsx(ProjectActionButton, { label: action.label, selected: focusedPanel === "actions" && selectedAction === i, onMouseDown: () => { setFocusedPanel("actions"); runAction(i); } }, action.key)))) : (_jsx("text", { fg: "#9ca3af", children: t("project.systemLabel") })) })] }), subModal === "create" ? (_jsx(CreateProjectModal, { onDone: async (project) => {
|
|
92
|
+
setSubModal("none");
|
|
93
|
+
await onRefresh();
|
|
94
|
+
const newIndex = projects.findIndex((p) => p.id === project.id);
|
|
95
|
+
if (newIndex >= 0)
|
|
96
|
+
setSelectedIndex(newIndex);
|
|
97
|
+
}, onCancel: () => setSubModal("none") })) : null, subModal === "edit" && selectedProject ? (_jsx(EditProjectModal, { project: selectedProject, onDone: async () => {
|
|
98
|
+
setSubModal("none");
|
|
99
|
+
await onRefresh();
|
|
100
|
+
}, onCancel: () => setSubModal("none") })) : null, subModal === "delete" && selectedProject ? (_jsx(DeleteProjectConfirm, { project: selectedProject, loopCount: loopCount(selectedProject.id), onDone: async () => {
|
|
101
|
+
setSubModal("none");
|
|
102
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
103
|
+
await onRefresh();
|
|
104
|
+
}, onCancel: () => setSubModal("none") })) : null] }));
|
|
105
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useMemo } 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 groupRuns(runs) {
|
|
9
|
+
const groups = [];
|
|
10
|
+
let i = 0;
|
|
11
|
+
while (i < runs.length) {
|
|
12
|
+
const current = runs[i];
|
|
13
|
+
if (current.chainGroupId) {
|
|
14
|
+
const group = runs.filter((r) => r.chainGroupId === current.chainGroupId);
|
|
15
|
+
const first = group[0];
|
|
16
|
+
const allSuccess = group.every((r) => r.status === "completed" && r.exitCode === 0);
|
|
17
|
+
const isRunning = group.some((r) => r.status === "running");
|
|
18
|
+
groups.push({
|
|
19
|
+
runs: group,
|
|
20
|
+
startedAt: first.startedAt,
|
|
21
|
+
totalDuration: group.reduce((s, r) => s + r.duration, 0),
|
|
22
|
+
totalLogSize: group.reduce((s, r) => s + r.logSize, 0),
|
|
23
|
+
allSuccess,
|
|
24
|
+
isRunning,
|
|
25
|
+
chainName: group.find((r) => r.chainName)?.chainName ?? null,
|
|
26
|
+
});
|
|
27
|
+
i += group.length;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
groups.push({
|
|
31
|
+
runs: [current],
|
|
32
|
+
startedAt: current.startedAt,
|
|
33
|
+
totalDuration: current.duration,
|
|
34
|
+
totalLogSize: current.logSize,
|
|
35
|
+
allSuccess: current.status === "completed" && current.exitCode === 0,
|
|
36
|
+
isRunning: current.status === "running",
|
|
37
|
+
chainName: null,
|
|
38
|
+
});
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return groups;
|
|
43
|
+
}
|
|
44
|
+
function fit(text, width) {
|
|
45
|
+
if (width <= 0)
|
|
46
|
+
return "";
|
|
47
|
+
if (text.length <= width)
|
|
48
|
+
return text.padEnd(width);
|
|
49
|
+
return text.length > width ? text.slice(0, width - 3) + "..." : text;
|
|
50
|
+
}
|
|
51
|
+
export function RunHistory(props) {
|
|
52
|
+
const { loop, selectedRunIndex, focused, onSelectRun, onOpenRun } = props;
|
|
53
|
+
const { height } = useTerminalDimensions();
|
|
54
|
+
const scrollRef = useRef(null);
|
|
55
|
+
const runs = loop?.runHistory ?? [];
|
|
56
|
+
const grouped = useMemo(() => groupRuns(runs), [runs]);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const id = `run-row-${selectedRunIndex}`;
|
|
59
|
+
scrollRef.current?.scrollChildIntoView(id);
|
|
60
|
+
}, [selectedRunIndex]);
|
|
61
|
+
const panelHeight = Math.max(4, Math.floor((height - 10) * 0.45));
|
|
62
|
+
const timeW = 8;
|
|
63
|
+
const durW = 7;
|
|
64
|
+
const sizeW = 8;
|
|
65
|
+
const header = " " +
|
|
66
|
+
fit(t("board.runHistoryTime"), timeW) +
|
|
67
|
+
" " +
|
|
68
|
+
fit(t("board.runHistoryDuration"), durW) +
|
|
69
|
+
" " +
|
|
70
|
+
fit(t("board.runHistorySize"), sizeW);
|
|
71
|
+
return (_jsxs("box", { title: t("board.runHistoryTitleHint"), border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx("text", { fg: "#6b7280", children: header }), grouped.length === 0 ? (_jsx("text", { fg: "#9ca3af", children: t("board.runHistoryEmpty") })) : (_jsx("scrollbox", { ref: scrollRef, style: { flexGrow: 1, maxHeight: panelHeight, backgroundColor: "#0b0b0b" }, children: [...grouped].reverse().map((group, revIndex) => (_jsx(GroupRow, { id: `run-row-${revIndex}`, group: group, isSelected: focused && revIndex === selectedRunIndex, timeW: timeW, durW: durW, sizeW: sizeW, onOpen: () => onOpenRun(group.runs[0]) }, group.runs[0].runNumber + (group.chainName ?? "")))) }, `runs-${loop?.id}-${runs.length}`))] }));
|
|
72
|
+
}
|
|
73
|
+
function GroupRow(props) {
|
|
74
|
+
const { id, group, isSelected, timeW, durW, sizeW, onOpen } = props;
|
|
75
|
+
const { isHovered, hoverProps } = useHoverState();
|
|
76
|
+
const bg = isSelected ? "#1e3a8a" : isHovered ? HOVER_BG : undefined;
|
|
77
|
+
const icon = group.isRunning ? "⟳" : group.allSuccess ? "✓" : "✗";
|
|
78
|
+
const iconColor = group.isRunning ? "#facc15" : group.allSuccess ? "#4ade80" : "#f87171";
|
|
79
|
+
const label = group.chainName
|
|
80
|
+
? `→ ${group.chainName}`
|
|
81
|
+
: "";
|
|
82
|
+
return (_jsx("box", { id: id, onMouseDown: onOpen, backgroundColor: bg, ...hoverProps, children: _jsxs("text", { children: [" ", _jsx("span", { fg: "#9ca3af", children: fit(formatRunTime(group.startedAt), timeW) }), " ", _jsx("span", { fg: iconColor, children: icon }), " ", label ? _jsx("span", { fg: "#a78bfa", children: fit(label, 12) }) : null, " ", fit(group.isRunning ? "…" : formatRunDuration(group.totalDuration), durW), " ", fit(formatFileSize(group.totalLogSize), sizeW)] }) }));
|
|
83
|
+
}
|