loop-task 1.5.1 → 1.5.2
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 +1 -1
- package/dist/board/components/CreateForm.js +31 -27
- package/dist/board/components/CreateProjectModal.js +5 -3
- package/dist/board/components/EditProjectModal.js +5 -3
- package/dist/board/components/ProjectsPage.js +7 -4
- package/dist/board/components/SearchSelect.js +55 -23
- package/dist/board/components/TaskBrowser.js +4 -2
- package/dist/board/components/TaskForm.js +19 -13
- package/dist/i18n/en.json +2 -0
- package/package.json +1 -1
package/dist/board/App.js
CHANGED
|
@@ -397,7 +397,7 @@ export function App(props) {
|
|
|
397
397
|
}
|
|
398
398
|
else {
|
|
399
399
|
push("projects");
|
|
400
|
-
} } }), view === "board" ? (_jsx(FilterBar, { filters: filters, sort: sort, searchActive: searchActive, focusedPanel: derivedFocusedPanel, 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); } })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: derivedTaskPanel, onQueryChange: setTaskQuery, onSearchActivate: () => setTaskSearchActive(true), onSearchDismiss: () => { setTaskSearchActive(false); } })) : 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: derivedTaskPanel === "tasks", query: taskQuery, onSelect: (index) => { setTaskSelectedIndex(index); }, 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: taskActionIdx >= 0, selectedAction: derivedTaskAction, selectable: stack.includes("create") || stack.includes("task-edit"), onAction: handleTaskAction }, `tab-${selectedTask?.id}`)] })] })) : view === "projects" ? (_jsx(ProjectsPage, { projects: projects, loops: loops, headerFocused: isHeaderFocused, onClose: () => pop(), onRefresh: refreshProjects, onOpenCreate: (trigger) => { createProjectTriggerRef.current = trigger; }, onEnterHeader: () => { } })) : (_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: derivedFocusedPanel === "loops" || derivedFocusedPanel === "navigator", projects: projects, onSelect: (index) => setSelectedIndex(index), onActivate: (index) => { setSelectedIndex(index); const loop = visible[index]; if (loop) {
|
|
400
|
+
} } }), view === "board" ? (_jsx(FilterBar, { filters: filters, sort: sort, searchActive: searchActive, focusedPanel: derivedFocusedPanel, 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); } })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: derivedTaskPanel, onQueryChange: setTaskQuery, onSearchActivate: () => setTaskSearchActive(true), onSearchDismiss: () => { setTaskSearchActive(false); } })) : 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, onCopy: () => pushToast("success", t("board.toastCopied")) })) : 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: derivedTaskPanel === "tasks", query: taskQuery, onSelect: (index) => { setTaskSelectedIndex(index); }, 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, onCopy: () => pushToast("success", t("board.toastCopied")) }, `ti-${selectedTask?.id}`), _jsx(TaskActionButtons, { task: selectedTask, focused: taskActionIdx >= 0, selectedAction: derivedTaskAction, selectable: stack.includes("create") || stack.includes("task-edit"), onAction: handleTaskAction }, `tab-${selectedTask?.id}`)] })] })) : view === "projects" ? (_jsx(ProjectsPage, { projects: projects, loops: loops, headerFocused: isHeaderFocused, onClose: () => pop(), onRefresh: refreshProjects, onOpenCreate: (trigger) => { createProjectTriggerRef.current = trigger; }, onEnterHeader: () => { } })) : (_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: derivedFocusedPanel === "loops" || derivedFocusedPanel === "navigator", projects: projects, onSelect: (index) => setSelectedIndex(index), onActivate: (index) => { setSelectedIndex(index); const loop = visible[index]; if (loop) {
|
|
401
401
|
setEditTarget(loop);
|
|
402
402
|
push("create");
|
|
403
403
|
} } }), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(Inspector, { loop: selected }), _jsx(RunHistory, { loop: selected, selectedRunIndex: selectedRunIndex, focused: derivedFocusedPanel === "runs" || derivedFocusedPanel === "run-history", onSelectRun: (index) => setSelectedRunIndex(index), onOpenRun: handleOpenRunLog }), _jsx(ActionButtons, { loop: selected, focused: actionIdx >= 0, selectedAction: derivedSelectedAction, 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, contextHelpOpen ? _jsx(ContextHelpModal, { onClose: () => setContextHelpOpen(false) }) : 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 })] }));
|
|
@@ -65,8 +65,11 @@ export function CreateView(props) {
|
|
|
65
65
|
});
|
|
66
66
|
const navItems = [...filteredFields, "save", "cancel"];
|
|
67
67
|
const { setFocusIndex, focusedItem, isFocused } = useTabNav(navItems);
|
|
68
|
+
const focusedItemRef = useRef(focusedItem);
|
|
69
|
+
focusedItemRef.current = focusedItem;
|
|
68
70
|
useInputShortcuts(() => {
|
|
69
|
-
|
|
71
|
+
const fi = focusedItemRef.current;
|
|
72
|
+
if (fi === "save" || fi === "cancel" || fi === "project")
|
|
70
73
|
return null;
|
|
71
74
|
return inputRef.current;
|
|
72
75
|
});
|
|
@@ -90,32 +93,33 @@ export function CreateView(props) {
|
|
|
90
93
|
setSelectedTaskName(props.selectedTaskName ?? props.selectedTaskId);
|
|
91
94
|
}
|
|
92
95
|
useKeyboard((key) => {
|
|
93
|
-
if (key.name
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
96
|
+
if (key.name !== "return" && key.name !== "enter" && key.name !== " ")
|
|
97
|
+
return;
|
|
98
|
+
const fi = focusedItemRef.current;
|
|
99
|
+
if (fi === "taskMode") {
|
|
100
|
+
const next = valuesRef.current.taskMode === TASK_MODE_INLINE ? TASK_MODE_EXISTING : TASK_MODE_INLINE;
|
|
101
|
+
updateValues({ ...valuesRef.current, taskMode: next });
|
|
102
|
+
key.preventDefault();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (fi === "runNow") {
|
|
106
|
+
const next = valuesRef.current.runNow === "y" ? "n" : "y";
|
|
107
|
+
updateValues({ ...valuesRef.current, runNow: next });
|
|
108
|
+
key.preventDefault();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (fi === "taskId" && !isInline) {
|
|
112
|
+
props.onChooseTask();
|
|
113
|
+
key.preventDefault();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (fi === "save") {
|
|
117
|
+
void submit(valuesRef.current);
|
|
118
|
+
key.preventDefault();
|
|
119
|
+
}
|
|
120
|
+
else if (fi === "cancel") {
|
|
121
|
+
props.onCancel();
|
|
122
|
+
key.preventDefault();
|
|
119
123
|
}
|
|
120
124
|
});
|
|
121
125
|
async function submit(current) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useState } from "react";
|
|
2
|
+
import { useState, useRef } from "react";
|
|
3
3
|
import { useTabNav } from "../hooks/useTabNav.js";
|
|
4
4
|
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
5
5
|
import { t } from "../../i18n/index.js";
|
|
@@ -14,20 +14,22 @@ export function CreateProjectModal(props) {
|
|
|
14
14
|
const [error, setError] = useState("");
|
|
15
15
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
16
16
|
const { focusedItem: focusField, setFocusIndex } = useTabNav(["name", "color", "save", "cancel"]);
|
|
17
|
+
const focusFieldRef = useRef(focusField);
|
|
18
|
+
focusFieldRef.current = focusField;
|
|
17
19
|
useKeyboard((key) => {
|
|
18
20
|
if (key.name === "escape") {
|
|
19
21
|
onCancel();
|
|
20
22
|
return;
|
|
21
23
|
}
|
|
22
24
|
if (key.name === "return" || key.name === "enter") {
|
|
23
|
-
if (
|
|
25
|
+
if (focusFieldRef.current === "cancel") {
|
|
24
26
|
onCancel();
|
|
25
27
|
return;
|
|
26
28
|
}
|
|
27
29
|
void submit();
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
30
|
-
if (
|
|
32
|
+
if (focusFieldRef.current === "color") {
|
|
31
33
|
if (key.name === "left" || key.name === "right") {
|
|
32
34
|
const idx = PROJECT_COLOR_KEYS.indexOf(selectedColorKey);
|
|
33
35
|
const dir = key.name === "left" ? -1 : 1;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useState } from "react";
|
|
2
|
+
import { useState, useRef } from "react";
|
|
3
3
|
import { useTabNav } from "../hooks/useTabNav.js";
|
|
4
4
|
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
5
5
|
import { t } from "../../i18n/index.js";
|
|
@@ -16,20 +16,22 @@ export function EditProjectModal(props) {
|
|
|
16
16
|
const [error, setError] = useState("");
|
|
17
17
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
18
18
|
const { focusedItem: focusField, setFocusIndex } = useTabNav(["name", "color", "save", "cancel"]);
|
|
19
|
+
const focusFieldRef = useRef(focusField);
|
|
20
|
+
focusFieldRef.current = focusField;
|
|
19
21
|
useKeyboard((key) => {
|
|
20
22
|
if (key.name === "escape") {
|
|
21
23
|
onCancel();
|
|
22
24
|
return;
|
|
23
25
|
}
|
|
24
26
|
if (key.name === "return" || key.name === "enter") {
|
|
25
|
-
if (
|
|
27
|
+
if (focusFieldRef.current === "cancel") {
|
|
26
28
|
onCancel();
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
void submit();
|
|
30
32
|
return;
|
|
31
33
|
}
|
|
32
|
-
if (
|
|
34
|
+
if (focusFieldRef.current === "color") {
|
|
33
35
|
if (key.name === "left" || key.name === "right") {
|
|
34
36
|
const idx = PROJECT_COLOR_KEYS.indexOf(selectedColorKey);
|
|
35
37
|
const dir = key.name === "left" ? -1 : 1;
|
|
@@ -27,6 +27,8 @@ export function ProjectsPage(props) {
|
|
|
27
27
|
onEnterHeader?.("left");
|
|
28
28
|
},
|
|
29
29
|
});
|
|
30
|
+
const focusedItemRef = useRef(focusedItem);
|
|
31
|
+
focusedItemRef.current = focusedItem;
|
|
30
32
|
const prevHeaderFocused = useRef(headerFocused);
|
|
31
33
|
useEffect(() => {
|
|
32
34
|
if (prevHeaderFocused.current && !headerFocused) {
|
|
@@ -52,8 +54,9 @@ export function ProjectsPage(props) {
|
|
|
52
54
|
return;
|
|
53
55
|
if (headerFocused)
|
|
54
56
|
return;
|
|
57
|
+
const fi = focusedItemRef.current;
|
|
55
58
|
if (key.name === "up" || key.name === "down") {
|
|
56
|
-
if (
|
|
59
|
+
if (fi !== "list") {
|
|
57
60
|
key.preventDefault();
|
|
58
61
|
return;
|
|
59
62
|
}
|
|
@@ -71,14 +74,14 @@ export function ProjectsPage(props) {
|
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
if (key.name === "return" || key.name === "enter") {
|
|
74
|
-
if (
|
|
77
|
+
if (fi === "list") {
|
|
75
78
|
setFocusIndex(1);
|
|
76
79
|
}
|
|
77
|
-
else if (
|
|
80
|
+
else if (fi === "edit") {
|
|
78
81
|
if (selectedProject && !selectedProject.isSystem)
|
|
79
82
|
setSubModal("edit");
|
|
80
83
|
}
|
|
81
|
-
else if (
|
|
84
|
+
else if (fi === "delete") {
|
|
82
85
|
if (selectedProject && !selectedProject.isSystem)
|
|
83
86
|
setSubModal("delete");
|
|
84
87
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { useRef, useState, useMemo } from "react";
|
|
2
|
+
import { useRef, useState, useMemo, useEffect } from "react";
|
|
3
3
|
import { useKeyboard } from "@opentui/react";
|
|
4
4
|
import { t } from "../../i18n/index.js";
|
|
5
5
|
import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
|
|
@@ -11,7 +11,7 @@ export function SearchSelect(props) {
|
|
|
11
11
|
const inputRef = useRef(null);
|
|
12
12
|
const [filter, setFilter] = useState("");
|
|
13
13
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
14
|
-
|
|
14
|
+
const [userNavigated, setUserNavigated] = useState(false);
|
|
15
15
|
const filtered = useMemo(() => {
|
|
16
16
|
if (!filter)
|
|
17
17
|
return options;
|
|
@@ -19,41 +19,74 @@ export function SearchSelect(props) {
|
|
|
19
19
|
return options.filter((o) => o.name.toLowerCase().includes(q) || o.value.toLowerCase().includes(q));
|
|
20
20
|
}, [options, filter]);
|
|
21
21
|
const currentIdx = filtered.findIndex((o) => o.value === value);
|
|
22
|
-
const
|
|
23
|
-
const clampedSelected = Math.min(
|
|
22
|
+
const displaySelected = userNavigated ? selectedIndex : (currentIdx >= 0 ? currentIdx : 0);
|
|
23
|
+
const clampedSelected = Math.min(displaySelected, Math.max(0, filtered.length - 1));
|
|
24
|
+
const filteredRef = useRef(filtered);
|
|
25
|
+
filteredRef.current = filtered;
|
|
26
|
+
const selectedIdxRef = useRef(clampedSelected);
|
|
27
|
+
selectedIdxRef.current = clampedSelected;
|
|
28
|
+
const focusedRef = useRef(focused);
|
|
29
|
+
focusedRef.current = focused;
|
|
30
|
+
const onChangeRef = useRef(onChange);
|
|
31
|
+
onChangeRef.current = onChange;
|
|
32
|
+
const filterRef = useRef(filter);
|
|
33
|
+
filterRef.current = filter;
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (focused) {
|
|
36
|
+
setUserNavigated(false);
|
|
37
|
+
setSelectedIndex(currentIdx >= 0 ? currentIdx : 0);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
setFilter("");
|
|
41
|
+
setUserNavigated(false);
|
|
42
|
+
}
|
|
43
|
+
}, [focused]);
|
|
44
|
+
useInputShortcuts(() => focusedRef.current ? inputRef.current : null);
|
|
24
45
|
useKeyboard((key) => {
|
|
25
|
-
if (!
|
|
46
|
+
if (!focusedRef.current)
|
|
26
47
|
return;
|
|
27
48
|
const name = key.name;
|
|
28
49
|
if (name === "up" || name === "k") {
|
|
29
|
-
|
|
30
|
-
|
|
50
|
+
const list = filteredRef.current;
|
|
51
|
+
if (list.length > 0) {
|
|
52
|
+
const cur = selectedIdxRef.current;
|
|
53
|
+
const next = cur <= 0 ? list.length - 1 : cur - 1;
|
|
54
|
+
setSelectedIndex(next);
|
|
55
|
+
setUserNavigated(true);
|
|
31
56
|
}
|
|
32
57
|
key.preventDefault();
|
|
33
58
|
key.stopPropagation();
|
|
34
59
|
return;
|
|
35
60
|
}
|
|
36
61
|
if (name === "down" || name === "j") {
|
|
37
|
-
|
|
38
|
-
|
|
62
|
+
const list = filteredRef.current;
|
|
63
|
+
if (list.length > 0) {
|
|
64
|
+
const cur = selectedIdxRef.current;
|
|
65
|
+
const next = cur >= list.length - 1 ? 0 : cur + 1;
|
|
66
|
+
setSelectedIndex(next);
|
|
67
|
+
setUserNavigated(true);
|
|
39
68
|
}
|
|
40
69
|
key.preventDefault();
|
|
41
70
|
key.stopPropagation();
|
|
42
71
|
return;
|
|
43
72
|
}
|
|
44
73
|
if (name === "return" || name === "enter") {
|
|
45
|
-
const
|
|
74
|
+
const list = filteredRef.current;
|
|
75
|
+
const option = list[selectedIdxRef.current];
|
|
46
76
|
if (option) {
|
|
47
|
-
|
|
77
|
+
onChangeRef.current(option.value);
|
|
48
78
|
}
|
|
49
79
|
key.preventDefault();
|
|
50
80
|
key.stopPropagation();
|
|
51
81
|
return;
|
|
52
82
|
}
|
|
53
83
|
if (name === "escape") {
|
|
54
|
-
if (
|
|
84
|
+
if (filterRef.current) {
|
|
55
85
|
setFilter("");
|
|
56
86
|
setSelectedIndex(0);
|
|
87
|
+
setUserNavigated(false);
|
|
88
|
+
if (inputRef.current)
|
|
89
|
+
inputRef.current.value = "";
|
|
57
90
|
}
|
|
58
91
|
key.preventDefault();
|
|
59
92
|
key.stopPropagation();
|
|
@@ -62,32 +95,31 @@ export function SearchSelect(props) {
|
|
|
62
95
|
if (name === "backspace") {
|
|
63
96
|
setFilter((f) => f.slice(0, -1));
|
|
64
97
|
setSelectedIndex(0);
|
|
98
|
+
setUserNavigated(false);
|
|
65
99
|
key.preventDefault();
|
|
66
100
|
key.stopPropagation();
|
|
67
101
|
return;
|
|
68
102
|
}
|
|
69
103
|
if (key.ctrl)
|
|
70
104
|
return;
|
|
105
|
+
let char = null;
|
|
71
106
|
if (name && name.length === 1 && /[a-z0-9 _\-./]/i.test(name)) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
key.
|
|
76
|
-
return;
|
|
107
|
+
char = name;
|
|
108
|
+
}
|
|
109
|
+
else if (key.sequence && key.sequence.length === 1 && /[a-z0-9 _\-./]/i.test(key.sequence)) {
|
|
110
|
+
char = key.sequence;
|
|
77
111
|
}
|
|
78
|
-
if (
|
|
79
|
-
setFilter((f) => f +
|
|
112
|
+
if (char) {
|
|
113
|
+
setFilter((f) => f + char);
|
|
80
114
|
setSelectedIndex(0);
|
|
115
|
+
setUserNavigated(false);
|
|
81
116
|
key.preventDefault();
|
|
82
117
|
key.stopPropagation();
|
|
83
118
|
return;
|
|
84
119
|
}
|
|
85
120
|
});
|
|
86
121
|
const listHeight = Math.min(filtered.length, maxHeight);
|
|
87
|
-
return (_jsxs("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "column", backgroundColor: "#0b0b0b" }, children: [_jsxs("box", { style: { height: 3, flexDirection: "row", alignItems: "center" }, children: [_jsx("text", { fg: "#6b7280", children: "
|
|
88
|
-
setFilter(v);
|
|
89
|
-
setSelectedIndex(0);
|
|
90
|
-
} })] }), _jsx("box", { style: { flexDirection: "column", height: listHeight }, children: filtered.map((option, i) => {
|
|
122
|
+
return (_jsxs("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "column", backgroundColor: focused ? "#0f172a" : "#0b0b0b" }, children: [_jsxs("box", { style: { height: 3, flexDirection: "row", alignItems: "center", paddingLeft: 1 }, children: [_jsx("text", { fg: "#6b7280", children: "/ " }), _jsx("text", { fg: filter ? "#e5e7eb" : "#6b7280", children: filter || placeholder })] }), _jsx("box", { style: { flexDirection: "column", height: listHeight }, children: filtered.map((option, i) => {
|
|
91
123
|
const isSelected = i === clampedSelected;
|
|
92
124
|
const isActive = option.value === value;
|
|
93
125
|
const prefix = isSelected ? "› " : " ";
|
|
@@ -5,6 +5,7 @@ import { t } from "../../i18n/index.js";
|
|
|
5
5
|
import { commandLine, truncate } from "../format.js";
|
|
6
6
|
import { useHoverState } from "../hooks/useHoverState.js";
|
|
7
7
|
import { HOVER_BG, ENTITY_COLORS } from "../../config/constants.js";
|
|
8
|
+
import { copyToClipboard } from "../../shared/clipboard.js";
|
|
8
9
|
function fit(text, width) {
|
|
9
10
|
if (width <= 0)
|
|
10
11
|
return "";
|
|
@@ -67,12 +68,13 @@ function TaskNavRow(props) {
|
|
|
67
68
|
return (_jsx("box", { id: id, onMouseDown: handleClick, backgroundColor: bg, ...hoverProps, children: _jsxs("text", { fg: fg, children: [isSelected ? "›" : " ", " ", fit(name, nameW), " ", fit(truncCmd, cmdW), " ", fit(chains, chainsW)] }) }));
|
|
68
69
|
}
|
|
69
70
|
export function TaskInspector(props) {
|
|
70
|
-
const { task } = props;
|
|
71
|
+
const { task, onCopy } = props;
|
|
71
72
|
if (!task) {
|
|
72
73
|
return (_jsx("box", { title: t("board.inspectorTitle"), border: true, style: { backgroundColor: "#0b0b0b" }, children: _jsx("text", { fg: "#9ca3af", children: t("board.inspectorEmpty") }) }));
|
|
73
74
|
}
|
|
74
75
|
const cmd = commandLine(task.command, task.commandArgs);
|
|
75
|
-
|
|
76
|
+
const { isHovered, hoverProps } = useHoverState();
|
|
77
|
+
return (_jsxs("box", { title: t("board.inspectorTitle"), border: true, style: { flexDirection: "column", backgroundColor: "#0b0b0b" }, children: [_jsxs("text", { children: [_jsx("strong", { children: t("board.fieldId") }), " ", task.id] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.taskLabelName") }), " ", task.name] }), _jsxs("box", { style: { flexDirection: "row" }, children: [_jsxs("text", { children: [_jsx("strong", { children: t("board.fieldCommand") }), " ", cmd] }), _jsx("box", { border: true, borderColor: isHovered ? "#38bdf8" : "#374151", onMouseDown: () => { copyToClipboard(cmd); onCopy?.(cmd); }, style: { paddingLeft: 1, paddingRight: 1, marginLeft: 1, backgroundColor: isHovered ? HOVER_BG : "#0b0b0b" }, ...hoverProps, children: _jsx("text", { fg: isHovered ? "#38bdf8" : "#6b7280", children: "\u2398" }) })] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.taskLabelOnSuccess") }), " ", task.onSuccessTaskId ?? t("board.taskNone")] }), _jsxs("text", { children: [_jsx("strong", { children: t("board.taskLabelOnFailure") }), " ", task.onFailureTaskId ?? t("board.taskNone")] })] }));
|
|
76
78
|
}
|
|
77
79
|
export function TaskActionButtons(props) {
|
|
78
80
|
const { task, focused, selectedAction, selectable = true, onAction } = props;
|
|
@@ -7,6 +7,7 @@ import { createTask, updateTask, listTasks } from "../daemon.js";
|
|
|
7
7
|
import { useHoverState } from "../hooks/useHoverState.js";
|
|
8
8
|
import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
|
|
9
9
|
import { useTabNav } from "../hooks/useTabNav.js";
|
|
10
|
+
import { copyToClipboard } from "../../shared/clipboard.js";
|
|
10
11
|
import { HOVER_BG } from "../../config/constants.js";
|
|
11
12
|
import { SearchSelect } from "./SearchSelect.js";
|
|
12
13
|
const taskFields = ["name", "command", "onSuccessTaskId", "onFailureTaskId"];
|
|
@@ -32,8 +33,11 @@ export function TaskForm(props) {
|
|
|
32
33
|
const btnWidth = Math.max(10, Math.min(14, Math.floor(termWidth / 6)));
|
|
33
34
|
const navItems = [...taskFields, "save", "cancel"];
|
|
34
35
|
const { setFocusIndex, focusedItem, isFocused } = useTabNav(navItems);
|
|
36
|
+
const focusedItemRef = useRef(focusedItem);
|
|
37
|
+
focusedItemRef.current = focusedItem;
|
|
35
38
|
useInputShortcuts(() => {
|
|
36
|
-
|
|
39
|
+
const fi = focusedItemRef.current;
|
|
40
|
+
if (fi != null && fi !== "save" && fi !== "cancel" && fi !== "onSuccessTaskId" && fi !== "onFailureTaskId") {
|
|
37
41
|
return inputRef.current;
|
|
38
42
|
}
|
|
39
43
|
return null;
|
|
@@ -47,11 +51,12 @@ export function TaskForm(props) {
|
|
|
47
51
|
});
|
|
48
52
|
useKeyboard((key) => {
|
|
49
53
|
if (key.name === "return" || key.name === "enter") {
|
|
50
|
-
|
|
54
|
+
const fi = focusedItemRef.current;
|
|
55
|
+
if (fi === "save") {
|
|
51
56
|
void submit(valuesRef.current);
|
|
52
57
|
key.preventDefault();
|
|
53
58
|
}
|
|
54
|
-
else if (
|
|
59
|
+
else if (fi === "cancel") {
|
|
55
60
|
props.onCancel();
|
|
56
61
|
key.preventDefault();
|
|
57
62
|
}
|
|
@@ -125,21 +130,22 @@ export function TaskForm(props) {
|
|
|
125
130
|
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
131
|
const leftField = taskFields[row * 2];
|
|
127
132
|
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, focused: isFocused(leftField), 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, focused: isFocused(rightField), 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));
|
|
133
|
+
return (_jsxs("box", { style: { flexDirection: "row", marginBottom: 1 }, children: [_jsx(TaskFormRow, { field: leftField, index: row * 2, focused: isFocused(leftField), values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, inputRef: inputRef, onCopy: props.onCopy, style: { width: "50%", paddingRight: 1 } }), rightField ? (_jsx(TaskFormRow, { field: rightField, index: row * 2 + 1, focused: isFocused(rightField), values: values, valuesRef: valuesRef, updateValues: updateValues, setFocusIndex: setFocusIndex, submit: submit, labels: labels, hints: hints, chainOptions: chainOptions, inputRef: inputRef, onCopy: props.onCopy, style: { width: "50%" } })) : (_jsx("box", { style: { width: "50%" } }))] }, row));
|
|
129
134
|
}) }), _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: isFocused("save"), width: btnWidth, marginRight: 1 }), _jsx(HoverButton, { label: t("board.cancel"), onMouseDown: props.onCancel, selected: isFocused("cancel"), width: btnWidth })] }), _jsx("text", { fg: "#9ca3af", children: t("board.formNav") }), error ? _jsx("text", { fg: "#f87171", children: error }) : null] }));
|
|
130
135
|
}
|
|
131
136
|
function TaskFormRow(props) {
|
|
132
|
-
const { field, index, focused, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, chainOptions, inputRef, style } = props;
|
|
137
|
+
const { field, index, focused, values, valuesRef, updateValues, setFocusIndex, submit, labels, hints, chainOptions, inputRef, onCopy, style } = props;
|
|
133
138
|
const isSelect = field === "onSuccessTaskId" || field === "onFailureTaskId";
|
|
134
139
|
const selectOpts = isSelect ? chainOptions : [];
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
const copyHover = useHoverState();
|
|
141
|
+
return (_jsxs("box", { style: { flexDirection: "column", ...style }, children: [_jsx("text", { fg: focused ? "#38bdf8" : "#e5e7eb", children: labels[field] }), _jsx("text", { fg: "#6b7280", children: hints[field] }), isSelect ? (_jsx(SearchSelect, { options: selectOpts, value: values[field], onChange: (v) => updateValues({ ...valuesRef.current, [field]: v }), focused: focused })) : (_jsxs("box", { style: { flexDirection: "row" }, children: [_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { height: 3, width: field === "command" ? "92%" : "100%", backgroundColor: "#0b0b0b" }, children: _jsx("input", { ref: inputRef, focused: focused, value: values[field], placeholder: field === "command" ? t("board.exampleCommand") : "", onInput: (value) => updateValues({ ...valuesRef.current, [field]: value }), onSubmit: () => {
|
|
142
|
+
if (index < taskFields.length - 1) {
|
|
143
|
+
setFocusIndex(index + 1);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
void submit(valuesRef.current);
|
|
147
|
+
}
|
|
148
|
+
} }) }), field === "command" ? (_jsx("box", { border: true, borderColor: copyHover.isHovered ? "#38bdf8" : "#374151", onMouseDown: () => { copyToClipboard(valuesRef.current.command); onCopy?.(valuesRef.current.command); }, style: { width: "8%", height: 3, justifyContent: "center", alignItems: "center", backgroundColor: copyHover.isHovered ? HOVER_BG : "#0b0b0b" }, ...copyHover.hoverProps, children: _jsx("text", { fg: copyHover.isHovered ? "#38bdf8" : "#6b7280", children: "\u2398" }) })) : null] }))] }));
|
|
143
149
|
}
|
|
144
150
|
function HoverButton(props) {
|
|
145
151
|
const { isHovered, hoverProps } = useHoverState();
|
package/dist/i18n/en.json
CHANGED
|
@@ -297,6 +297,8 @@
|
|
|
297
297
|
"board.toastTaskCreated": "Task created {id}",
|
|
298
298
|
"board.toastTaskUpdated": "Task updated {id}",
|
|
299
299
|
"board.toastTaskSelected": "Selected task \"{desc}\"",
|
|
300
|
+
"board.toastCopied": "Copied to clipboard",
|
|
301
|
+
"board.copyCommand": "Copy",
|
|
300
302
|
"board.logWaiting": " Waiting for live output...",
|
|
301
303
|
"board.logStreamError": "Log stream error: {message}",
|
|
302
304
|
"board.hintSearch": "search",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
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": {
|