loop-task 1.4.3 → 1.4.5
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 +8 -1
- package/dist/board/components/CreateForm.js +40 -16
- package/dist/board/components/HelpModal.js +8 -5
- package/dist/board/components/LogModal.js +27 -3
- package/dist/board/components/ProjectsPage.js +21 -2
- package/dist/board/components/SearchBox.js +5 -1
- package/dist/board/components/TaskForm.js +25 -3
- package/dist/board/hooks/useBoardKeybindings.js +105 -13
- package/dist/board/hooks/useInputShortcuts.js +42 -0
- package/dist/board/hooks/useTaskKeybindings.js +41 -10
- package/dist/core/loop-controller.js +4 -0
- package/dist/daemon/index.js +3 -1
- package/dist/daemon/manager.js +1 -4
- package/dist/daemon/server.js +10 -7
- package/dist/i18n/en.json +1 -0
- package/dist/shared/clipboard.js +22 -0
- package/package.json +1 -1
package/dist/board/App.js
CHANGED
|
@@ -310,7 +310,9 @@ export function App(props) {
|
|
|
310
310
|
refreshTasks,
|
|
311
311
|
onViewTasks: () => { void refreshTasks(); push("task-list"); },
|
|
312
312
|
onViewProjects: () => push("projects"),
|
|
313
|
+
onViewLoops: () => replace("board"),
|
|
313
314
|
onAddLoop: () => { setEditTarget(null); push("create"); },
|
|
315
|
+
onAddTask: () => { setEditTask(null); push("task-create"); },
|
|
314
316
|
onSelectProject: () => setProjectsModalOpen(true),
|
|
315
317
|
});
|
|
316
318
|
useTaskKeybindings({
|
|
@@ -330,6 +332,9 @@ export function App(props) {
|
|
|
330
332
|
onTaskAction: handleTaskAction,
|
|
331
333
|
onCancel: cancelTaskList,
|
|
332
334
|
onCreateTask: handleCreateTask,
|
|
335
|
+
onEnterHeader: (direction) => {
|
|
336
|
+
setFocusedPanel(direction === "right" ? "header-tasks" : "header-new");
|
|
337
|
+
},
|
|
333
338
|
selectable: stack.includes("create") || stack.includes("task-edit"),
|
|
334
339
|
});
|
|
335
340
|
const counts = {
|
|
@@ -345,7 +350,9 @@ export function App(props) {
|
|
|
345
350
|
}
|
|
346
351
|
else {
|
|
347
352
|
push("projects");
|
|
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; }
|
|
353
|
+
} } }), 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; }, onEnterHeader: (direction) => {
|
|
354
|
+
setFocusedPanel(direction === "right" ? "header-tasks" : "header-new");
|
|
355
|
+
} })) : (_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) {
|
|
349
356
|
setEditTarget(loop);
|
|
350
357
|
push("create");
|
|
351
358
|
} } }), _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 })] }));
|
|
@@ -7,6 +7,7 @@ import { t } from "../../i18n/index.js";
|
|
|
7
7
|
import { commandLine } from "../format.js";
|
|
8
8
|
import { createLoop, updateLoop, listTasks } from "../daemon.js";
|
|
9
9
|
import { useHoverState } from "../hooks/useHoverState.js";
|
|
10
|
+
import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
|
|
10
11
|
import { HOVER_BG } from "../../config/constants.js";
|
|
11
12
|
export const TASK_MODE_INLINE = "inline";
|
|
12
13
|
export const TASK_MODE_EXISTING = "existing";
|
|
@@ -47,6 +48,12 @@ export function CreateView(props) {
|
|
|
47
48
|
const [error, setError] = useState("");
|
|
48
49
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
49
50
|
const [selectedTaskName, setSelectedTaskName] = useState(props.selectedTaskName ?? null);
|
|
51
|
+
const inputRef = useRef(null);
|
|
52
|
+
useInputShortcuts(() => {
|
|
53
|
+
if (focusIndex >= saveIndex)
|
|
54
|
+
return null;
|
|
55
|
+
return inputRef.current;
|
|
56
|
+
});
|
|
50
57
|
useState(() => {
|
|
51
58
|
if (values.taskId && !selectedTaskName) {
|
|
52
59
|
void listTasks().then((tasks) => {
|
|
@@ -92,6 +99,31 @@ export function CreateView(props) {
|
|
|
92
99
|
return 0;
|
|
93
100
|
return next;
|
|
94
101
|
});
|
|
102
|
+
key.preventDefault();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (key.name === "up" || key.name === "down") {
|
|
106
|
+
const field = filteredFields[focusIndex];
|
|
107
|
+
if (field === "project" && projectOptions.length > 1) {
|
|
108
|
+
const currentIdx = projectOptions.findIndex((p) => p.value === valuesRef.current.project);
|
|
109
|
+
const nextIdx = key.name === "down"
|
|
110
|
+
? (currentIdx + 1) % projectOptions.length
|
|
111
|
+
: (currentIdx - 1 + projectOptions.length) % projectOptions.length;
|
|
112
|
+
const next = projectOptions[nextIdx];
|
|
113
|
+
if (next)
|
|
114
|
+
updateValues({ ...valuesRef.current, project: next.value });
|
|
115
|
+
key.preventDefault();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
setFocusIndex((i) => {
|
|
119
|
+
const next = key.name === "up" ? i - 1 : i + 1;
|
|
120
|
+
if (next < 0)
|
|
121
|
+
return cancelIndex;
|
|
122
|
+
if (next > cancelIndex)
|
|
123
|
+
return 0;
|
|
124
|
+
return next;
|
|
125
|
+
});
|
|
126
|
+
key.preventDefault();
|
|
95
127
|
return;
|
|
96
128
|
}
|
|
97
129
|
if (key.name === "return" || key.name === "enter" || key.name === " ") {
|
|
@@ -99,35 +131,27 @@ export function CreateView(props) {
|
|
|
99
131
|
if (field === "taskMode") {
|
|
100
132
|
const next = valuesRef.current.taskMode === TASK_MODE_INLINE ? TASK_MODE_EXISTING : TASK_MODE_INLINE;
|
|
101
133
|
updateValues({ ...valuesRef.current, taskMode: next });
|
|
134
|
+
key.preventDefault();
|
|
102
135
|
return;
|
|
103
136
|
}
|
|
104
137
|
if (field === "runNow") {
|
|
105
138
|
const next = valuesRef.current.runNow === "y" ? "n" : "y";
|
|
106
139
|
updateValues({ ...valuesRef.current, runNow: next });
|
|
140
|
+
key.preventDefault();
|
|
107
141
|
return;
|
|
108
142
|
}
|
|
109
143
|
if (focusIndex === chooseTaskIdx && !isInline) {
|
|
110
144
|
props.onChooseTask();
|
|
145
|
+
key.preventDefault();
|
|
111
146
|
return;
|
|
112
147
|
}
|
|
113
148
|
if (focusIndex === saveIndex) {
|
|
114
149
|
void submit(valuesRef.current);
|
|
150
|
+
key.preventDefault();
|
|
115
151
|
}
|
|
116
152
|
else if (focusIndex === cancelIndex) {
|
|
117
153
|
props.onCancel();
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
if (key.name === "up" || key.name === "down") {
|
|
121
|
-
const field = filteredFields[focusIndex];
|
|
122
|
-
if (field === "project" && projectOptions.length > 1) {
|
|
123
|
-
const currentIdx = projectOptions.findIndex((p) => p.value === valuesRef.current.project);
|
|
124
|
-
const nextIdx = key.name === "down"
|
|
125
|
-
? (currentIdx + 1) % projectOptions.length
|
|
126
|
-
: (currentIdx - 1 + projectOptions.length) % projectOptions.length;
|
|
127
|
-
const next = projectOptions[nextIdx];
|
|
128
|
-
if (next)
|
|
129
|
-
updateValues({ ...valuesRef.current, project: next.value });
|
|
130
|
-
return;
|
|
154
|
+
key.preventDefault();
|
|
131
155
|
}
|
|
132
156
|
}
|
|
133
157
|
});
|
|
@@ -254,7 +278,7 @@ export function CreateView(props) {
|
|
|
254
278
|
return (_jsxs("box", { title: title, border: true, style: { flexDirection: "column", flexGrow: 1, padding: 1, backgroundColor: "#0b0b0b" }, children: [_jsxs("box", { style: { flexDirection: "column", marginBottom: 1 }, children: [_jsx("text", { fg: "#9ca3af", children: t("board.exampleHeading") }), _jsx("text", { fg: "#d1d5db", children: t("board.exampleFull") })] }), _jsx("box", { style: { flexDirection: "column", flexGrow: 1 }, children: Array.from({ length: Math.ceil(filteredFields.length / 2) }, (_, row) => {
|
|
255
279
|
const leftField = filteredFields[row * 2];
|
|
256
280
|
const rightField = row * 2 + 1 < filteredFields.length ? filteredFields[row * 2 + 1] : null;
|
|
257
|
-
return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(FormRow, { field: leftField, index: row * 2, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, taskModeOptions: taskModeOptions, runNowOptions: runNowOptions, projectOptions: projectOptions, selectedTaskName: selectedTaskName, fields: filteredFields, onChooseTask: props.onChooseTask, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(FormRow, { field: rightField, index: row * 2 + 1, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, taskModeOptions: taskModeOptions, runNowOptions: runNowOptions, projectOptions: projectOptions, selectedTaskName: selectedTaskName, fields: filteredFields, onChooseTask: props.onChooseTask, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
|
|
281
|
+
return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(FormRow, { field: leftField, index: row * 2, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, taskModeOptions: taskModeOptions, runNowOptions: runNowOptions, projectOptions: projectOptions, selectedTaskName: selectedTaskName, fields: filteredFields, onChooseTask: props.onChooseTask, inputRef: inputRef, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(FormRow, { field: rightField, index: row * 2 + 1, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, examples: examples, taskModeOptions: taskModeOptions, runNowOptions: runNowOptions, projectOptions: projectOptions, selectedTaskName: selectedTaskName, fields: filteredFields, onChooseTask: props.onChooseTask, inputRef: inputRef, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
|
|
258
282
|
}) }), _jsxs("box", { style: { flexDirection: "row", height: 3, marginBottom: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(HoverButton, { label: isSubmitting ? t("board.saving") : props.mode === "edit" ? t("board.save") : t("board.create"), onMouseDown: () => void submit(valuesRef.current), selected: focusIndex === saveIndex, width: btnWidth, marginRight: 1 }), _jsx(HoverButton, { label: t("board.cancel"), onMouseDown: props.onCancel, selected: focusIndex === cancelIndex, width: btnWidth })] }), _jsx("text", { fg: "#9ca3af", children: t("board.formNav") }), error ? _jsx("text", { fg: "#f87171", children: error }) : null] }));
|
|
259
283
|
}
|
|
260
284
|
function HoverButton(props) {
|
|
@@ -264,7 +288,7 @@ function HoverButton(props) {
|
|
|
264
288
|
return (_jsx("box", { border: true, onMouseDown: props.onMouseDown, borderColor: borderColor, style: { width: props.width, justifyContent: "center", alignItems: "center", marginRight: props.marginRight, backgroundColor: bg }, ...hoverProps, children: _jsx("text", { fg: "#e5e7eb", children: _jsx("strong", { children: props.label }) }) }));
|
|
265
289
|
}
|
|
266
290
|
function FormRow(props) {
|
|
267
|
-
const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, examples, taskModeOptions, runNowOptions, projectOptions, selectedTaskName, fields, onChooseTask, style } = props;
|
|
291
|
+
const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, examples, taskModeOptions, runNowOptions, projectOptions, selectedTaskName, fields, onChooseTask, inputRef, style } = props;
|
|
268
292
|
const { isHovered, hoverProps } = useHoverState();
|
|
269
293
|
const isFocused = focusIndex === index;
|
|
270
294
|
const isToggleField = field === "taskMode" || field === "runNow";
|
|
@@ -280,7 +304,7 @@ function FormRow(props) {
|
|
|
280
304
|
: t("board.chooseTask") }) })) : isProjectField ? (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, flexDirection: "row", backgroundColor: "#0b0b0b", alignItems: "center", paddingLeft: 1 }, onMouseDown: () => setFocusIndex(index), children: projectOptions.map((opt) => {
|
|
281
305
|
const isActive = values.project === opt.value;
|
|
282
306
|
return (_jsx("box", { onMouseDown: () => updateValues({ ...valuesRef.current, project: opt.value }), style: { backgroundColor: isActive ? "#1e3a8a" : undefined, paddingLeft: 1, paddingRight: 1, marginRight: 1 }, children: _jsx("text", { fg: isActive ? "#ffffff" : "#9ca3af", children: `● ${opt.name}` }) }, opt.value));
|
|
283
|
-
}) })) : (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b" }, children: _jsx("input", { focused: isFocused, value: values[field], placeholder: examples[field] ? t("board.placeholderExample", { example: examples[field] }) : t("board.placeholderBlank"), onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
|
|
307
|
+
}) })) : (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b" }, children: _jsx("input", { ref: inputRef, focused: isFocused, value: values[field], placeholder: examples[field] ? t("board.placeholderExample", { example: examples[field] }) : t("board.placeholderBlank"), onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
|
|
284
308
|
if (index < fields.length - 1) {
|
|
285
309
|
setFocusIndex(index + 1);
|
|
286
310
|
}
|
|
@@ -5,40 +5,43 @@ export function HelpModal(props) {
|
|
|
5
5
|
const { view } = props;
|
|
6
6
|
const { width } = useTerminalDimensions();
|
|
7
7
|
const loopRows = [
|
|
8
|
+
["tab", "switch panels"],
|
|
9
|
+
["up/down", "navigate list items"],
|
|
10
|
+
["enter", t("board.helpEdit")],
|
|
8
11
|
["e", "edit loop"],
|
|
9
12
|
["d/del", "delete loop"],
|
|
10
13
|
["c", "clone loop"],
|
|
11
14
|
["f", "force run"],
|
|
12
15
|
["p", "pause/play (contextual)"],
|
|
13
16
|
["s", "stop loop (resets schedule)"],
|
|
14
|
-
[t("board.helpKeyEnter"), t("board.helpEdit")],
|
|
15
17
|
["n", t("board.helpCreate")],
|
|
16
18
|
["t", t("board.helpCreateTask")],
|
|
17
19
|
["o", t("board.helpCycleSort")],
|
|
18
20
|
["x", "cycle status filter"],
|
|
19
21
|
["r", "project filter"],
|
|
20
|
-
["←/→", t("board.helpSwitchPanel")],
|
|
21
22
|
["/", t("board.helpSearch")],
|
|
22
23
|
["h", t("board.helpToggleHelp")],
|
|
23
24
|
["esc", t("board.helpQuit")],
|
|
24
25
|
];
|
|
25
26
|
const taskRows = [
|
|
27
|
+
["tab", "switch panels"],
|
|
28
|
+
["up/down", "navigate tasks"],
|
|
29
|
+
["enter", "focus actions panel"],
|
|
26
30
|
["e", "edit task"],
|
|
27
31
|
["d/del", "delete task"],
|
|
28
32
|
["s", "select task (when creating/editing a loop)"],
|
|
29
|
-
["enter", "focus actions panel"],
|
|
30
33
|
["n", t("board.helpCreateTask")],
|
|
31
|
-
["←/→", t("board.helpSwitchPanel")],
|
|
32
34
|
["/", t("board.helpSearch")],
|
|
33
35
|
["h", t("board.helpToggleHelp")],
|
|
34
36
|
["esc", t("board.helpQuit")],
|
|
35
37
|
];
|
|
36
38
|
const projectRows = [
|
|
39
|
+
["tab", "switch panel (list/actions)"],
|
|
40
|
+
["up/down", "navigate projects"],
|
|
37
41
|
["enter", "focus actions"],
|
|
38
42
|
["n", "new project"],
|
|
39
43
|
["e", "edit project"],
|
|
40
44
|
["d", "delete project"],
|
|
41
|
-
["←/→", "switch panel (list/actions)"],
|
|
42
45
|
["h", t("board.helpToggleHelp")],
|
|
43
46
|
["esc", t("board.helpQuit")],
|
|
44
47
|
];
|
|
@@ -6,12 +6,31 @@ import { formatRunDuration, formatRunTime } from "../format.js";
|
|
|
6
6
|
import { streamRunLog } from "../daemon.js";
|
|
7
7
|
import { LOG_LINES_MAX } from "../../config/constants.js";
|
|
8
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
|
+
}
|
|
9
27
|
export function LogModal(props) {
|
|
10
28
|
const { loopId, run, logLines: staticLines, loading } = props;
|
|
11
29
|
const { width, height } = useTerminalDimensions();
|
|
12
30
|
const isRunning = run.status === "running";
|
|
13
31
|
const [streamLines, setStreamLines] = useState([]);
|
|
14
32
|
const socketRef = useRef(null);
|
|
33
|
+
const scrollRef = useRef(null);
|
|
15
34
|
useEffect(() => {
|
|
16
35
|
if (!isRunning || !loopId)
|
|
17
36
|
return;
|
|
@@ -28,13 +47,18 @@ export function LogModal(props) {
|
|
|
28
47
|
}
|
|
29
48
|
};
|
|
30
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]);
|
|
31
56
|
useKeyboard((key) => {
|
|
32
57
|
if (key.ctrl && key.name === "c") {
|
|
33
|
-
const all =
|
|
58
|
+
const all = logLines.join("\n");
|
|
34
59
|
copyToClipboard(all);
|
|
35
60
|
}
|
|
36
61
|
});
|
|
37
|
-
const logLines = isRunning ? streamLines : staticLines;
|
|
38
62
|
const success = isRunning ? true : run.exitCode === 0;
|
|
39
63
|
const icon = isRunning ? "⟳" : success ? "✓" : "✗";
|
|
40
64
|
const iconColor = isRunning ? "#facc15" : success ? "#4ade80" : "#f87171";
|
|
@@ -54,5 +78,5 @@ export function LogModal(props) {
|
|
|
54
78
|
width: Math.min(width - 4, 100),
|
|
55
79
|
height: Math.min(height - 4, 30),
|
|
56
80
|
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("
|
|
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") })] })] }) }));
|
|
58
82
|
}
|
|
@@ -15,7 +15,7 @@ function ProjectActionButton(props) {
|
|
|
15
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
16
|
}
|
|
17
17
|
export function ProjectsPage(props) {
|
|
18
|
-
const { projects, loops, onClose, onRefresh, onOpenCreate } = props;
|
|
18
|
+
const { projects, loops, onClose, onRefresh, onOpenCreate, onEnterHeader } = props;
|
|
19
19
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
20
20
|
const [subModal, setSubModal] = useState("none");
|
|
21
21
|
const [focusedPanel, setFocusedPanel] = useState("list");
|
|
@@ -41,6 +41,7 @@ export function ProjectsPage(props) {
|
|
|
41
41
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
42
42
|
else
|
|
43
43
|
setSelectedAction((a) => Math.max(0, a - 1));
|
|
44
|
+
key.preventDefault();
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
47
|
if (key.name === "down") {
|
|
@@ -48,10 +49,23 @@ export function ProjectsPage(props) {
|
|
|
48
49
|
setSelectedIndex((i) => Math.min(projects.length - 1, i + 1));
|
|
49
50
|
else
|
|
50
51
|
setSelectedAction((a) => Math.min(PROJECT_ACTION_COUNT - 1, a + 1));
|
|
52
|
+
key.preventDefault();
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
|
-
if (key.name === "
|
|
55
|
+
if (key.name === "tab") {
|
|
56
|
+
const direction = key.shift ? "left" : "right";
|
|
57
|
+
if (focusedPanel === "list" && direction === "left") {
|
|
58
|
+
onEnterHeader?.("left");
|
|
59
|
+
key.preventDefault();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (focusedPanel === "actions" && direction === "right") {
|
|
63
|
+
onEnterHeader?.("right");
|
|
64
|
+
key.preventDefault();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
54
67
|
setFocusedPanel((p) => (p === "list" ? "actions" : "list"));
|
|
68
|
+
key.preventDefault();
|
|
55
69
|
return;
|
|
56
70
|
}
|
|
57
71
|
if (key.name === "return" || key.name === "enter") {
|
|
@@ -61,22 +75,27 @@ export function ProjectsPage(props) {
|
|
|
61
75
|
else {
|
|
62
76
|
runAction(selectedAction);
|
|
63
77
|
}
|
|
78
|
+
key.preventDefault();
|
|
64
79
|
return;
|
|
65
80
|
}
|
|
66
81
|
if (key.name === "n") {
|
|
67
82
|
setSubModal("create");
|
|
83
|
+
key.preventDefault();
|
|
68
84
|
return;
|
|
69
85
|
}
|
|
70
86
|
if (key.name === "e" && selectedProject && !selectedProject.isSystem) {
|
|
71
87
|
setSubModal("edit");
|
|
88
|
+
key.preventDefault();
|
|
72
89
|
return;
|
|
73
90
|
}
|
|
74
91
|
if (key.name === "d" && selectedProject && !selectedProject.isSystem) {
|
|
75
92
|
setSubModal("delete");
|
|
93
|
+
key.preventDefault();
|
|
76
94
|
return;
|
|
77
95
|
}
|
|
78
96
|
if (key.name === "escape") {
|
|
79
97
|
onClose();
|
|
98
|
+
key.preventDefault();
|
|
80
99
|
}
|
|
81
100
|
});
|
|
82
101
|
const actions = [
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useRef } from "react";
|
|
2
3
|
import { t } from "../../i18n/index.js";
|
|
4
|
+
import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
|
|
3
5
|
export function SearchBox(props) {
|
|
4
6
|
const { query, searchActive, focused, onQueryChange, onActivate, onDismiss } = props;
|
|
5
|
-
|
|
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") })) }));
|
|
6
10
|
}
|
|
@@ -5,6 +5,7 @@ import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
|
5
5
|
import { t } from "../../i18n/index.js";
|
|
6
6
|
import { createTask, updateTask, listTasks } from "../daemon.js";
|
|
7
7
|
import { useHoverState } from "../hooks/useHoverState.js";
|
|
8
|
+
import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
|
|
8
9
|
import { HOVER_BG } from "../../config/constants.js";
|
|
9
10
|
const taskFields = ["name", "command", "onSuccessTaskId", "onFailureTaskId"];
|
|
10
11
|
function taskInitialValues(task) {
|
|
@@ -25,8 +26,14 @@ export function TaskForm(props) {
|
|
|
25
26
|
const [error, setError] = useState("");
|
|
26
27
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
27
28
|
const [allTasks, setAllTasks] = useState([]);
|
|
29
|
+
const inputRef = useRef(null);
|
|
28
30
|
const { width: termWidth } = useTerminalDimensions();
|
|
29
31
|
const btnWidth = Math.max(10, Math.min(14, Math.floor(termWidth / 6)));
|
|
32
|
+
useInputShortcuts(() => {
|
|
33
|
+
if (focusIndex >= saveIndex)
|
|
34
|
+
return null;
|
|
35
|
+
return inputRef.current;
|
|
36
|
+
});
|
|
30
37
|
const saveIndex = taskFields.length;
|
|
31
38
|
const cancelIndex = taskFields.length + 1;
|
|
32
39
|
function updateValues(next) {
|
|
@@ -46,14 +53,29 @@ export function TaskForm(props) {
|
|
|
46
53
|
return 0;
|
|
47
54
|
return next;
|
|
48
55
|
});
|
|
56
|
+
key.preventDefault();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (key.name === "up" || key.name === "down") {
|
|
60
|
+
setFocusIndex((i) => {
|
|
61
|
+
const next = key.name === "up" ? i - 1 : i + 1;
|
|
62
|
+
if (next < 0)
|
|
63
|
+
return cancelIndex;
|
|
64
|
+
if (next > cancelIndex)
|
|
65
|
+
return 0;
|
|
66
|
+
return next;
|
|
67
|
+
});
|
|
68
|
+
key.preventDefault();
|
|
49
69
|
return;
|
|
50
70
|
}
|
|
51
71
|
if (key.name === "return" || key.name === "enter") {
|
|
52
72
|
if (focusIndex === saveIndex) {
|
|
53
73
|
void submit(valuesRef.current);
|
|
74
|
+
key.preventDefault();
|
|
54
75
|
}
|
|
55
76
|
else if (focusIndex === cancelIndex) {
|
|
56
77
|
props.onCancel();
|
|
78
|
+
key.preventDefault();
|
|
57
79
|
}
|
|
58
80
|
}
|
|
59
81
|
});
|
|
@@ -125,16 +147,16 @@ export function TaskForm(props) {
|
|
|
125
147
|
return (_jsxs("box", { title: title, border: true, style: { flexDirection: "column", flexGrow: 1, padding: 1, backgroundColor: "#0b0b0b" }, children: [_jsx("box", { style: { flexDirection: "column", flexGrow: 1 }, children: Array.from({ length: Math.ceil(taskFields.length / 2) }, (_, row) => {
|
|
126
148
|
const leftField = taskFields[row * 2];
|
|
127
149
|
const rightField = row * 2 + 1 < taskFields.length ? taskFields[row * 2 + 1] : null;
|
|
128
|
-
return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(TaskFormRow, { field: leftField, index: row * 2, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(TaskFormRow, { field: rightField, index: row * 2 + 1, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
|
|
150
|
+
return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(TaskFormRow, { field: leftField, index: row * 2, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, inputRef: inputRef, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(TaskFormRow, { field: rightField, index: row * 2 + 1, focusIndex: focusIndex, values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, inputRef: inputRef, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
|
|
129
151
|
}) }), _jsxs("box", { style: { flexDirection: "row", height: 3, marginBottom: 1, backgroundColor: "#0b0b0b" }, children: [_jsx(HoverButton, { label: isSubmitting ? t("board.saving") : props.mode === "edit" ? t("board.save") : t("board.create"), onMouseDown: () => void submit(valuesRef.current), selected: focusIndex === saveIndex, width: btnWidth, marginRight: 1 }), _jsx(HoverButton, { label: t("board.cancel"), onMouseDown: props.onCancel, selected: focusIndex === cancelIndex, width: btnWidth })] }), _jsx("text", { fg: "#9ca3af", children: t("board.formNav") }), error ? _jsx("text", { fg: "#f87171", children: error }) : null] }));
|
|
130
152
|
}
|
|
131
153
|
function TaskFormRow(props) {
|
|
132
|
-
const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, chainOptions, style } = props;
|
|
154
|
+
const { field, index, focusIndex, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, chainOptions, inputRef, style } = props;
|
|
133
155
|
const isFocused = focusIndex === index;
|
|
134
156
|
const isSelect = field === "onSuccessTaskId" || field === "onFailureTaskId";
|
|
135
157
|
const selectOpts = isSelect ? chainOptions : [];
|
|
136
158
|
const selectedIdx = isSelect ? selectOpts.findIndex((o) => o.value === values[field]) : 0;
|
|
137
|
-
return (_jsxs("box", { style: { flexDirection: "column", ...style }, children: [_jsx("text", { fg: isFocused ? "#38bdf8" : "#e5e7eb", children: labels[field] }), _jsx("text", { fg: "#6b7280", children: hints[field] }), isSelect ? (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: Math.min(selectOpts.length + 2, 6), backgroundColor: "#0b0b0b" }, children: _jsx("select", { focused: isFocused, options: selectOpts, selectedIndex: Math.max(0, selectedIdx), showDescription: false, style: { flexGrow: 1 }, onChange: (_i, option) => updateValues({ ...valuesRef.current, [field]: option?.value ?? "" }) }) })) : (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b" }, children: _jsx("input", { focused: isFocused, value: values[field], placeholder: field === "command" ? t("board.exampleCommand") : "", onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
|
|
159
|
+
return (_jsxs("box", { style: { flexDirection: "column", ...style }, children: [_jsx("text", { fg: isFocused ? "#38bdf8" : "#e5e7eb", children: labels[field] }), _jsx("text", { fg: "#6b7280", children: hints[field] }), isSelect ? (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: Math.min(selectOpts.length + 2, 6), backgroundColor: "#0b0b0b" }, children: _jsx("select", { focused: isFocused, options: selectOpts, selectedIndex: Math.max(0, selectedIdx), showDescription: false, style: { flexGrow: 1 }, onChange: (_i, option) => updateValues({ ...valuesRef.current, [field]: option?.value ?? "" }) }) })) : (_jsx("box", { border: true, borderColor: isFocused ? "#38bdf8" : undefined, style: { height: 3, backgroundColor: "#0b0b0b" }, children: _jsx("input", { ref: inputRef, focused: isFocused, value: values[field], placeholder: field === "command" ? t("board.exampleCommand") : "", onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
|
|
138
160
|
if (index < taskFields.length - 1) {
|
|
139
161
|
setFocusIndex(index + 1);
|
|
140
162
|
}
|
|
@@ -195,26 +195,30 @@ const BOARD_SHORTCUTS = {
|
|
|
195
195
|
r: (p) => p.onSelectProject?.(),
|
|
196
196
|
};
|
|
197
197
|
export function useBoardKeybindings(params) {
|
|
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;
|
|
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, onViewLoops, onAddLoop, onAddTask, onSelectProject, } = params;
|
|
199
199
|
useKeyboard((key) => {
|
|
200
200
|
const name = key.name;
|
|
201
201
|
if (confirm) {
|
|
202
202
|
CONFIRM_KEYS[name]?.({ confirm, confirmChoice, setConfirm, setConfirmChoice });
|
|
203
|
+
key.preventDefault();
|
|
203
204
|
return;
|
|
204
205
|
}
|
|
205
206
|
if (logModalRun) {
|
|
206
207
|
if (name === "escape" || name === "q") {
|
|
207
208
|
OVERLAY_DISMISS.log({ setLogModalRun, setHelpOpen, setSearchActive, setFocusedPanel }, name);
|
|
209
|
+
key.preventDefault();
|
|
208
210
|
return;
|
|
209
211
|
}
|
|
210
212
|
if (key.ctrl && name === "c") {
|
|
211
213
|
copyToClipboard(logModalLines.join("\n"));
|
|
214
|
+
key.preventDefault();
|
|
212
215
|
return;
|
|
213
216
|
}
|
|
214
217
|
return;
|
|
215
218
|
}
|
|
216
219
|
if (helpOpen && (name === "h" || name === "escape")) {
|
|
217
220
|
OVERLAY_DISMISS.help({ setLogModalRun, setHelpOpen, setSearchActive, setFocusedPanel }, name);
|
|
221
|
+
key.preventDefault();
|
|
218
222
|
return;
|
|
219
223
|
}
|
|
220
224
|
if (helpOpen)
|
|
@@ -223,27 +227,116 @@ export function useBoardKeybindings(params) {
|
|
|
223
227
|
if (name === "escape") {
|
|
224
228
|
setSearchActive(false);
|
|
225
229
|
setFocusedPanel("loops");
|
|
230
|
+
key.preventDefault();
|
|
226
231
|
return;
|
|
227
232
|
}
|
|
233
|
+
if (name === "up" || name === "k" || name === "down" || name === "j") {
|
|
234
|
+
const handler = panelHandlers["loops"];
|
|
235
|
+
if (handler) {
|
|
236
|
+
handler(name, {
|
|
237
|
+
setSearchActive, setFilters, setSort, setEditTarget, push,
|
|
238
|
+
setSelectedIndex, setSelectedRunIndex, setFocusedPanel, selectedRunCount, selected,
|
|
239
|
+
selectedRunIndex, setSelectedAction, selectedAction, onAction, onOpenRunLog,
|
|
240
|
+
refreshTasks, onViewTasks, onViewProjects, onAddLoop,
|
|
241
|
+
});
|
|
242
|
+
key.preventDefault();
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (view !== "board" && name === "escape") {
|
|
249
|
+
VIEW_ESCAPE[view]?.({ setEditTarget, setEditTask, onBack: pop });
|
|
250
|
+
key.preventDefault();
|
|
228
251
|
return;
|
|
229
252
|
}
|
|
230
|
-
if (name === "
|
|
253
|
+
if (name === "tab" && view !== "board") {
|
|
254
|
+
const HEADER_PANELS = ["header-tasks", "header-projects", "header-new"];
|
|
255
|
+
const isHeader = HEADER_PANELS.includes(focusedPanel);
|
|
256
|
+
const direction = key.shift ? "left" : "right";
|
|
257
|
+
if (isHeader) {
|
|
258
|
+
const idx = HEADER_PANELS.indexOf(focusedPanel);
|
|
259
|
+
const nextIdx = direction === "right"
|
|
260
|
+
? (idx + 1) % HEADER_PANELS.length
|
|
261
|
+
: (idx - 1 + HEADER_PANELS.length) % HEADER_PANELS.length;
|
|
262
|
+
setFocusedPanel(HEADER_PANELS[nextIdx]);
|
|
263
|
+
key.preventDefault();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (view !== "board" && (name === "return" || name === "enter")) {
|
|
268
|
+
if (view === "task-list") {
|
|
269
|
+
if (focusedPanel === "header-tasks") {
|
|
270
|
+
onViewProjects?.();
|
|
271
|
+
key.preventDefault();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (focusedPanel === "header-projects") {
|
|
275
|
+
onViewLoops?.();
|
|
276
|
+
key.preventDefault();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (focusedPanel === "header-new") {
|
|
280
|
+
onAddTask?.();
|
|
281
|
+
key.preventDefault();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else if (view === "projects") {
|
|
286
|
+
if (focusedPanel === "header-tasks") {
|
|
287
|
+
onViewLoops?.();
|
|
288
|
+
key.preventDefault();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (focusedPanel === "header-projects") {
|
|
292
|
+
onViewTasks?.();
|
|
293
|
+
key.preventDefault();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (focusedPanel === "header-new") {
|
|
297
|
+
onViewProjects?.();
|
|
298
|
+
key.preventDefault();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else if (view === "create" || view === "task-create" || view === "task-edit") {
|
|
303
|
+
if (focusedPanel === "header-tasks") {
|
|
304
|
+
onViewProjects?.();
|
|
305
|
+
key.preventDefault();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (focusedPanel === "header-projects") {
|
|
309
|
+
onViewTasks?.();
|
|
310
|
+
key.preventDefault();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (focusedPanel === "header-new") {
|
|
314
|
+
onAddLoop?.();
|
|
315
|
+
key.preventDefault();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (view !== "board")
|
|
321
|
+
return;
|
|
322
|
+
if (name === "tab") {
|
|
323
|
+
const direction = key.shift ? "left" : "right";
|
|
231
324
|
if (focusedPanel === "actions") {
|
|
232
325
|
const actionCount = selected ? getActionCount(selected.status) : 0;
|
|
233
|
-
if (
|
|
326
|
+
if (direction === "left" && selectedAction === 0) {
|
|
234
327
|
setFocusedPanel((p) => nextPanel(p, "left"));
|
|
235
328
|
}
|
|
236
|
-
else if (
|
|
329
|
+
else if (direction === "right" && selectedAction === actionCount - 1) {
|
|
237
330
|
setFocusedPanel((p) => nextPanel(p, "right"));
|
|
238
331
|
}
|
|
239
332
|
else {
|
|
240
|
-
setSelectedAction((i) =>
|
|
333
|
+
setSelectedAction((i) => direction === "right"
|
|
241
334
|
? Math.min(actionCount - 1, i + 1)
|
|
242
335
|
: Math.max(0, i - 1));
|
|
243
336
|
}
|
|
244
337
|
}
|
|
245
338
|
else {
|
|
246
|
-
const next = nextPanel(focusedPanel,
|
|
339
|
+
const next = nextPanel(focusedPanel, direction);
|
|
247
340
|
if (next === "actions") {
|
|
248
341
|
setFocusedPanel("actions");
|
|
249
342
|
setSelectedAction(0);
|
|
@@ -252,17 +345,13 @@ export function useBoardKeybindings(params) {
|
|
|
252
345
|
setFocusedPanel(next);
|
|
253
346
|
}
|
|
254
347
|
}
|
|
348
|
+
key.preventDefault();
|
|
255
349
|
return;
|
|
256
350
|
}
|
|
257
|
-
if (view !== "board" && name === "escape") {
|
|
258
|
-
VIEW_ESCAPE[view]?.({ setEditTarget, setEditTask, onBack: pop });
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
if (view !== "board")
|
|
262
|
-
return;
|
|
263
351
|
const globalHandler = GLOBAL_KEYS[name];
|
|
264
352
|
if (globalHandler) {
|
|
265
353
|
globalHandler({ destroyLogSocket, onQuit, setHelpOpen, setEditTarget, setEditTask, push, onAction });
|
|
354
|
+
key.preventDefault();
|
|
266
355
|
return;
|
|
267
356
|
}
|
|
268
357
|
const panelHandler = panelHandlers[focusedPanel];
|
|
@@ -271,8 +360,10 @@ export function useBoardKeybindings(params) {
|
|
|
271
360
|
setSelectedIndex, setSelectedRunIndex, setFocusedPanel, selectedRunCount, selected,
|
|
272
361
|
selectedRunIndex, setSelectedAction, selectedAction, onAction, onOpenRunLog,
|
|
273
362
|
refreshTasks, onViewTasks, onViewProjects, onAddLoop,
|
|
274
|
-
}))
|
|
363
|
+
})) {
|
|
364
|
+
key.preventDefault();
|
|
275
365
|
return;
|
|
366
|
+
}
|
|
276
367
|
const shortcut = BOARD_SHORTCUTS[name];
|
|
277
368
|
if (shortcut) {
|
|
278
369
|
shortcut({
|
|
@@ -281,6 +372,7 @@ export function useBoardKeybindings(params) {
|
|
|
281
372
|
selectedRunIndex, setSelectedAction, selectedAction, onAction, onOpenRunLog,
|
|
282
373
|
onSelectProject,
|
|
283
374
|
});
|
|
375
|
+
key.preventDefault();
|
|
284
376
|
}
|
|
285
377
|
});
|
|
286
378
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import { copyToClipboard, readFromClipboard } from "../../shared/clipboard.js";
|
|
3
|
+
export function useInputShortcuts(getActiveInput) {
|
|
4
|
+
useKeyboard((key) => {
|
|
5
|
+
if (!key.ctrl)
|
|
6
|
+
return;
|
|
7
|
+
const input = getActiveInput();
|
|
8
|
+
if (!input)
|
|
9
|
+
return;
|
|
10
|
+
const name = key.name;
|
|
11
|
+
if (name === "c") {
|
|
12
|
+
const text = input.getSelectedText();
|
|
13
|
+
if (text) {
|
|
14
|
+
copyToClipboard(text);
|
|
15
|
+
}
|
|
16
|
+
key.preventDefault();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (name === "x") {
|
|
20
|
+
const text = input.getSelectedText();
|
|
21
|
+
if (text) {
|
|
22
|
+
copyToClipboard(text);
|
|
23
|
+
input.deleteSelection();
|
|
24
|
+
}
|
|
25
|
+
key.preventDefault();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (name === "v") {
|
|
29
|
+
const text = readFromClipboard();
|
|
30
|
+
if (text) {
|
|
31
|
+
input.insertText(text);
|
|
32
|
+
}
|
|
33
|
+
key.preventDefault();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (name === "a") {
|
|
37
|
+
input.selectAll();
|
|
38
|
+
key.preventDefault();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/react";
|
|
2
2
|
import { nextTaskPanel } from "../components/TaskBrowser.js";
|
|
3
3
|
export function useTaskKeybindings(params) {
|
|
4
|
-
const { confirm, view, tasks, taskSelectedIndex, setTaskSelectedIndex, taskSelectedAction, setTaskSelectedAction, taskFocusedPanel, setTaskFocusedPanel, taskSearchActive, setTaskSearchActive, taskQuery, setTaskQuery, onTaskAction, onCancel, onCreateTask, selectable = true, } = params;
|
|
4
|
+
const { confirm, view, tasks, taskSelectedIndex, setTaskSelectedIndex, taskSelectedAction, setTaskSelectedAction, taskFocusedPanel, setTaskFocusedPanel, taskSearchActive, setTaskSearchActive, taskQuery, setTaskQuery, onTaskAction, onCancel, onCreateTask, onEnterHeader, selectable = true, } = params;
|
|
5
5
|
const taskActions = selectable
|
|
6
6
|
? ["select", "edit", "delete"]
|
|
7
7
|
: ["edit", "delete"];
|
|
@@ -16,94 +16,125 @@ export function useTaskKeybindings(params) {
|
|
|
16
16
|
if (name === "escape") {
|
|
17
17
|
setTaskSearchActive(false);
|
|
18
18
|
setTaskFocusedPanel("tasks");
|
|
19
|
+
key.preventDefault();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (name === "up" || name === "k") {
|
|
23
|
+
setTaskSelectedIndex((i) => Math.max(0, i - 1));
|
|
24
|
+
key.preventDefault();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (name === "down" || name === "j") {
|
|
28
|
+
setTaskSelectedIndex((i) => Math.min(tasks.length - 1, i + 1));
|
|
29
|
+
key.preventDefault();
|
|
19
30
|
return;
|
|
20
31
|
}
|
|
21
32
|
return;
|
|
22
33
|
}
|
|
23
34
|
if (name === "escape") {
|
|
24
35
|
onCancel();
|
|
36
|
+
key.preventDefault();
|
|
25
37
|
return;
|
|
26
38
|
}
|
|
27
39
|
if (name === "n") {
|
|
28
40
|
onCreateTask();
|
|
41
|
+
key.preventDefault();
|
|
29
42
|
return;
|
|
30
43
|
}
|
|
31
44
|
if (name === "/" && taskFocusedPanel === "tasks") {
|
|
32
45
|
setTaskSearchActive(true);
|
|
46
|
+
key.preventDefault();
|
|
33
47
|
return;
|
|
34
48
|
}
|
|
35
49
|
if (name === "tab") {
|
|
36
|
-
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
if (name === "left" || name === "right") {
|
|
50
|
+
const direction = key.shift ? "left" : "right";
|
|
40
51
|
if (taskFocusedPanel === "actions") {
|
|
41
|
-
if (
|
|
52
|
+
if (direction === "left" && taskSelectedAction === 0) {
|
|
42
53
|
setTaskFocusedPanel((p) => nextTaskPanel(p, "left"));
|
|
43
54
|
}
|
|
44
|
-
else if (
|
|
45
|
-
|
|
55
|
+
else if (direction === "right" && taskSelectedAction === taskActionCount - 1) {
|
|
56
|
+
onEnterHeader("right");
|
|
57
|
+
key.preventDefault();
|
|
58
|
+
return;
|
|
46
59
|
}
|
|
47
60
|
else {
|
|
48
|
-
setTaskSelectedAction((i) =>
|
|
61
|
+
setTaskSelectedAction((i) => direction === "right"
|
|
49
62
|
? Math.min(taskActionCount - 1, i + 1)
|
|
50
63
|
: Math.max(0, i - 1));
|
|
51
64
|
}
|
|
52
65
|
}
|
|
66
|
+
else if (taskFocusedPanel === "search" && direction === "left") {
|
|
67
|
+
onEnterHeader("left");
|
|
68
|
+
key.preventDefault();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
53
71
|
else {
|
|
54
|
-
setTaskFocusedPanel((p) => nextTaskPanel(p,
|
|
72
|
+
setTaskFocusedPanel((p) => nextTaskPanel(p, direction));
|
|
55
73
|
}
|
|
74
|
+
key.preventDefault();
|
|
56
75
|
return;
|
|
57
76
|
}
|
|
58
77
|
if (taskFocusedPanel === "tasks") {
|
|
59
78
|
if (name === "up" || name === "k") {
|
|
60
79
|
setTaskSelectedIndex((i) => Math.max(0, i - 1));
|
|
80
|
+
key.preventDefault();
|
|
61
81
|
return;
|
|
62
82
|
}
|
|
63
83
|
if (name === "down" || name === "j") {
|
|
64
84
|
setTaskSelectedIndex((i) => Math.min(tasks.length - 1, i + 1));
|
|
85
|
+
key.preventDefault();
|
|
65
86
|
return;
|
|
66
87
|
}
|
|
67
88
|
if (name === "return" || name === "enter") {
|
|
68
89
|
setTaskFocusedPanel("actions");
|
|
90
|
+
key.preventDefault();
|
|
69
91
|
return;
|
|
70
92
|
}
|
|
71
93
|
if (name === "e" && tasks[taskSelectedIndex]) {
|
|
72
94
|
onTaskAction("edit");
|
|
95
|
+
key.preventDefault();
|
|
73
96
|
return;
|
|
74
97
|
}
|
|
75
98
|
if (name === "d" && tasks[taskSelectedIndex]) {
|
|
76
99
|
onTaskAction("delete");
|
|
100
|
+
key.preventDefault();
|
|
77
101
|
return;
|
|
78
102
|
}
|
|
79
103
|
if (name === "delete" && tasks[taskSelectedIndex]) {
|
|
80
104
|
onTaskAction("delete");
|
|
105
|
+
key.preventDefault();
|
|
81
106
|
return;
|
|
82
107
|
}
|
|
83
108
|
if (selectable && name === "s" && tasks[taskSelectedIndex]) {
|
|
84
109
|
onTaskAction("select");
|
|
110
|
+
key.preventDefault();
|
|
85
111
|
return;
|
|
86
112
|
}
|
|
87
113
|
}
|
|
88
114
|
if (taskFocusedPanel === "actions") {
|
|
89
115
|
if (name === "up" || name === "k") {
|
|
90
116
|
setTaskSelectedAction((i) => Math.max(0, i - 1));
|
|
117
|
+
key.preventDefault();
|
|
91
118
|
return;
|
|
92
119
|
}
|
|
93
120
|
if (name === "down" || name === "j") {
|
|
94
121
|
setTaskSelectedAction((i) => Math.min(taskActionCount - 1, i + 1));
|
|
122
|
+
key.preventDefault();
|
|
95
123
|
return;
|
|
96
124
|
}
|
|
97
125
|
if (name === "return" || name === "enter") {
|
|
98
126
|
onTaskAction(taskActions[taskSelectedAction] ?? "select");
|
|
127
|
+
key.preventDefault();
|
|
99
128
|
return;
|
|
100
129
|
}
|
|
101
130
|
if (name === "d") {
|
|
102
131
|
onTaskAction("delete");
|
|
132
|
+
key.preventDefault();
|
|
103
133
|
return;
|
|
104
134
|
}
|
|
105
135
|
if (selectable && name === "s") {
|
|
106
136
|
onTaskAction("select");
|
|
137
|
+
key.preventDefault();
|
|
107
138
|
return;
|
|
108
139
|
}
|
|
109
140
|
}
|
|
@@ -5,6 +5,7 @@ import { sleep } from "../shared/sleep.js";
|
|
|
5
5
|
import { SLEEP_CHUNK_MS } from "../config/constants.js";
|
|
6
6
|
import { executeCommand } from "./command-runner.js";
|
|
7
7
|
import { rotateLogIfNeeded } from "./log-rotator.js";
|
|
8
|
+
import { t } from "../i18n/index.js";
|
|
8
9
|
export class LoopController extends EventEmitter {
|
|
9
10
|
abortController;
|
|
10
11
|
runAbortController = null;
|
|
@@ -344,6 +345,9 @@ export class LoopController extends EventEmitter {
|
|
|
344
345
|
const chainTask = this.taskResolver(currentTargetId);
|
|
345
346
|
if (!chainTask)
|
|
346
347
|
break;
|
|
348
|
+
if (this.logStream) {
|
|
349
|
+
this.logStream.write(t("loop.chainHeader", { name: chainTask.name }));
|
|
350
|
+
}
|
|
347
351
|
const chainStartedAt = new Date().toISOString();
|
|
348
352
|
const chainOffset = fs.existsSync(this.logPath) ? fs.statSync(this.logPath).size : 0;
|
|
349
353
|
this.runHistory.push({
|
package/dist/daemon/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { LoopManager } from "./manager.js";
|
|
2
2
|
import { TaskManager } from "./task-manager.js";
|
|
3
3
|
import { IpcServer } from "./server.js";
|
|
4
|
-
import { writeDaemonPid, removeDaemonPid, writeDaemonSignature, removeDaemonSignature, computeCodeSignature, } from "./state.js";
|
|
4
|
+
import { writeDaemonPid, removeDaemonPid, writeDaemonSignature, removeDaemonSignature, computeCodeSignature, migrateTasksToJson, migrateLoopsToJson, } from "./state.js";
|
|
5
5
|
import { t } from "../i18n/index.js";
|
|
6
6
|
import { daemonLog } from "./daemon-log.js";
|
|
7
7
|
async function main() {
|
|
8
8
|
const taskManager = new TaskManager();
|
|
9
|
+
migrateLoopsToJson();
|
|
10
|
+
migrateTasksToJson();
|
|
9
11
|
taskManager.init();
|
|
10
12
|
const manager = new LoopManager(taskManager);
|
|
11
13
|
const server = new IpcServer(manager, taskManager);
|
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,
|
|
4
|
+
import { saveLoop, loadAllLoops, deleteLoop as deleteLoopState, getLogPath, } from "./state.js";
|
|
5
5
|
import { daemonLog } from "./daemon-log.js";
|
|
6
6
|
export class LoopManager {
|
|
7
7
|
loops = new Map();
|
|
@@ -16,9 +16,6 @@ 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();
|
|
22
19
|
const saved = loadAllLoops();
|
|
23
20
|
let restarted = 0;
|
|
24
21
|
let migrated = 0;
|
package/dist/daemon/server.js
CHANGED
|
@@ -257,8 +257,10 @@ export class IpcServer {
|
|
|
257
257
|
send(socket, { type: "error", message: t("errors.loopNotFound", { id }) });
|
|
258
258
|
return;
|
|
259
259
|
}
|
|
260
|
-
const
|
|
261
|
-
|
|
260
|
+
const records = meta.runHistory
|
|
261
|
+
.filter((r) => r.runNumber === runNumber)
|
|
262
|
+
.sort((a, b) => a.logOffset - b.logOffset);
|
|
263
|
+
if (records.length === 0) {
|
|
262
264
|
send(socket, { type: "ok", data: "" });
|
|
263
265
|
return;
|
|
264
266
|
}
|
|
@@ -267,12 +269,12 @@ export class IpcServer {
|
|
|
267
269
|
send(socket, { type: "ok", data: "" });
|
|
268
270
|
return;
|
|
269
271
|
}
|
|
270
|
-
const
|
|
272
|
+
const firstOffset = records[0].logOffset;
|
|
271
273
|
const stat = fs.statSync(logPath);
|
|
272
|
-
if (stat.size >
|
|
274
|
+
if (stat.size > firstOffset) {
|
|
273
275
|
const fd = fs.openSync(logPath, "r");
|
|
274
|
-
const buf = Buffer.alloc(stat.size -
|
|
275
|
-
fs.readSync(fd, buf, 0, buf.length,
|
|
276
|
+
const buf = Buffer.alloc(stat.size - firstOffset);
|
|
277
|
+
fs.readSync(fd, buf, 0, buf.length, firstOffset);
|
|
276
278
|
fs.closeSync(fd);
|
|
277
279
|
for (const line of buf.toString().split("\n")) {
|
|
278
280
|
if (line) {
|
|
@@ -280,7 +282,8 @@ export class IpcServer {
|
|
|
280
282
|
}
|
|
281
283
|
}
|
|
282
284
|
}
|
|
283
|
-
|
|
285
|
+
const allCompleted = records.every((r) => r.status === "completed");
|
|
286
|
+
if (allCompleted) {
|
|
284
287
|
send(socket, { type: "end" });
|
|
285
288
|
return;
|
|
286
289
|
}
|
package/dist/i18n/en.json
CHANGED
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"cli.projectAmbiguous": "Multiple projects named \"{name}\" - use the id instead",
|
|
66
66
|
"cli.projectInvalidColor": "Invalid color \"{color}\". Valid names: {valid}, or a #rrggbb hex",
|
|
67
67
|
"loop.runHeader": "\n[Run #{runNumber} - {timestamp}]\n",
|
|
68
|
+
"loop.chainHeader": "\n--- Chain: {name} ---\n",
|
|
68
69
|
"loop.exitMarker": "[exit {code} · {duration}]\n",
|
|
69
70
|
"loop.cwdMissingLog": "[error] working directory does not exist: {cwd}\n",
|
|
70
71
|
"loop.cwdMissing": "Working directory does not exist: {cwd}",
|
package/dist/shared/clipboard.js
CHANGED
|
@@ -17,3 +17,25 @@ export function copyToClipboard(text) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
export function readFromClipboard() {
|
|
21
|
+
const os = platform();
|
|
22
|
+
try {
|
|
23
|
+
if (os === "win32") {
|
|
24
|
+
return execFileSync("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], { encoding: "utf-8" }).replace(/\r?\n$/, "");
|
|
25
|
+
}
|
|
26
|
+
else if (os === "darwin") {
|
|
27
|
+
return execFileSync("pbpaste", { encoding: "utf-8" });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
try {
|
|
31
|
+
return execFileSync("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf-8" });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return execFileSync("xsel", ["--clipboard", "--output"], { encoding: "utf-8" });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.5",
|
|
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": {
|