loop-task 1.4.1 → 1.4.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.
- package/dist/board/App.js +16 -3
- package/dist/board/components/ActionButtons.js +1 -0
- package/dist/board/components/CreateForm.js +6 -30
- package/dist/board/components/FilterBar.js +4 -3
- package/dist/board/components/HelpModal.js +29 -3
- package/dist/board/components/SearchBox.js +6 -0
- package/dist/board/components/TaskBrowser.js +5 -6
- package/dist/board/components/TaskFilterBar.js +4 -4
- package/dist/board/components/TaskForm.js +5 -29
- package/dist/board/hooks/useBoardKeybindings.js +3 -16
- package/dist/board/hooks/useTaskKeybindings.js +0 -19
- package/dist/config/paths.js +9 -0
- package/dist/core/loop-controller.js +11 -7
- package/dist/daemon/manager.js +4 -1
- package/dist/daemon/projects.js +47 -26
- package/dist/daemon/state.js +83 -32
- package/dist/i18n/en.json +1 -0
- package/package.json +1 -1
package/dist/board/App.js
CHANGED
|
@@ -61,6 +61,7 @@ export function App(props) {
|
|
|
61
61
|
const [confirm, setConfirm] = useState(null);
|
|
62
62
|
const [confirmChoice, setConfirmChoice] = useState(0);
|
|
63
63
|
const [editTarget, setEditTarget] = useState(null);
|
|
64
|
+
const [cloneMode, setCloneMode] = useState(false);
|
|
64
65
|
const [editTask, setEditTask] = useState(null);
|
|
65
66
|
const [pendingTaskSelection, setPendingTaskSelection] = useState(null);
|
|
66
67
|
const [selectedRunIndex, setSelectedRunIndex] = useState(0);
|
|
@@ -175,6 +176,17 @@ export function App(props) {
|
|
|
175
176
|
push("create");
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
179
|
+
if (action === "clone") {
|
|
180
|
+
const copy = {
|
|
181
|
+
...selected,
|
|
182
|
+
id: "",
|
|
183
|
+
description: `${selected.description} - Copy`,
|
|
184
|
+
};
|
|
185
|
+
setEditTarget(copy);
|
|
186
|
+
setCloneMode(true);
|
|
187
|
+
push("create");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
178
190
|
if (action === "delete") {
|
|
179
191
|
confirmAction(t("board.confirmDelete", { desc: selectedDesc }), t("board.toastDeleted", { desc: selectedDesc }), () => deleteLoop(selectedId));
|
|
180
192
|
return;
|
|
@@ -237,7 +249,7 @@ export function App(props) {
|
|
|
237
249
|
});
|
|
238
250
|
}
|
|
239
251
|
}
|
|
240
|
-
const cancelCreate = () => { setEditTarget(null); setPendingTaskSelection(null); pop(); };
|
|
252
|
+
const cancelCreate = () => { setEditTarget(null); setCloneMode(false); setPendingTaskSelection(null); pop(); };
|
|
241
253
|
const cancelTask = () => { setEditTask(null); pop(); };
|
|
242
254
|
const cancelTaskList = () => pop();
|
|
243
255
|
function handleChooseTask() {
|
|
@@ -250,6 +262,7 @@ export function App(props) {
|
|
|
250
262
|
}
|
|
251
263
|
const onCreateDone = (updated, _id, desc) => {
|
|
252
264
|
setEditTarget(null);
|
|
265
|
+
setCloneMode(false);
|
|
253
266
|
setPendingTaskSelection(null);
|
|
254
267
|
pop();
|
|
255
268
|
pushToast("success", updated ? t("board.toastUpdated", { desc }) : t("board.toastStarted", { desc }));
|
|
@@ -332,9 +345,9 @@ export function App(props) {
|
|
|
332
345
|
}
|
|
333
346
|
else {
|
|
334
347
|
push("projects");
|
|
335
|
-
} } }), view === "board" ? (_jsx(FilterBar, { filters: filters, sort: sort, searchActive: searchActive, focusedPanel: focusedPanel, onStatusCycle: () => setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })), onSortCycle: () => setSort(cycleSortMode(sort)), onSelectProject: () => setProjectsModalOpen(true), currentProjectName: currentProjectId === "all" ? t("project.showAll") : (projects.find(p => p.id === currentProjectId)?.name ?? "Default") })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: taskFocusedPanel })) : null, _jsx("box", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: view === "create" ? (_jsx(CreateView, { mode: editTarget ? "edit" : "create", editId: editTarget
|
|
348
|
+
} } }), view === "board" ? (_jsx(FilterBar, { filters: filters, sort: sort, searchActive: searchActive, focusedPanel: focusedPanel, onStatusCycle: () => setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })), onSortCycle: () => setSort(cycleSortMode(sort)), onSelectProject: () => setProjectsModalOpen(true), currentProjectName: currentProjectId === "all" ? t("project.showAll") : (projects.find(p => p.id === currentProjectId)?.name ?? "Default"), onQueryChange: (value) => setFilters((prev) => ({ ...prev, query: value })), onSearchActivate: () => setSearchActive(true), onSearchDismiss: () => { setSearchActive(false); setFocusedPanel("loops"); } })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: taskFocusedPanel, onQueryChange: setTaskQuery, onSearchActivate: () => setTaskSearchActive(true), onSearchDismiss: () => { setTaskSearchActive(false); setTaskFocusedPanel("tasks"); } })) : null, _jsx("box", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: view === "create" ? (_jsx(CreateView, { mode: editTarget && !cloneMode ? "edit" : "create", editId: editTarget && !cloneMode ? editTarget.id : null, initial: createInitialValues(editTarget, currentProjectId), selectedTaskId: pendingTaskSelection?.id ?? null, selectedTaskName: pendingTaskSelection?.name ?? null, projects: projects, currentProjectId: currentProjectId, onCancel: cancelCreate, onDone: onCreateDone, onChooseTask: handleChooseTask })) : TASK_FORM_VIEWS.has(view) ? (_jsx(TaskForm, { mode: view === "task-edit" ? "edit" : "create", editTask: editTask, onCancel: cancelTask, onDone: onTaskDone })) : view === "task-list" ? (_jsxs("box", { style: { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(TaskNavigator, { visible: filteredTasks, total: tasks.length, selectedIndex: taskClampedIndex, focused: taskFocusedPanel === "tasks", query: taskQuery, onSelect: (index) => { setTaskSelectedIndex(index); setTaskFocusedPanel("tasks"); }, onActivate: (index) => { setTaskSelectedIndex(index); setEditTask(filteredTasks[index] ?? null); push("task-edit"); } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(TaskInspector, { task: selectedTask }, `ti-${selectedTask?.id}`), _jsx(TaskActionButtons, { task: selectedTask, focused: taskFocusedPanel === "actions", selectedAction: taskSelectedAction, selectable: stack.includes("create") || stack.includes("task-edit"), onAction: handleTaskAction }, `tab-${selectedTask?.id}`)] })] })) : view === "projects" ? (_jsx(ProjectsPage, { projects: projects, loops: loops, onClose: () => pop(), onRefresh: refreshProjects, onOpenCreate: (trigger) => { createProjectTriggerRef.current = trigger; } })) : (_jsxs("box", { style: { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(Navigator, { visible: visible, total: loops.length, selectedIndex: clampedIndex, filters: filters, sort: sort, breakpoint: breakpoint, focused: focusedPanel === "loops", projects: projects, onSelect: (index) => { setSelectedIndex(index); setFocusedPanel("loops"); }, onActivate: (index) => { setSelectedIndex(index); const loop = visible[index]; if (loop) {
|
|
336
349
|
setEditTarget(loop);
|
|
337
350
|
push("create");
|
|
338
|
-
} } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(Inspector, { loop: selected }), _jsx(RunHistory, { loop: selected, selectedRunIndex: selectedRunIndex, focused: focusedPanel === "runs", onSelectRun: (index) => { setSelectedRunIndex(index); setFocusedPanel("runs"); }, onOpenRun: handleOpenRunLog }), _jsx(ActionButtons, { loop: selected, focused: focusedPanel === "actions", selectedAction: selectedAction, onAction: handleAction })] })] })) }, viewKey(view, editTarget, editTask)), _jsx(Footer, { mode: mode }), confirm ? (_jsx(ConfirmModal, { message: confirm.message, choice: confirmChoice, onYes: () => { const action = confirm.action; setConfirm(null); void action(); }, onNo: () => setConfirm(null) })) : null, helpOpen ? _jsx(HelpModal, {}) : null, logModalRun ? (_jsx(LogModal, { loopId: selectedId, run: logModalRun, logLines: logModalLines, loading: logModalLoading, onClose: () => setLogModalRun(null) })) : null, projectsModalOpen ? (_jsx(ProjectsModal, { projects: projects, loops: loops, currentProjectId: currentProjectId, onSelect: (id) => { setCurrentProjectId(id); setProjectsModalOpen(false); }, onClose: () => setProjectsModalOpen(false) })) : null, _jsx(ToastStack, { toasts: toasts })] }));
|
|
351
|
+
} } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(Inspector, { loop: selected }), _jsx(RunHistory, { loop: selected, selectedRunIndex: selectedRunIndex, focused: focusedPanel === "runs", onSelectRun: (index) => { setSelectedRunIndex(index); setFocusedPanel("runs"); }, onOpenRun: handleOpenRunLog }), _jsx(ActionButtons, { loop: selected, focused: focusedPanel === "actions", selectedAction: selectedAction, onAction: handleAction })] })] })) }, viewKey(view, editTarget, editTask)), _jsx(Footer, { mode: mode }), confirm ? (_jsx(ConfirmModal, { message: confirm.message, choice: confirmChoice, onYes: () => { const action = confirm.action; setConfirm(null); void action(); }, onNo: () => setConfirm(null) })) : null, helpOpen ? _jsx(HelpModal, { view: view }) : null, logModalRun ? (_jsx(LogModal, { loopId: selectedId, run: logModalRun, logLines: logModalLines, loading: logModalLoading, onClose: () => setLogModalRun(null) })) : null, projectsModalOpen ? (_jsx(ProjectsModal, { projects: projects, loops: loops, currentProjectId: currentProjectId, onSelect: (id) => { setCurrentProjectId(id); setProjectsModalOpen(false); }, onClose: () => setProjectsModalOpen(false) })) : null, _jsx(ToastStack, { toasts: toasts })] }));
|
|
339
352
|
}
|
|
340
353
|
const TASK_FORM_VIEWS = new Set(["task-create", "task-edit"]);
|
|
@@ -19,6 +19,7 @@ export function getActions(status) {
|
|
|
19
19
|
actions.push({ key: "play", label: t("board.actionPlay") });
|
|
20
20
|
}
|
|
21
21
|
if (status !== "running") {
|
|
22
|
+
actions.push({ key: "clone", label: t("board.actionClone") });
|
|
22
23
|
actions.push({ key: "trigger", label: t("board.actionTrigger") });
|
|
23
24
|
}
|
|
24
25
|
return actions;
|
|
@@ -40,7 +40,7 @@ export function createInitialValues(loop, currentProjectId) {
|
|
|
40
40
|
export function CreateView(props) {
|
|
41
41
|
const [values, setValues] = useState({
|
|
42
42
|
...props.initial,
|
|
43
|
-
project: props.
|
|
43
|
+
project: props.initial.project ?? props.currentProjectId ?? "default",
|
|
44
44
|
});
|
|
45
45
|
const valuesRef = useRef(values);
|
|
46
46
|
const [focusIndex, setFocusIndex] = useState(0);
|
|
@@ -86,38 +86,14 @@ export function CreateView(props) {
|
|
|
86
86
|
if (key.name === "tab") {
|
|
87
87
|
setFocusIndex((i) => {
|
|
88
88
|
const next = key.shift ? i - 1 : i + 1;
|
|
89
|
-
|
|
89
|
+
if (next < 0)
|
|
90
|
+
return cancelIndex;
|
|
91
|
+
if (next > cancelIndex)
|
|
92
|
+
return 0;
|
|
93
|
+
return next;
|
|
90
94
|
});
|
|
91
95
|
return;
|
|
92
96
|
}
|
|
93
|
-
if (key.name === "left" || key.name === "right") {
|
|
94
|
-
if (focusIndex === saveIndex) {
|
|
95
|
-
setFocusIndex(key.name === "right" ? cancelIndex : focusIndex - 1 >= 0 ? filteredFields.length - 1 : 0);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (focusIndex === cancelIndex) {
|
|
99
|
-
setFocusIndex(key.name === "right" ? 0 : saveIndex);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
if (key.name === "right") {
|
|
103
|
-
if (focusIndex + 1 < filteredFields.length) {
|
|
104
|
-
setFocusIndex((i) => i + 1);
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
setFocusIndex(saveIndex);
|
|
108
|
-
}
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (key.name === "left") {
|
|
112
|
-
if (focusIndex > 0) {
|
|
113
|
-
setFocusIndex((i) => i - 1);
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
setFocusIndex(cancelIndex);
|
|
117
|
-
}
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
97
|
if (key.name === "return" || key.name === "enter" || key.name === " ") {
|
|
122
98
|
const field = filteredFields[focusIndex];
|
|
123
99
|
if (field === "taskMode") {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import { t } from "../../i18n/index.js";
|
|
3
3
|
import { useHoverState } from "../hooks/useHoverState.js";
|
|
4
4
|
import { HOVER_BG } from "../../config/constants.js";
|
|
5
|
+
import { SearchBox } from "./SearchBox.js";
|
|
5
6
|
export function FilterBar(props) {
|
|
6
|
-
const { filters, sort, searchActive, focusedPanel, onStatusCycle, onSortCycle, onSelectProject, currentProjectName } = props;
|
|
7
|
+
const { filters, sort, searchActive, focusedPanel, onStatusCycle, onSortCycle, onSelectProject, currentProjectName, onQueryChange, onSearchActivate, onSearchDismiss } = props;
|
|
7
8
|
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(
|
|
9
|
+
return (_jsxs("box", { style: { flexDirection: "row", height: 3, paddingLeft: 1, paddingRight: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(SearchBox, { query: filters.query, searchActive: searchActive, focused: focusedPanel === "search", onQueryChange: onQueryChange, onActivate: onSearchActivate, onDismiss: onSearchDismiss }), onSelectProject ? (_jsx(ClickableBadge, { title: t("project.filterTitle"), text: currentProjectName ?? "Default", textColor: "#f59e0b", focused: focusedPanel === "project-filter", onMouseDown: onSelectProject, marginRight: 1 })) : null, _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 })] }));
|
|
9
10
|
}
|
|
10
11
|
function ClickableBadge(props) {
|
|
11
12
|
const { isHovered, hoverProps } = useHoverState();
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
2
|
import { useTerminalDimensions } from "@opentui/react";
|
|
3
3
|
import { t } from "../../i18n/index.js";
|
|
4
|
-
export function HelpModal() {
|
|
4
|
+
export function HelpModal(props) {
|
|
5
|
+
const { view } = props;
|
|
5
6
|
const { width } = useTerminalDimensions();
|
|
6
|
-
const
|
|
7
|
+
const loopRows = [
|
|
7
8
|
["e", "edit loop"],
|
|
8
9
|
["d/del", "delete loop"],
|
|
10
|
+
["c", "clone loop"],
|
|
9
11
|
["f", "force run"],
|
|
10
12
|
["p", "pause/play (contextual)"],
|
|
11
13
|
["s", "stop loop (resets schedule)"],
|
|
@@ -13,11 +15,35 @@ export function HelpModal() {
|
|
|
13
15
|
["n", t("board.helpCreate")],
|
|
14
16
|
["t", t("board.helpCreateTask")],
|
|
15
17
|
["o", t("board.helpCycleSort")],
|
|
18
|
+
["x", "cycle status filter"],
|
|
19
|
+
["r", "project filter"],
|
|
16
20
|
["←/→", t("board.helpSwitchPanel")],
|
|
17
21
|
["/", t("board.helpSearch")],
|
|
18
22
|
["h", t("board.helpToggleHelp")],
|
|
19
23
|
["esc", t("board.helpQuit")],
|
|
20
24
|
];
|
|
25
|
+
const taskRows = [
|
|
26
|
+
["e", "edit task"],
|
|
27
|
+
["d/del", "delete task"],
|
|
28
|
+
["s", "select task (when creating/editing a loop)"],
|
|
29
|
+
["enter", "focus actions panel"],
|
|
30
|
+
["n", t("board.helpCreateTask")],
|
|
31
|
+
["←/→", t("board.helpSwitchPanel")],
|
|
32
|
+
["/", t("board.helpSearch")],
|
|
33
|
+
["h", t("board.helpToggleHelp")],
|
|
34
|
+
["esc", t("board.helpQuit")],
|
|
35
|
+
];
|
|
36
|
+
const projectRows = [
|
|
37
|
+
["enter", "focus actions"],
|
|
38
|
+
["n", "new project"],
|
|
39
|
+
["e", "edit project"],
|
|
40
|
+
["d", "delete project"],
|
|
41
|
+
["←/→", "switch panel (list/actions)"],
|
|
42
|
+
["h", t("board.helpToggleHelp")],
|
|
43
|
+
["esc", t("board.helpQuit")],
|
|
44
|
+
];
|
|
45
|
+
const rows = view === "task-list" ? taskRows : view === "projects" ? projectRows : loopRows;
|
|
46
|
+
const title = view === "task-list" ? "Task List Shortcuts" : view === "projects" ? "Projects Shortcuts" : t("board.helpTitle");
|
|
21
47
|
return (_jsx("box", { style: {
|
|
22
48
|
position: "absolute",
|
|
23
49
|
top: 0,
|
|
@@ -27,7 +53,7 @@ export function HelpModal() {
|
|
|
27
53
|
justifyContent: "center",
|
|
28
54
|
alignItems: "center",
|
|
29
55
|
zIndex: 90,
|
|
30
|
-
}, children: _jsx("box", { title:
|
|
56
|
+
}, children: _jsx("box", { title: title, border: true, style: {
|
|
31
57
|
flexDirection: "column",
|
|
32
58
|
padding: 1,
|
|
33
59
|
minWidth: Math.min(52, width - 4),
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { t } from "../../i18n/index.js";
|
|
3
|
+
export function SearchBox(props) {
|
|
4
|
+
const { query, searchActive, focused, onQueryChange, onActivate, onDismiss } = props;
|
|
5
|
+
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", { 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") })) }));
|
|
6
|
+
}
|
|
@@ -80,17 +80,16 @@ export function TaskActionButtons(props) {
|
|
|
80
80
|
return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, backgroundColor: "#0b0b0b", justifyContent: "center", alignItems: "center" }, children: _jsx("text", { fg: "#9ca3af", children: t("board.noActions") }) }));
|
|
81
81
|
}
|
|
82
82
|
const allActions = [
|
|
83
|
-
{ key: "select", label: t("board.taskActionSelect")
|
|
84
|
-
{ key: "edit", label: t("board.taskActionEdit")
|
|
85
|
-
{ key: "delete", label: t("board.taskActionDelete")
|
|
83
|
+
{ key: "select", label: t("board.taskActionSelect") },
|
|
84
|
+
{ key: "edit", label: t("board.taskActionEdit") },
|
|
85
|
+
{ key: "delete", label: t("board.taskActionDelete") },
|
|
86
86
|
];
|
|
87
87
|
const actions = selectable ? allActions : allActions.filter((a) => a.key !== "select");
|
|
88
|
-
return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, paddingLeft: 1, backgroundColor: "#0b0b0b", alignItems: "center" }, children: actions.map((action, i) => (_jsx(TaskActionButton, { label: action.label,
|
|
88
|
+
return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, paddingLeft: 1, backgroundColor: "#0b0b0b", alignItems: "center" }, children: actions.map((action, i) => (_jsx(TaskActionButton, { label: action.label, selected: focused && selectedAction === i, onMouseDown: () => onAction(action.key) }, action.key))) }));
|
|
89
89
|
}
|
|
90
90
|
function TaskActionButton(props) {
|
|
91
91
|
const { isHovered, hoverProps } = useHoverState();
|
|
92
92
|
const bg = props.selected ? "#1e3a8a" : isHovered ? HOVER_BG : undefined;
|
|
93
93
|
const fg = props.selected ? "#ffffff" : isHovered ? "#e5e7eb" : "#9ca3af";
|
|
94
|
-
|
|
95
|
-
return (_jsxs("box", { onMouseDown: props.onMouseDown, style: { backgroundColor: bg, paddingLeft: 1, paddingRight: 1, marginRight: 1, flexDirection: "row", alignItems: "center" }, ...hoverProps, children: [_jsxs("text", { fg: keycapFg, children: ["[", props.hotkey, "] "] }), _jsx("text", { fg: fg, children: _jsx("strong", { children: props.label }) })] }));
|
|
94
|
+
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 }) }) }));
|
|
96
95
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { SearchBox } from "./SearchBox.js";
|
|
3
3
|
export function TaskFilterBar(props) {
|
|
4
|
-
const { query, searchActive, focusedPanel } = props;
|
|
5
|
-
return (_jsx("box", { style: { flexDirection: "row", height: 3, paddingLeft: 1, paddingRight: 1, backgroundColor: "#0b0b0b" }, children: _jsx("box", {
|
|
4
|
+
const { query, searchActive, focusedPanel, onQueryChange, onSearchActivate, onSearchDismiss } = props;
|
|
5
|
+
return (_jsx("box", { style: { flexDirection: "row", height: 3, paddingLeft: 1, paddingRight: 1, backgroundColor: "#0b0b0b" }, children: _jsx("box", { style: { flexGrow: 1 }, children: _jsx(SearchBox, { query: query, searchActive: searchActive, focused: focusedPanel === "search", onQueryChange: onQueryChange, onActivate: onSearchActivate, onDismiss: onSearchDismiss }) }) }));
|
|
6
6
|
}
|
|
@@ -40,38 +40,14 @@ export function TaskForm(props) {
|
|
|
40
40
|
if (key.name === "tab") {
|
|
41
41
|
setFocusIndex((i) => {
|
|
42
42
|
const next = key.shift ? i - 1 : i + 1;
|
|
43
|
-
|
|
43
|
+
if (next < 0)
|
|
44
|
+
return cancelIndex;
|
|
45
|
+
if (next > cancelIndex)
|
|
46
|
+
return 0;
|
|
47
|
+
return next;
|
|
44
48
|
});
|
|
45
49
|
return;
|
|
46
50
|
}
|
|
47
|
-
if (key.name === "left" || key.name === "right") {
|
|
48
|
-
if (focusIndex === saveIndex) {
|
|
49
|
-
setFocusIndex(key.name === "right" ? cancelIndex : taskFields.length - 1);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (focusIndex === cancelIndex) {
|
|
53
|
-
setFocusIndex(key.name === "right" ? 0 : saveIndex);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
if (key.name === "right") {
|
|
57
|
-
if (focusIndex + 1 < taskFields.length) {
|
|
58
|
-
setFocusIndex((i) => i + 1);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
setFocusIndex(saveIndex);
|
|
62
|
-
}
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
if (key.name === "left") {
|
|
66
|
-
if (focusIndex > 0) {
|
|
67
|
-
setFocusIndex((i) => i - 1);
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
setFocusIndex(cancelIndex);
|
|
71
|
-
}
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
51
|
if (key.name === "return" || key.name === "enter") {
|
|
76
52
|
if (focusIndex === saveIndex) {
|
|
77
53
|
void submit(valuesRef.current);
|
|
@@ -82,6 +82,7 @@ const GLOBAL_KEYS = {
|
|
|
82
82
|
e: (p) => p.onAction("edit"),
|
|
83
83
|
d: (p) => p.onAction("delete"),
|
|
84
84
|
delete: (p) => p.onAction("delete"),
|
|
85
|
+
c: (p) => p.onAction("clone"),
|
|
85
86
|
p: (p) => p.onAction("pause-or-play"),
|
|
86
87
|
s: (p) => p.onAction("stop"),
|
|
87
88
|
f: (p) => p.onAction("trigger"),
|
|
@@ -192,7 +193,6 @@ const BOARD_SHORTCUTS = {
|
|
|
192
193
|
o: (p) => p.setSort((prev) => cycleSortMode(prev)),
|
|
193
194
|
x: (p) => p.setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })),
|
|
194
195
|
r: (p) => p.onSelectProject?.(),
|
|
195
|
-
c: (p) => p.onSelectProject?.(),
|
|
196
196
|
};
|
|
197
197
|
export function useBoardKeybindings(params) {
|
|
198
198
|
const { confirm, confirmChoice, setConfirm, setConfirmChoice, helpOpen, setHelpOpen, searchActive, setSearchActive, view, push, pop, setEditTarget, setEditTask, selected, visibleCount, setSelectedIndex, setFilters, setSort, onQuit, destroyLogSocket, logModalRun, setLogModalRun, logModalLines, selectedRunIndex, setSelectedRunIndex, selectedRunCount, focusedPanel, setFocusedPanel, selectedAction, setSelectedAction, onAction, onOpenRunLog, refreshTasks, onViewTasks, onViewProjects, onAddLoop, onSelectProject, } = params;
|
|
@@ -219,23 +219,10 @@ export function useBoardKeybindings(params) {
|
|
|
219
219
|
}
|
|
220
220
|
if (helpOpen)
|
|
221
221
|
return;
|
|
222
|
-
if (searchActive && (name === "escape" || name === "return" || name === "enter")) {
|
|
223
|
-
OVERLAY_DISMISS.search({ setLogModalRun, setHelpOpen, setSearchActive, setFocusedPanel }, name);
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
222
|
if (searchActive) {
|
|
227
|
-
if (name === "
|
|
223
|
+
if (name === "escape") {
|
|
228
224
|
setSearchActive(false);
|
|
229
|
-
setFocusedPanel("
|
|
230
|
-
setSelectedAction((selected ? getActionCount(selected.status) : 0) - 1);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
if (name === "backspace") {
|
|
234
|
-
setFilters((prev) => ({ ...prev, query: prev.query.slice(0, -1) }));
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
if (key.sequence && key.sequence.length === 1 && key.sequence >= " " && key.sequence <= "~") {
|
|
238
|
-
setFilters((prev) => ({ ...prev, query: prev.query + key.sequence }));
|
|
225
|
+
setFocusedPanel("loops");
|
|
239
226
|
return;
|
|
240
227
|
}
|
|
241
228
|
return;
|
|
@@ -18,25 +18,6 @@ export function useTaskKeybindings(params) {
|
|
|
18
18
|
setTaskFocusedPanel("tasks");
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
|
-
if (name === "return" || name === "enter") {
|
|
22
|
-
setTaskSearchActive(false);
|
|
23
|
-
setTaskFocusedPanel("tasks");
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (name === "left") {
|
|
27
|
-
setTaskSearchActive(false);
|
|
28
|
-
setTaskFocusedPanel("actions");
|
|
29
|
-
setTaskSelectedAction(taskActionCount - 1);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
if (name === "backspace") {
|
|
33
|
-
setTaskQuery((q) => q.slice(0, -1));
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
if (key.sequence && key.sequence.length === 1 && key.sequence >= " " && key.sequence <= "~") {
|
|
37
|
-
setTaskQuery((q) => q + key.sequence);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
21
|
return;
|
|
41
22
|
}
|
|
42
23
|
if (name === "escape") {
|
package/dist/config/paths.js
CHANGED
|
@@ -21,6 +21,15 @@ export function loopFile(id) {
|
|
|
21
21
|
export function taskFile(id) {
|
|
22
22
|
return path.join(getTasksDir(), `${id}.json`);
|
|
23
23
|
}
|
|
24
|
+
export function loopsJson() {
|
|
25
|
+
return path.join(getDataDir(), "loops.json");
|
|
26
|
+
}
|
|
27
|
+
export function tasksJson() {
|
|
28
|
+
return path.join(getDataDir(), "tasks.json");
|
|
29
|
+
}
|
|
30
|
+
export function projectsJson() {
|
|
31
|
+
return path.join(getDataDir(), "projects.json");
|
|
32
|
+
}
|
|
24
33
|
export function logFile(id) {
|
|
25
34
|
return path.join(getLogsDir(), `${id}.log`);
|
|
26
35
|
}
|
|
@@ -335,12 +335,15 @@ export class LoopController extends EventEmitter {
|
|
|
335
335
|
}
|
|
336
336
|
const chainTargetId = result.exitCode === 0 ? task?.onSuccessTaskId : task?.onFailureTaskId;
|
|
337
337
|
if (chainTargetId) {
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
338
|
+
const chainGroupId = crypto.randomUUID().slice(0, 8);
|
|
339
|
+
const mainRecord = this.runHistory[this.runHistory.length - 1];
|
|
340
|
+
if (mainRecord)
|
|
341
|
+
mainRecord.chainGroupId = chainGroupId;
|
|
342
|
+
let currentTargetId = chainTargetId;
|
|
343
|
+
while (currentTargetId) {
|
|
344
|
+
const chainTask = this.taskResolver(currentTargetId);
|
|
345
|
+
if (!chainTask)
|
|
346
|
+
break;
|
|
344
347
|
const chainStartedAt = new Date().toISOString();
|
|
345
348
|
const chainOffset = fs.existsSync(this.logPath) ? fs.statSync(this.logPath).size : 0;
|
|
346
349
|
this.runHistory.push({
|
|
@@ -356,7 +359,7 @@ export class LoopController extends EventEmitter {
|
|
|
356
359
|
});
|
|
357
360
|
const chainResult = await executeCommand(chainTask.command, chainTask.commandArgs, this.options.cwd, this.logStream, signal, this.runCount);
|
|
358
361
|
const chainLogSize = fs.existsSync(this.logPath) ? fs.statSync(this.logPath).size - chainOffset : 0;
|
|
359
|
-
const chainRecord = this.runHistory.find((r) => r.chainGroupId === chainGroupId && r.status === "running");
|
|
362
|
+
const chainRecord = this.runHistory.find((r) => r.chainGroupId === chainGroupId && r.status === "running" && r.chainName === chainTask.name);
|
|
360
363
|
if (chainRecord) {
|
|
361
364
|
chainRecord.exitCode = chainResult.exitCode;
|
|
362
365
|
chainRecord.duration = chainResult.duration;
|
|
@@ -365,6 +368,7 @@ export class LoopController extends EventEmitter {
|
|
|
365
368
|
}
|
|
366
369
|
this.lastExitCode = chainResult.exitCode;
|
|
367
370
|
this.lastDuration = (this.lastDuration ?? 0) + chainResult.duration;
|
|
371
|
+
currentTargetId = (chainResult.exitCode === 0 ? chainTask.onSuccessTaskId : chainTask.onFailureTaskId) ?? null;
|
|
368
372
|
}
|
|
369
373
|
}
|
|
370
374
|
if (this.runHistory.length > 50) {
|
package/dist/daemon/manager.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { LoopController } from "../core/loop-controller.js";
|
|
3
3
|
import { ProjectManager } from "./projects.js";
|
|
4
|
-
import { saveLoop, loadAllLoops, deleteLoop as deleteLoopState, getLogPath, } from "./state.js";
|
|
4
|
+
import { saveLoop, loadAllLoops, deleteLoop as deleteLoopState, getLogPath, migrateLoopsToJson, migrateTasksToJson, } from "./state.js";
|
|
5
5
|
import { daemonLog } from "./daemon-log.js";
|
|
6
6
|
export class LoopManager {
|
|
7
7
|
loops = new Map();
|
|
@@ -16,6 +16,9 @@ export class LoopManager {
|
|
|
16
16
|
init() {
|
|
17
17
|
// Initialize projects first
|
|
18
18
|
this.projectManager.init();
|
|
19
|
+
// Migrate from individual files to consolidated JSON
|
|
20
|
+
migrateLoopsToJson();
|
|
21
|
+
migrateTasksToJson();
|
|
19
22
|
const saved = loadAllLoops();
|
|
20
23
|
let restarted = 0;
|
|
21
24
|
let migrated = 0;
|
package/dist/daemon/projects.js
CHANGED
|
@@ -1,53 +1,76 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { writeFileAtomic } from "../shared/fs-utils.js";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
3
|
import { daemonLog } from "./daemon-log.js";
|
|
6
4
|
import crypto from "node:crypto";
|
|
5
|
+
import { getDataDir, projectsJson } from "../config/paths.js";
|
|
6
|
+
import path from "node:path";
|
|
7
7
|
export class ProjectManager {
|
|
8
8
|
projects = new Map();
|
|
9
|
-
projectsDir;
|
|
10
|
-
constructor(loopCliHome) {
|
|
11
|
-
const baseDir = loopCliHome || join(homedir(), ".loop-cli");
|
|
12
|
-
this.projectsDir = join(baseDir, "projects");
|
|
13
|
-
}
|
|
14
9
|
init() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
const dataDir = getDataDir();
|
|
11
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
12
|
+
this.migrateProjectsToJson();
|
|
18
13
|
this.loadProjects();
|
|
19
14
|
if (!this.projects.has("default")) {
|
|
20
15
|
this.createDefaultProject();
|
|
21
16
|
}
|
|
22
17
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
18
|
+
migrateProjectsToJson() {
|
|
19
|
+
const jsonFile = projectsJson();
|
|
20
|
+
if (fs.existsSync(jsonFile))
|
|
21
|
+
return;
|
|
22
|
+
const oldDir = path.join(getDataDir(), "projects");
|
|
23
|
+
if (!fs.existsSync(oldDir))
|
|
26
24
|
return;
|
|
27
25
|
try {
|
|
28
|
-
const files = fs.readdirSync(
|
|
26
|
+
const files = fs.readdirSync(oldDir).filter((f) => f.endsWith(".json"));
|
|
27
|
+
if (files.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
const projects = [];
|
|
29
30
|
for (const file of files) {
|
|
30
|
-
if (!file.endsWith(".json"))
|
|
31
|
-
continue;
|
|
32
31
|
try {
|
|
33
|
-
const raw = fs.readFileSync(join(
|
|
32
|
+
const raw = fs.readFileSync(path.join(oldDir, file), "utf-8");
|
|
34
33
|
const content = JSON.parse(raw);
|
|
35
34
|
if (content.id)
|
|
36
|
-
|
|
35
|
+
projects.push(content);
|
|
37
36
|
}
|
|
38
|
-
catch
|
|
39
|
-
|
|
37
|
+
catch {
|
|
38
|
+
// skip corrupted files
|
|
40
39
|
}
|
|
41
40
|
}
|
|
41
|
+
writeFileAtomic(jsonFile, JSON.stringify(projects, null, 2));
|
|
42
|
+
daemonLog(`migrated ${projects.length} project(s) to projects.json`);
|
|
42
43
|
}
|
|
43
44
|
catch (err) {
|
|
44
|
-
daemonLog(`Failed to
|
|
45
|
+
daemonLog(`Failed to migrate projects: ${err}`);
|
|
45
46
|
}
|
|
46
47
|
}
|
|
48
|
+
loadProjects() {
|
|
49
|
+
this.projects.clear();
|
|
50
|
+
const jsonFile = projectsJson();
|
|
51
|
+
if (!fs.existsSync(jsonFile))
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
const raw = fs.readFileSync(jsonFile, "utf-8");
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
if (!Array.isArray(parsed))
|
|
57
|
+
return;
|
|
58
|
+
for (const item of parsed) {
|
|
59
|
+
if (item && item.id)
|
|
60
|
+
this.projects.set(item.id, item);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
daemonLog(`Failed to load projects.json: ${err}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
saveAllProjects() {
|
|
68
|
+
const all = Array.from(this.projects.values());
|
|
69
|
+
writeFileAtomic(projectsJson(), JSON.stringify(all, null, 2));
|
|
70
|
+
}
|
|
47
71
|
saveProject(project) {
|
|
48
|
-
const filePath = join(this.projectsDir, `${project.id}.json`);
|
|
49
|
-
writeFileAtomic(filePath, JSON.stringify(project, null, 2));
|
|
50
72
|
this.projects.set(project.id, project);
|
|
73
|
+
this.saveAllProjects();
|
|
51
74
|
}
|
|
52
75
|
createDefaultProject() {
|
|
53
76
|
const defaultProject = {
|
|
@@ -96,9 +119,7 @@ export class ProjectManager {
|
|
|
96
119
|
throw new Error(`Project ${id} not found`);
|
|
97
120
|
if (project.isSystem)
|
|
98
121
|
throw new Error("Cannot delete system project");
|
|
99
|
-
const filePath = join(this.projectsDir, `${id}.json`);
|
|
100
|
-
if (fs.existsSync(filePath))
|
|
101
|
-
fs.rmSync(filePath);
|
|
102
122
|
this.projects.delete(id);
|
|
123
|
+
this.saveAllProjects();
|
|
103
124
|
}
|
|
104
125
|
}
|
package/dist/daemon/state.js
CHANGED
|
@@ -3,27 +3,39 @@ import path from "node:path";
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { removeIfExists, writeFileAtomic } from "../shared/fs-utils.js";
|
|
6
|
-
import { getLoopsDir, getTasksDir, getLogsDir,
|
|
6
|
+
import { getLoopsDir, getTasksDir, getLogsDir, logFile, getPidFile, getSignatureFile, getSocketPath, loopsJson, tasksJson, getDataDir, } from "../config/paths.js";
|
|
7
7
|
export { getDataDir, getPidFile, getSocketPath } from "../config/paths.js";
|
|
8
8
|
function ensureDirs() {
|
|
9
|
-
fs.mkdirSync(
|
|
9
|
+
fs.mkdirSync(getDataDir(), { recursive: true });
|
|
10
10
|
fs.mkdirSync(getLogsDir(), { recursive: true });
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
function readJsonArray(filePath) {
|
|
13
|
+
if (!fs.existsSync(filePath))
|
|
14
|
+
return [];
|
|
15
|
+
try {
|
|
16
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return null;
|
|
20
|
-
const raw = fs.readFileSync(file, "utf-8");
|
|
21
|
-
return JSON.parse(raw);
|
|
24
|
+
function writeJsonArray(filePath, items) {
|
|
25
|
+
ensureDirs();
|
|
26
|
+
writeFileAtomic(filePath, JSON.stringify(items, null, 2));
|
|
22
27
|
}
|
|
23
|
-
export function
|
|
28
|
+
export function migrateLoopsToJson() {
|
|
24
29
|
ensureDirs();
|
|
30
|
+
const jsonFile = loopsJson();
|
|
31
|
+
if (fs.existsSync(jsonFile))
|
|
32
|
+
return;
|
|
25
33
|
const dir = getLoopsDir();
|
|
34
|
+
if (!fs.existsSync(dir))
|
|
35
|
+
return;
|
|
26
36
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
37
|
+
if (files.length === 0)
|
|
38
|
+
return;
|
|
27
39
|
const loops = [];
|
|
28
40
|
for (const file of files) {
|
|
29
41
|
try {
|
|
@@ -34,28 +46,20 @@ export function loadAllLoops() {
|
|
|
34
46
|
// skip corrupted files
|
|
35
47
|
}
|
|
36
48
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export function deleteLoop(id) {
|
|
40
|
-
removeIfExists(loopFile(id));
|
|
41
|
-
removeIfExists(logFile(id));
|
|
42
|
-
}
|
|
43
|
-
export function saveTask(task) {
|
|
44
|
-
const dir = getTasksDir();
|
|
45
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
-
writeFileAtomic(taskFile(task.id), JSON.stringify(task, null, 2));
|
|
47
|
-
}
|
|
48
|
-
export function loadTask(id) {
|
|
49
|
-
const file = taskFile(id);
|
|
50
|
-
if (!fs.existsSync(file))
|
|
51
|
-
return null;
|
|
52
|
-
const raw = fs.readFileSync(file, "utf-8");
|
|
53
|
-
return JSON.parse(raw);
|
|
49
|
+
loops.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
50
|
+
writeJsonArray(jsonFile, loops);
|
|
54
51
|
}
|
|
55
|
-
export function
|
|
52
|
+
export function migrateTasksToJson() {
|
|
53
|
+
ensureDirs();
|
|
54
|
+
const jsonFile = tasksJson();
|
|
55
|
+
if (fs.existsSync(jsonFile))
|
|
56
|
+
return;
|
|
56
57
|
const dir = getTasksDir();
|
|
57
|
-
fs.
|
|
58
|
+
if (!fs.existsSync(dir))
|
|
59
|
+
return;
|
|
58
60
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
61
|
+
if (files.length === 0)
|
|
62
|
+
return;
|
|
59
63
|
const tasks = [];
|
|
60
64
|
for (const file of files) {
|
|
61
65
|
try {
|
|
@@ -66,10 +70,57 @@ export function loadAllTasks() {
|
|
|
66
70
|
// skip corrupted files
|
|
67
71
|
}
|
|
68
72
|
}
|
|
73
|
+
tasks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
74
|
+
writeJsonArray(jsonFile, tasks);
|
|
75
|
+
}
|
|
76
|
+
export function saveLoop(meta) {
|
|
77
|
+
const loops = readJsonArray(loopsJson());
|
|
78
|
+
const idx = loops.findIndex((l) => l.id === meta.id);
|
|
79
|
+
if (idx >= 0)
|
|
80
|
+
loops[idx] = meta;
|
|
81
|
+
else
|
|
82
|
+
loops.push(meta);
|
|
83
|
+
writeJsonArray(loopsJson(), loops);
|
|
84
|
+
}
|
|
85
|
+
export function loadLoop(id) {
|
|
86
|
+
const loops = readJsonArray(loopsJson());
|
|
87
|
+
return loops.find((l) => l.id === id) ?? null;
|
|
88
|
+
}
|
|
89
|
+
export function loadAllLoops() {
|
|
90
|
+
const loops = readJsonArray(loopsJson());
|
|
91
|
+
return loops.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
92
|
+
}
|
|
93
|
+
export function deleteLoop(id) {
|
|
94
|
+
const loops = readJsonArray(loopsJson());
|
|
95
|
+
const filtered = loops.filter((l) => l.id !== id);
|
|
96
|
+
if (filtered.length !== loops.length) {
|
|
97
|
+
writeJsonArray(loopsJson(), filtered);
|
|
98
|
+
}
|
|
99
|
+
removeIfExists(logFile(id));
|
|
100
|
+
}
|
|
101
|
+
export function saveTask(task) {
|
|
102
|
+
const tasks = readJsonArray(tasksJson());
|
|
103
|
+
const idx = tasks.findIndex((t) => t.id === task.id);
|
|
104
|
+
if (idx >= 0)
|
|
105
|
+
tasks[idx] = task;
|
|
106
|
+
else
|
|
107
|
+
tasks.push(task);
|
|
108
|
+
writeJsonArray(tasksJson(), tasks);
|
|
109
|
+
}
|
|
110
|
+
export function loadTask(id) {
|
|
111
|
+
const tasks = readJsonArray(tasksJson());
|
|
112
|
+
return tasks.find((t) => t.id === id) ?? null;
|
|
113
|
+
}
|
|
114
|
+
export function loadAllTasks() {
|
|
115
|
+
const tasks = readJsonArray(tasksJson());
|
|
69
116
|
return tasks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
70
117
|
}
|
|
71
118
|
export function deleteTask(id) {
|
|
72
|
-
|
|
119
|
+
const tasks = readJsonArray(tasksJson());
|
|
120
|
+
const filtered = tasks.filter((t) => t.id !== id);
|
|
121
|
+
if (filtered.length !== tasks.length) {
|
|
122
|
+
writeJsonArray(tasksJson(), filtered);
|
|
123
|
+
}
|
|
73
124
|
}
|
|
74
125
|
export function getLogPath(id) {
|
|
75
126
|
ensureDirs();
|
package/dist/i18n/en.json
CHANGED
|
@@ -275,6 +275,7 @@
|
|
|
275
275
|
"board.actionPause": "Pause",
|
|
276
276
|
"board.actionStop": "Stop",
|
|
277
277
|
"board.actionPlay": "Play",
|
|
278
|
+
"board.actionClone": "Clone",
|
|
278
279
|
"board.actionTrigger": "Force Run",
|
|
279
280
|
"board.noActions": "Select a loop to see actions",
|
|
280
281
|
"board.confirmPause": "Pause loop \"{desc}\"?",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|