loop-task 2.0.2 → 2.0.3

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 (121) hide show
  1. package/package.json +2 -2
  2. package/dist/board/App.js +0 -405
  3. package/dist/board/board-log.js +0 -18
  4. package/dist/board/components/ActionButtons.js +0 -46
  5. package/dist/board/components/BoardButton.js +0 -58
  6. package/dist/board/components/ConfirmModal.js +0 -36
  7. package/dist/board/components/ContextHelpModal.js +0 -22
  8. package/dist/board/components/CreateForm.js +0 -271
  9. package/dist/board/components/CreateProjectModal.js +0 -97
  10. package/dist/board/components/DeleteProjectConfirm.js +0 -53
  11. package/dist/board/components/DetailView.js +0 -25
  12. package/dist/board/components/EditProjectModal.js +0 -99
  13. package/dist/board/components/FilterBar.js +0 -17
  14. package/dist/board/components/Footer.js +0 -52
  15. package/dist/board/components/Header.js +0 -46
  16. package/dist/board/components/HelpModal.js +0 -65
  17. package/dist/board/components/Inspector.js +0 -12
  18. package/dist/board/components/LogModal.js +0 -82
  19. package/dist/board/components/Navigator.js +0 -84
  20. package/dist/board/components/ProjectsModal.js +0 -70
  21. package/dist/board/components/ProjectsPage.js +0 -134
  22. package/dist/board/components/RunHistory.js +0 -83
  23. package/dist/board/components/SearchBox.js +0 -10
  24. package/dist/board/components/SearchSelect.js +0 -132
  25. package/dist/board/components/TaskBrowser.js +0 -95
  26. package/dist/board/components/TaskFilterBar.js +0 -6
  27. package/dist/board/components/TaskForm.js +0 -155
  28. package/dist/board/components/Timeline.js +0 -5
  29. package/dist/board/daemon.js +0 -117
  30. package/dist/board/focus-context.js +0 -98
  31. package/dist/board/hooks/useBoardKeybindings.js +0 -344
  32. package/dist/board/hooks/useBreakpoint.js +0 -6
  33. package/dist/board/hooks/useHoverState.js +0 -11
  34. package/dist/board/hooks/useInputShortcuts.js +0 -61
  35. package/dist/board/hooks/useLogStream.js +0 -32
  36. package/dist/board/hooks/useLoopPolling.js +0 -28
  37. package/dist/board/hooks/useTabNav.js +0 -60
  38. package/dist/board/hooks/useTaskKeybindings.js +0 -120
  39. package/dist/board/index.js +0 -32
  40. package/dist/board/router.js +0 -16
  41. package/dist/board/state.js +0 -99
  42. package/dist/board/toast.js +0 -65
  43. package/dist/board/types.js +0 -1
  44. package/dist/cli.d.ts +0 -3
  45. package/dist/cli.d.ts.map +0 -1
  46. package/dist/cli.js.map +0 -1
  47. package/dist/client/commands.d.ts +0 -11
  48. package/dist/client/commands.d.ts.map +0 -1
  49. package/dist/client/commands.js.map +0 -1
  50. package/dist/client/ipc.d.ts +0 -5
  51. package/dist/client/ipc.d.ts.map +0 -1
  52. package/dist/client/ipc.js.map +0 -1
  53. package/dist/daemon/index.d.ts +0 -2
  54. package/dist/daemon/index.d.ts.map +0 -1
  55. package/dist/daemon/index.js.map +0 -1
  56. package/dist/daemon/manager.d.ts +0 -17
  57. package/dist/daemon/manager.d.ts.map +0 -1
  58. package/dist/daemon/manager.js.map +0 -1
  59. package/dist/daemon/server.d.ts +0 -15
  60. package/dist/daemon/server.d.ts.map +0 -1
  61. package/dist/daemon/server.js.map +0 -1
  62. package/dist/daemon/spawner.d.ts +0 -3
  63. package/dist/daemon/spawner.d.ts.map +0 -1
  64. package/dist/daemon/spawner.js.map +0 -1
  65. package/dist/daemon/state.d.ts +0 -15
  66. package/dist/daemon/state.d.ts.map +0 -1
  67. package/dist/daemon/state.js.map +0 -1
  68. package/dist/duration.d.ts +0 -3
  69. package/dist/duration.d.ts.map +0 -1
  70. package/dist/duration.js.map +0 -1
  71. package/dist/logger.d.ts +0 -11
  72. package/dist/logger.d.ts.map +0 -1
  73. package/dist/logger.js.map +0 -1
  74. package/dist/loop-config.d.ts +0 -13
  75. package/dist/loop-config.d.ts.map +0 -1
  76. package/dist/loop-config.js.map +0 -1
  77. package/dist/loop.d.ts +0 -46
  78. package/dist/loop.d.ts.map +0 -1
  79. package/dist/loop.js +0 -363
  80. package/dist/loop.js.map +0 -1
  81. package/dist/open-board.d.ts +0 -2
  82. package/dist/open-board.d.ts.map +0 -1
  83. package/dist/open-board.js +0 -19
  84. package/dist/open-board.js.map +0 -1
  85. package/dist/tui/app.d.ts +0 -57
  86. package/dist/tui/app.d.ts.map +0 -1
  87. package/dist/tui/app.js.map +0 -1
  88. package/dist/tui/components/ActionButtons.js +0 -49
  89. package/dist/tui/components/CommandBuilderField.js +0 -143
  90. package/dist/tui/components/CommandEditorModal.js +0 -159
  91. package/dist/tui/components/ConfirmModal.js +0 -9
  92. package/dist/tui/components/FilterBar.js +0 -13
  93. package/dist/tui/components/FocusableSearchSelect.js +0 -82
  94. package/dist/tui/components/Footer.js +0 -42
  95. package/dist/tui/components/PatchEditForm.js +0 -78
  96. package/dist/tui/components/SearchSelect.js +0 -67
  97. package/dist/tui/components/TaskFilterBar.js +0 -9
  98. package/dist/tui/create-loop.d.ts +0 -3
  99. package/dist/tui/create-loop.d.ts.map +0 -1
  100. package/dist/tui/create-loop.js +0 -90
  101. package/dist/tui/create-loop.js.map +0 -1
  102. package/dist/tui/dashboard.d.ts +0 -2
  103. package/dist/tui/dashboard.d.ts.map +0 -1
  104. package/dist/tui/dashboard.js +0 -45
  105. package/dist/tui/dashboard.js.map +0 -1
  106. package/dist/tui/events.d.ts +0 -3
  107. package/dist/tui/events.d.ts.map +0 -1
  108. package/dist/tui/events.js +0 -244
  109. package/dist/tui/events.js.map +0 -1
  110. package/dist/tui/popup.d.ts +0 -3
  111. package/dist/tui/popup.d.ts.map +0 -1
  112. package/dist/tui/popup.js +0 -58
  113. package/dist/tui/popup.js.map +0 -1
  114. package/dist/tui/render.d.ts +0 -9
  115. package/dist/tui/render.d.ts.map +0 -1
  116. package/dist/tui/render.js +0 -305
  117. package/dist/tui/render.js.map +0 -1
  118. package/dist/types.d.ts +0 -94
  119. package/dist/types.d.ts.map +0 -1
  120. package/dist/types.js.map +0 -1
  121. /package/dist/tui/{app.js → App.js} +0 -0
@@ -1,46 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "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
- }
@@ -1,65 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useTerminalDimensions } from "@opentui/react";
3
- import { t } from "../../i18n/index.js";
4
- export function HelpModal(props) {
5
- const { view } = props;
6
- const { width } = useTerminalDimensions();
7
- const loopRows = [
8
- ["tab", "switch panels"],
9
- ["up/down", "navigate list items"],
10
- ["enter", t("board.helpEdit")],
11
- ["e", "edit loop"],
12
- ["d/del", "delete loop"],
13
- ["c", "clone loop"],
14
- ["f", "force run"],
15
- ["p", "pause/play (contextual)"],
16
- ["s", "stop loop (resets schedule)"],
17
- ["n", t("board.helpCreate")],
18
- ["t", t("board.helpCreateTask")],
19
- ["o", t("board.helpCycleSort")],
20
- ["x", "cycle status filter"],
21
- ["r", "project filter"],
22
- ["/", t("board.helpSearch")],
23
- ["h", t("board.helpToggleHelp")],
24
- ["esc", t("board.helpQuit")],
25
- ];
26
- const taskRows = [
27
- ["tab", "switch panels"],
28
- ["up/down", "navigate tasks"],
29
- ["enter", "focus actions panel"],
30
- ["e", "edit task"],
31
- ["d/del", "delete task"],
32
- ["s", "select task (when creating/editing a loop)"],
33
- ["n", t("board.helpCreateTask")],
34
- ["/", t("board.helpSearch")],
35
- ["h", t("board.helpToggleHelp")],
36
- ["esc", t("board.helpQuit")],
37
- ];
38
- const projectRows = [
39
- ["tab", "switch panel (list/actions)"],
40
- ["up/down", "navigate projects"],
41
- ["enter", "focus actions"],
42
- ["n", "new project"],
43
- ["e", "edit project"],
44
- ["d", "delete project"],
45
- ["h", t("board.helpToggleHelp")],
46
- ["esc", t("board.helpQuit")],
47
- ];
48
- const rows = view === "task-list" ? taskRows : view === "projects" ? projectRows : loopRows;
49
- const title = view === "task-list" ? "Task List Shortcuts" : view === "projects" ? "Projects Shortcuts" : t("board.helpTitle");
50
- return (_jsx("box", { style: {
51
- position: "absolute",
52
- top: 0,
53
- left: 0,
54
- width: "100%",
55
- height: "100%",
56
- justifyContent: "center",
57
- alignItems: "center",
58
- zIndex: 90,
59
- }, children: _jsx("box", { title: title, border: true, style: {
60
- flexDirection: "column",
61
- padding: 1,
62
- minWidth: Math.min(52, width - 4),
63
- backgroundColor: "#111827",
64
- }, children: rows.map(([keys, desc]) => (_jsxs("text", { children: [_jsx("span", { fg: "#38bdf8", children: keys.padEnd(16) }), desc] }, keys))) }) }));
65
- }
@@ -1,12 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "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
- }
@@ -1,82 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "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
- function colorizeLine(line) {
10
- if (/^\[Run #/.test(line)) {
11
- return _jsx("text", { fg: "#38bdf8", children: _jsx("strong", { children: line }) });
12
- }
13
- if (/^--- Chain:/.test(line)) {
14
- return _jsx("text", { fg: "#a78bfa", children: _jsx("strong", { children: line }) });
15
- }
16
- if (/^\[exit 0/.test(line)) {
17
- return _jsx("text", { fg: "#4ade80", children: line });
18
- }
19
- if (/^\[exit /.test(line)) {
20
- return _jsx("text", { fg: "#f87171", children: line });
21
- }
22
- if (/^\[error\]/.test(line)) {
23
- return _jsx("text", { fg: "#f87171", children: line });
24
- }
25
- return _jsx("text", { fg: "#e5e7eb", children: line });
26
- }
27
- export function LogModal(props) {
28
- const { loopId, run, logLines: staticLines, loading } = props;
29
- const { width, height } = useTerminalDimensions();
30
- const isRunning = run.status === "running";
31
- const [streamLines, setStreamLines] = useState([]);
32
- const socketRef = useRef(null);
33
- const scrollRef = useRef(null);
34
- useEffect(() => {
35
- if (!isRunning || !loopId)
36
- return;
37
- setStreamLines([t("board.logWaiting")]);
38
- const socket = streamRunLog(loopId, run.runNumber, (line) => setStreamLines((prev) => {
39
- const next = prev[0] === t("board.logWaiting") ? [] : prev;
40
- return [...next, line].slice(-LOG_LINES_MAX);
41
- }), () => { }, () => { });
42
- socketRef.current = socket;
43
- return () => {
44
- socket.destroy();
45
- if (socketRef.current === socket) {
46
- socketRef.current = null;
47
- }
48
- };
49
- }, [isRunning, loopId, run.runNumber]);
50
- const logLines = isRunning ? streamLines : staticLines;
51
- useEffect(() => {
52
- if (logLines.length > 0) {
53
- scrollRef.current?.scrollChildIntoView(`log-line-${logLines.length - 1}`);
54
- }
55
- }, [logLines]);
56
- useKeyboard((key) => {
57
- if (key.ctrl && key.name === "c") {
58
- const all = logLines.join("\n");
59
- copyToClipboard(all);
60
- }
61
- });
62
- const success = isRunning ? true : run.exitCode === 0;
63
- const icon = isRunning ? "⟳" : success ? "✓" : "✗";
64
- const iconColor = isRunning ? "#facc15" : success ? "#4ade80" : "#f87171";
65
- const chainLabel = run.chainName ? ` → ${run.chainName}` : "";
66
- return (_jsx("box", { style: {
67
- position: "absolute",
68
- top: 0,
69
- left: 0,
70
- width: "100%",
71
- height: "100%",
72
- justifyContent: "center",
73
- alignItems: "center",
74
- zIndex: 100,
75
- }, children: _jsxs("box", { title: `${t("board.logModalTitle", { icon, time: formatRunTime(run.startedAt) })}${chainLabel}`, border: true, style: {
76
- flexDirection: "column",
77
- minWidth: Math.min(60, width - 4),
78
- width: Math.min(width - 4, 100),
79
- height: Math.min(height - 4, 30),
80
- backgroundColor: "#111827",
81
- }, children: [_jsx("scrollbox", { ref: scrollRef, 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("box", { id: `log-line-${i}`, children: colorizeLine(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") })] })] }) }));
82
- }
@@ -1,84 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "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(32, Math.max(10, Math.round(avail * 0.38)));
30
- const timingW = Math.min(16, Math.max(6, Math.round(avail * 0.25)));
31
- const sinceW = Math.max(8, avail - descW - timingW);
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
- }
@@ -1,70 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "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
- }
@@ -1,134 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState, useEffect, useRef } from "react";
3
- import { useKeyboard } from "@opentui/react";
4
- import { t } from "../../i18n/index.js";
5
- import { useHoverState } from "../hooks/useHoverState.js";
6
- import { useTabNav } from "../hooks/useTabNav.js";
7
- import { HOVER_BG, ENTITY_COLORS } from "../../config/constants.js";
8
- import { CreateProjectModal } from "./CreateProjectModal.js";
9
- import { EditProjectModal } from "./EditProjectModal.js";
10
- import { DeleteProjectConfirm } from "./DeleteProjectConfirm.js";
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, headerFocused, onClose, onRefresh, onOpenCreate, onEnterHeader } = props;
19
- const [selectedIndex, setSelectedIndex] = useState(0);
20
- const [subModal, setSubModal] = useState("none");
21
- const navItems = ["list", "edit", "delete"];
22
- const { setFocusIndex, focusedItem, isFocused } = useTabNav(navItems, {
23
- onCycleOut: (dir) => {
24
- if (dir === "right")
25
- onEnterHeader?.("right");
26
- else
27
- onEnterHeader?.("left");
28
- },
29
- });
30
- const focusedItemRef = useRef(focusedItem);
31
- focusedItemRef.current = focusedItem;
32
- const prevHeaderFocused = useRef(headerFocused);
33
- useEffect(() => {
34
- if (prevHeaderFocused.current && !headerFocused) {
35
- setFocusIndex(0);
36
- }
37
- prevHeaderFocused.current = headerFocused;
38
- }, [headerFocused]);
39
- // Expose create trigger to parent via callback
40
- useState(() => { onOpenCreate?.(() => setSubModal("create")); });
41
- const clampedIndex = Math.min(selectedIndex, Math.max(0, projects.length - 1));
42
- const selectedProject = projects[clampedIndex] ?? null;
43
- const loopCount = (projectId) => loops.filter((l) => (l.projectId ?? "default") === projectId).length;
44
- function runAction(actionIndex) {
45
- if (!selectedProject || selectedProject.isSystem)
46
- return;
47
- if (actionIndex === 0)
48
- setSubModal("edit");
49
- else if (actionIndex === 1)
50
- setSubModal("delete");
51
- }
52
- useKeyboard((key) => {
53
- if (subModal !== "none")
54
- return;
55
- if (headerFocused)
56
- return;
57
- const fi = focusedItemRef.current;
58
- if (key.name === "up" || key.name === "down") {
59
- if (fi !== "list") {
60
- key.preventDefault();
61
- return;
62
- }
63
- if (key.name === "up") {
64
- setSelectedIndex((i) => Math.max(0, i - 1));
65
- }
66
- else {
67
- setSelectedIndex((i) => Math.min(projects.length - 1, i + 1));
68
- }
69
- key.preventDefault();
70
- return;
71
- }
72
- if (key.name === "left" || key.name === "right") {
73
- key.preventDefault();
74
- return;
75
- }
76
- if (key.name === "return" || key.name === "enter") {
77
- if (fi === "list") {
78
- setFocusIndex(1);
79
- }
80
- else if (fi === "edit") {
81
- if (selectedProject && !selectedProject.isSystem)
82
- setSubModal("edit");
83
- }
84
- else if (fi === "delete") {
85
- if (selectedProject && !selectedProject.isSystem)
86
- setSubModal("delete");
87
- }
88
- key.preventDefault();
89
- return;
90
- }
91
- if (key.name === "n") {
92
- setSubModal("create");
93
- key.preventDefault();
94
- return;
95
- }
96
- if (key.name === "e" && selectedProject && !selectedProject.isSystem) {
97
- setSubModal("edit");
98
- key.preventDefault();
99
- return;
100
- }
101
- if (key.name === "d" && selectedProject && !selectedProject.isSystem) {
102
- setSubModal("delete");
103
- key.preventDefault();
104
- return;
105
- }
106
- if (key.name === "escape") {
107
- onClose();
108
- key.preventDefault();
109
- }
110
- });
111
- const actions = [
112
- { key: "edit", label: t("project.editProjectLabel") },
113
- { key: "delete", label: t("project.deleteProjectLabel") },
114
- ];
115
- return (_jsxs("box", { style: { flexDirection: "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsxs("box", { title: t("project.projectsTitle"), border: true, borderColor: focusedItem === "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) => {
116
- const isSelected = index === clampedIndex;
117
- const bg = isSelected ? "#1e3a8a" : undefined;
118
- const count = loopCount(project.id);
119
- return (_jsx("box", { backgroundColor: bg, onMouseDown: () => { setSelectedIndex(index); setFocusIndex(0); }, 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));
120
- })] }), _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: focusedItem === "edit" || focusedItem === "delete" ? 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: isFocused(action.key), onMouseDown: () => { setFocusIndex(i + 1); runAction(i); } }, action.key)))) : (_jsx("text", { fg: "#9ca3af", children: t("project.systemLabel") })) })] }), subModal === "create" ? (_jsx(CreateProjectModal, { onDone: async (project) => {
121
- setSubModal("none");
122
- await onRefresh();
123
- const newIndex = projects.findIndex((p) => p.id === project.id);
124
- if (newIndex >= 0)
125
- setSelectedIndex(newIndex);
126
- }, onCancel: () => setSubModal("none") })) : null, subModal === "edit" && selectedProject ? (_jsx(EditProjectModal, { project: selectedProject, onDone: async () => {
127
- setSubModal("none");
128
- await onRefresh();
129
- }, onCancel: () => setSubModal("none") })) : null, subModal === "delete" && selectedProject ? (_jsx(DeleteProjectConfirm, { project: selectedProject, loopCount: loopCount(selectedProject.id), onDone: async () => {
130
- setSubModal("none");
131
- setSelectedIndex((i) => Math.max(0, i - 1));
132
- await onRefresh();
133
- }, onCancel: () => setSubModal("none") })) : null] }));
134
- }
@@ -1,83 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "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
- }
@@ -1,10 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useRef } from "react";
3
- import { t } from "../../i18n/index.js";
4
- import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
5
- export function SearchBox(props) {
6
- const { query, searchActive, focused, onQueryChange, onActivate, onDismiss } = props;
7
- const inputRef = useRef(null);
8
- useInputShortcuts(() => searchActive ? inputRef.current : null);
9
- return (_jsx("box", { title: t("board.searchTitle"), border: true, borderColor: focused ? "#38bdf8" : undefined, style: { width: "25%", height: 3, marginRight: 1, paddingLeft: 1, backgroundColor: focused ? "#1e2a4a" : "#0b0b0b" }, children: searchActive ? (_jsx("input", { ref: inputRef, focused: true, value: query, placeholder: t("board.searchPlaceholder"), onInput: (value) => onQueryChange(value), onSubmit: () => onDismiss() })) : (_jsx("text", { fg: query ? "#e5e7eb" : "#6b7280", onMouseDown: onActivate, children: query || t("board.searchEmpty") })) }));
10
- }