loop-task 1.4.4 → 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 +10 -3
- package/dist/board/components/HelpModal.js +6 -6
- package/dist/board/components/LogModal.js +27 -3
- package/dist/board/components/ProjectsPage.js +13 -2
- package/dist/board/components/SearchBox.js +5 -1
- package/dist/board/components/TaskForm.js +10 -3
- package/dist/board/hooks/useBoardKeybindings.js +84 -4
- package/dist/board/hooks/useInputShortcuts.js +42 -0
- package/dist/board/hooks/useTaskKeybindings.js +22 -5
- package/dist/core/loop-controller.js +4 -0
- 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) => {
|
|
@@ -271,7 +278,7 @@ export function CreateView(props) {
|
|
|
271
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) => {
|
|
272
279
|
const leftField = filteredFields[row * 2];
|
|
273
280
|
const rightField = row * 2 + 1 < filteredFields.length ? filteredFields[row * 2 + 1] : null;
|
|
274
|
-
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));
|
|
275
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] }));
|
|
276
283
|
}
|
|
277
284
|
function HoverButton(props) {
|
|
@@ -281,7 +288,7 @@ function HoverButton(props) {
|
|
|
281
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 }) }) }));
|
|
282
289
|
}
|
|
283
290
|
function FormRow(props) {
|
|
284
|
-
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;
|
|
285
292
|
const { isHovered, hoverProps } = useHoverState();
|
|
286
293
|
const isFocused = focusIndex === index;
|
|
287
294
|
const isToggleField = field === "taskMode" || field === "runNow";
|
|
@@ -297,7 +304,7 @@ function FormRow(props) {
|
|
|
297
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) => {
|
|
298
305
|
const isActive = values.project === opt.value;
|
|
299
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));
|
|
300
|
-
}) })) : (_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: () => {
|
|
301
308
|
if (index < fields.length - 1) {
|
|
302
309
|
setFocusIndex(index + 1);
|
|
303
310
|
}
|
|
@@ -5,8 +5,8 @@ export function HelpModal(props) {
|
|
|
5
5
|
const { view } = props;
|
|
6
6
|
const { width } = useTerminalDimensions();
|
|
7
7
|
const loopRows = [
|
|
8
|
-
["tab
|
|
9
|
-
["
|
|
8
|
+
["tab", "switch panels"],
|
|
9
|
+
["up/down", "navigate list items"],
|
|
10
10
|
["enter", t("board.helpEdit")],
|
|
11
11
|
["e", "edit loop"],
|
|
12
12
|
["d/del", "delete loop"],
|
|
@@ -24,8 +24,8 @@ export function HelpModal(props) {
|
|
|
24
24
|
["esc", t("board.helpQuit")],
|
|
25
25
|
];
|
|
26
26
|
const taskRows = [
|
|
27
|
-
["tab
|
|
28
|
-
["
|
|
27
|
+
["tab", "switch panels"],
|
|
28
|
+
["up/down", "navigate tasks"],
|
|
29
29
|
["enter", "focus actions panel"],
|
|
30
30
|
["e", "edit task"],
|
|
31
31
|
["d/del", "delete task"],
|
|
@@ -36,8 +36,8 @@ export function HelpModal(props) {
|
|
|
36
36
|
["esc", t("board.helpQuit")],
|
|
37
37
|
];
|
|
38
38
|
const projectRows = [
|
|
39
|
-
["tab
|
|
40
|
-
["
|
|
39
|
+
["tab", "switch panel (list/actions)"],
|
|
40
|
+
["up/down", "navigate projects"],
|
|
41
41
|
["enter", "focus actions"],
|
|
42
42
|
["n", "new project"],
|
|
43
43
|
["e", "edit project"],
|
|
@@ -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");
|
|
@@ -52,7 +52,18 @@ export function ProjectsPage(props) {
|
|
|
52
52
|
key.preventDefault();
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
-
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
|
+
}
|
|
56
67
|
setFocusedPanel((p) => (p === "list" ? "actions" : "list"));
|
|
57
68
|
key.preventDefault();
|
|
58
69
|
return;
|
|
@@ -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) {
|
|
@@ -140,16 +147,16 @@ export function TaskForm(props) {
|
|
|
140
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) => {
|
|
141
148
|
const leftField = taskFields[row * 2];
|
|
142
149
|
const rightField = row * 2 + 1 < taskFields.length ? taskFields[row * 2 + 1] : null;
|
|
143
|
-
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));
|
|
144
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] }));
|
|
145
152
|
}
|
|
146
153
|
function TaskFormRow(props) {
|
|
147
|
-
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;
|
|
148
155
|
const isFocused = focusIndex === index;
|
|
149
156
|
const isSelect = field === "onSuccessTaskId" || field === "onFailureTaskId";
|
|
150
157
|
const selectOpts = isSelect ? chainOptions : [];
|
|
151
158
|
const selectedIdx = isSelect ? selectOpts.findIndex((o) => o.value === values[field]) : 0;
|
|
152
|
-
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: () => {
|
|
153
160
|
if (index < taskFields.length - 1) {
|
|
154
161
|
setFocusIndex(index + 1);
|
|
155
162
|
}
|
|
@@ -195,7 +195,7 @@ 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) {
|
|
@@ -230,6 +230,19 @@ export function useBoardKeybindings(params) {
|
|
|
230
230
|
key.preventDefault();
|
|
231
231
|
return;
|
|
232
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
|
+
}
|
|
233
246
|
return;
|
|
234
247
|
}
|
|
235
248
|
if (view !== "board" && name === "escape") {
|
|
@@ -237,11 +250,78 @@ export function useBoardKeybindings(params) {
|
|
|
237
250
|
key.preventDefault();
|
|
238
251
|
return;
|
|
239
252
|
}
|
|
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
|
+
}
|
|
240
320
|
if (view !== "board")
|
|
241
321
|
return;
|
|
242
|
-
if (name === "
|
|
243
|
-
const direction =
|
|
244
|
-
if (focusedPanel === "actions"
|
|
322
|
+
if (name === "tab") {
|
|
323
|
+
const direction = key.shift ? "left" : "right";
|
|
324
|
+
if (focusedPanel === "actions") {
|
|
245
325
|
const actionCount = selected ? getActionCount(selected.status) : 0;
|
|
246
326
|
if (direction === "left" && selectedAction === 0) {
|
|
247
327
|
setFocusedPanel((p) => nextPanel(p, "left"));
|
|
@@ -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"];
|
|
@@ -19,6 +19,16 @@ export function useTaskKeybindings(params) {
|
|
|
19
19
|
key.preventDefault();
|
|
20
20
|
return;
|
|
21
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();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
22
32
|
return;
|
|
23
33
|
}
|
|
24
34
|
if (name === "escape") {
|
|
@@ -36,14 +46,16 @@ export function useTaskKeybindings(params) {
|
|
|
36
46
|
key.preventDefault();
|
|
37
47
|
return;
|
|
38
48
|
}
|
|
39
|
-
if (name === "tab"
|
|
40
|
-
const direction =
|
|
41
|
-
if (taskFocusedPanel === "actions"
|
|
49
|
+
if (name === "tab") {
|
|
50
|
+
const direction = key.shift ? "left" : "right";
|
|
51
|
+
if (taskFocusedPanel === "actions") {
|
|
42
52
|
if (direction === "left" && taskSelectedAction === 0) {
|
|
43
53
|
setTaskFocusedPanel((p) => nextTaskPanel(p, "left"));
|
|
44
54
|
}
|
|
45
55
|
else if (direction === "right" && taskSelectedAction === taskActionCount - 1) {
|
|
46
|
-
|
|
56
|
+
onEnterHeader("right");
|
|
57
|
+
key.preventDefault();
|
|
58
|
+
return;
|
|
47
59
|
}
|
|
48
60
|
else {
|
|
49
61
|
setTaskSelectedAction((i) => direction === "right"
|
|
@@ -51,6 +63,11 @@ export function useTaskKeybindings(params) {
|
|
|
51
63
|
: Math.max(0, i - 1));
|
|
52
64
|
}
|
|
53
65
|
}
|
|
66
|
+
else if (taskFocusedPanel === "search" && direction === "left") {
|
|
67
|
+
onEnterHeader("left");
|
|
68
|
+
key.preventDefault();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
54
71
|
else {
|
|
55
72
|
setTaskFocusedPanel((p) => nextTaskPanel(p, direction));
|
|
56
73
|
}
|
|
@@ -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/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": {
|