loop-task 1.5.2 → 1.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/board/App.js 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, 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) {
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 }, `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 })] }));
@@ -2,77 +2,81 @@ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
2
  import { useRef, useState, useMemo, useEffect } from "react";
3
3
  import { useKeyboard } from "@opentui/react";
4
4
  import { t } from "../../i18n/index.js";
5
- import { useInputShortcuts } from "../hooks/useInputShortcuts.js";
6
5
  import { SEARCH_SELECT_HEIGHT } from "../../config/constants.js";
7
6
  export function SearchSelect(props) {
8
7
  const { options, value, onChange, focused, height } = props;
9
8
  const placeholder = props.placeholder ?? t("board.searchSelectPlaceholder");
10
9
  const maxHeight = height ?? SEARCH_SELECT_HEIGHT;
11
- const inputRef = useRef(null);
12
10
  const [filter, setFilter] = useState("");
13
11
  const [selectedIndex, setSelectedIndex] = useState(0);
14
- const [userNavigated, setUserNavigated] = useState(false);
12
+ const filterRef = useRef(filter);
13
+ filterRef.current = filter;
15
14
  const filtered = useMemo(() => {
16
15
  if (!filter)
17
16
  return options;
18
17
  const q = filter.toLowerCase();
19
18
  return options.filter((o) => o.name.toLowerCase().includes(q) || o.value.toLowerCase().includes(q));
20
19
  }, [options, filter]);
21
- const currentIdx = filtered.findIndex((o) => o.value === value);
22
- const displaySelected = userNavigated ? selectedIndex : (currentIdx >= 0 ? currentIdx : 0);
23
- const clampedSelected = Math.min(displaySelected, Math.max(0, filtered.length - 1));
20
+ const clampedSelected = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
24
21
  const filteredRef = useRef(filtered);
25
22
  filteredRef.current = filtered;
26
- const selectedIdxRef = useRef(clampedSelected);
27
- selectedIdxRef.current = clampedSelected;
23
+ const selectedRef = useRef(clampedSelected);
24
+ selectedRef.current = clampedSelected;
28
25
  const focusedRef = useRef(focused);
29
26
  focusedRef.current = focused;
30
27
  const onChangeRef = useRef(onChange);
31
28
  onChangeRef.current = onChange;
32
- const filterRef = useRef(filter);
33
- filterRef.current = filter;
29
+ const scrollRef = useRef(null);
34
30
  useEffect(() => {
35
31
  if (focused) {
36
- setUserNavigated(false);
37
- setSelectedIndex(currentIdx >= 0 ? currentIdx : 0);
32
+ const idx = filtered.findIndex((o) => o.value === value);
33
+ const target = idx >= 0 ? idx : 0;
34
+ setSelectedIndex(target);
35
+ requestAnimationFrame(() => {
36
+ scrollRef.current?.scrollChildIntoView(`select-option-${target}`);
37
+ });
38
38
  }
39
39
  else {
40
40
  setFilter("");
41
- setUserNavigated(false);
42
41
  }
43
42
  }, [focused]);
44
- useInputShortcuts(() => focusedRef.current ? inputRef.current : null);
43
+ useEffect(() => {
44
+ scrollRef.current?.scrollChildIntoView(`select-option-${clampedSelected}`);
45
+ }, [clampedSelected, filter]);
46
+ const computeFiltered = (f) => {
47
+ if (!f)
48
+ return options;
49
+ const q = f.toLowerCase();
50
+ return options.filter((o) => o.name.toLowerCase().includes(q) || o.value.toLowerCase().includes(q));
51
+ };
45
52
  useKeyboard((key) => {
46
53
  if (!focusedRef.current)
47
54
  return;
48
55
  const name = key.name;
49
56
  if (name === "up" || name === "k") {
50
- const list = filteredRef.current;
57
+ const list = computeFiltered(filterRef.current);
51
58
  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);
59
+ const cur = Math.min(selectedRef.current, list.length - 1);
60
+ setSelectedIndex(cur <= 0 ? list.length - 1 : cur - 1);
56
61
  }
57
62
  key.preventDefault();
58
63
  key.stopPropagation();
59
64
  return;
60
65
  }
61
66
  if (name === "down" || name === "j") {
62
- const list = filteredRef.current;
67
+ const list = computeFiltered(filterRef.current);
63
68
  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);
69
+ const cur = Math.min(selectedRef.current, list.length - 1);
70
+ setSelectedIndex(cur >= list.length - 1 ? 0 : cur + 1);
68
71
  }
69
72
  key.preventDefault();
70
73
  key.stopPropagation();
71
74
  return;
72
75
  }
73
76
  if (name === "return" || name === "enter") {
74
- const list = filteredRef.current;
75
- const option = list[selectedIdxRef.current];
77
+ const list = computeFiltered(filterRef.current);
78
+ const cur = Math.min(selectedRef.current, list.length - 1);
79
+ const option = list[cur];
76
80
  if (option) {
77
81
  onChangeRef.current(option.value);
78
82
  }
@@ -81,21 +85,17 @@ export function SearchSelect(props) {
81
85
  return;
82
86
  }
83
87
  if (name === "escape") {
84
- if (filterRef.current) {
85
- setFilter("");
86
- setSelectedIndex(0);
87
- setUserNavigated(false);
88
- if (inputRef.current)
89
- inputRef.current.value = "";
90
- }
88
+ setFilter("");
89
+ setSelectedIndex(0);
91
90
  key.preventDefault();
92
91
  key.stopPropagation();
93
92
  return;
94
93
  }
95
94
  if (name === "backspace") {
96
- setFilter((f) => f.slice(0, -1));
95
+ const newFilter = filterRef.current.slice(0, -1);
96
+ filterRef.current = newFilter;
97
+ setFilter(newFilter);
97
98
  setSelectedIndex(0);
98
- setUserNavigated(false);
99
99
  key.preventDefault();
100
100
  key.stopPropagation();
101
101
  return;
@@ -110,22 +110,23 @@ export function SearchSelect(props) {
110
110
  char = key.sequence;
111
111
  }
112
112
  if (char) {
113
- setFilter((f) => f + char);
113
+ const newFilter = filterRef.current + char;
114
+ filterRef.current = newFilter;
115
+ setFilter(newFilter);
114
116
  setSelectedIndex(0);
115
- setUserNavigated(false);
116
117
  key.preventDefault();
117
118
  key.stopPropagation();
118
119
  return;
119
120
  }
120
121
  });
121
122
  const listHeight = Math.min(filtered.length, maxHeight);
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) => {
123
+ 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("scrollbox", { ref: scrollRef, style: { height: listHeight, backgroundColor: "#0b0b0b" }, children: filtered.map((option, i) => {
123
124
  const isSelected = i === clampedSelected;
124
125
  const isActive = option.value === value;
125
126
  const prefix = isSelected ? "› " : " ";
126
127
  const colorIndicator = option.color ? `\u25cf ` : "";
127
128
  const bg = isSelected ? "#1e3a8a" : undefined;
128
129
  const fg = isSelected ? "#ffffff" : isActive ? "#38bdf8" : "#9ca3af";
129
- return (_jsx("box", { style: { flexDirection: "row", backgroundColor: bg }, children: _jsx("text", { fg: fg, children: `${prefix}${colorIndicator}${option.name}` }) }, option.value));
130
+ return (_jsx("box", { id: `select-option-${i}`, style: { flexDirection: "row", backgroundColor: bg }, children: _jsx("text", { fg: fg, children: `${prefix}${colorIndicator}${option.name}` }) }, option.value));
130
131
  }) })] }));
131
132
  }
@@ -5,7 +5,6 @@ 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";
9
8
  function fit(text, width) {
10
9
  if (width <= 0)
11
10
  return "";
@@ -68,13 +67,12 @@ function TaskNavRow(props) {
68
67
  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)] }) }));
69
68
  }
70
69
  export function TaskInspector(props) {
71
- const { task, onCopy } = props;
70
+ const { task } = props;
72
71
  if (!task) {
73
72
  return (_jsx("box", { title: t("board.inspectorTitle"), border: true, style: { backgroundColor: "#0b0b0b" }, children: _jsx("text", { fg: "#9ca3af", children: t("board.inspectorEmpty") }) }));
74
73
  }
75
74
  const cmd = commandLine(task.command, task.commandArgs);
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")] })] }));
75
+ 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("text", { children: [_jsx("strong", { children: t("board.fieldCommand") }), " ", cmd] }), _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")] })] }));
78
76
  }
79
77
  export function TaskActionButtons(props) {
80
78
  const { task, focused, selectedAction, selectable = true, onAction } = props;
@@ -198,6 +198,14 @@ export function useBoardKeybindings(params) {
198
198
  const { confirm, confirmChoice, setConfirm, setConfirmChoice, helpOpen, setHelpOpen, searchActive, setSearchActive, view, push, pop, setEditTarget, setEditTask, selected, visibleCount, setSelectedIndex, setFilters, setSort, onQuit, destroyLogSocket, logModalRun, setLogModalRun, logModalLines, selectedRunIndex, setSelectedRunIndex, selectedRunCount, focusedPanel, setFocusedPanel, selectedAction, setSelectedAction, onAction, onOpenRunLog, refreshTasks, onViewTasks, onViewProjects, onViewLoops, onAddLoop, onAddTask, onSelectProject, } = params;
199
199
  useKeyboard((key) => {
200
200
  const name = key.name;
201
+ if (key.ctrl && name === "c") {
202
+ if (!logModalRun && !confirm && !helpOpen) {
203
+ onQuit();
204
+ key.preventDefault();
205
+ key.stopPropagation();
206
+ return;
207
+ }
208
+ }
201
209
  if (confirm) {
202
210
  CONFIRM_KEYS[name]?.({ confirm, confirmChoice, setConfirm, setConfirmChoice });
203
211
  key.preventDefault();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loop-task",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
5
5
  "type": "module",
6
6
  "bin": {