loop-task 1.5.1 → 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 })) : 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 }, `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
- if (focusedItem === "save" || focusedItem === "cancel")
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 === "return" || key.name === "enter" || key.name === " ") {
94
- if (focusedItem === "taskMode") {
95
- const next = valuesRef.current.taskMode === TASK_MODE_INLINE ? TASK_MODE_EXISTING : TASK_MODE_INLINE;
96
- updateValues({ ...valuesRef.current, taskMode: next });
97
- key.preventDefault();
98
- return;
99
- }
100
- if (focusedItem === "runNow") {
101
- const next = valuesRef.current.runNow === "y" ? "n" : "y";
102
- updateValues({ ...valuesRef.current, runNow: next });
103
- key.preventDefault();
104
- return;
105
- }
106
- if (focusedItem === "taskId" && !isInline) {
107
- props.onChooseTask();
108
- key.preventDefault();
109
- return;
110
- }
111
- if (focusedItem === "save") {
112
- void submit(valuesRef.current);
113
- key.preventDefault();
114
- }
115
- else if (focusedItem === "cancel") {
116
- props.onCancel();
117
- key.preventDefault();
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 (focusField === "cancel") {
25
+ if (focusFieldRef.current === "cancel") {
24
26
  onCancel();
25
27
  return;
26
28
  }
27
29
  void submit();
28
30
  return;
29
31
  }
30
- if (focusField === "color") {
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 (focusField === "cancel") {
27
+ if (focusFieldRef.current === "cancel") {
26
28
  onCancel();
27
29
  return;
28
30
  }
29
31
  void submit();
30
32
  return;
31
33
  }
32
- if (focusField === "color") {
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 (focusedItem !== "list") {
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 (focusedItem === "list") {
77
+ if (fi === "list") {
75
78
  setFocusIndex(1);
76
79
  }
77
- else if (focusedItem === "edit") {
80
+ else if (fi === "edit") {
78
81
  if (selectedProject && !selectedProject.isSystem)
79
82
  setSubModal("edit");
80
83
  }
81
- else if (focusedItem === "delete") {
84
+ else if (fi === "delete") {
82
85
  if (selectedProject && !selectedProject.isSystem)
83
86
  setSubModal("delete");
84
87
  }
@@ -1,66 +1,100 @@
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
- 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
- useInputShortcuts(() => focused ? inputRef.current : null);
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 safeSelected = currentIdx >= 0 ? currentIdx : selectedIndex;
23
- const clampedSelected = Math.min(safeSelected, Math.max(0, filtered.length - 1));
20
+ const clampedSelected = Math.min(selectedIndex, Math.max(0, filtered.length - 1));
21
+ const filteredRef = useRef(filtered);
22
+ filteredRef.current = filtered;
23
+ const selectedRef = useRef(clampedSelected);
24
+ selectedRef.current = clampedSelected;
25
+ const focusedRef = useRef(focused);
26
+ focusedRef.current = focused;
27
+ const onChangeRef = useRef(onChange);
28
+ onChangeRef.current = onChange;
29
+ const scrollRef = useRef(null);
30
+ useEffect(() => {
31
+ if (focused) {
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
+ }
39
+ else {
40
+ setFilter("");
41
+ }
42
+ }, [focused]);
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
+ };
24
52
  useKeyboard((key) => {
25
- if (!focused)
53
+ if (!focusedRef.current)
26
54
  return;
27
55
  const name = key.name;
28
56
  if (name === "up" || name === "k") {
29
- if (filtered.length > 0) {
30
- setSelectedIndex((i) => i <= 0 ? filtered.length - 1 : i - 1);
57
+ const list = computeFiltered(filterRef.current);
58
+ if (list.length > 0) {
59
+ const cur = Math.min(selectedRef.current, list.length - 1);
60
+ setSelectedIndex(cur <= 0 ? list.length - 1 : cur - 1);
31
61
  }
32
62
  key.preventDefault();
33
63
  key.stopPropagation();
34
64
  return;
35
65
  }
36
66
  if (name === "down" || name === "j") {
37
- if (filtered.length > 0) {
38
- setSelectedIndex((i) => i >= filtered.length - 1 ? 0 : i + 1);
67
+ const list = computeFiltered(filterRef.current);
68
+ if (list.length > 0) {
69
+ const cur = Math.min(selectedRef.current, list.length - 1);
70
+ setSelectedIndex(cur >= list.length - 1 ? 0 : cur + 1);
39
71
  }
40
72
  key.preventDefault();
41
73
  key.stopPropagation();
42
74
  return;
43
75
  }
44
76
  if (name === "return" || name === "enter") {
45
- const option = filtered[clampedSelected];
77
+ const list = computeFiltered(filterRef.current);
78
+ const cur = Math.min(selectedRef.current, list.length - 1);
79
+ const option = list[cur];
46
80
  if (option) {
47
- onChange(option.value);
81
+ onChangeRef.current(option.value);
48
82
  }
49
83
  key.preventDefault();
50
84
  key.stopPropagation();
51
85
  return;
52
86
  }
53
87
  if (name === "escape") {
54
- if (filter) {
55
- setFilter("");
56
- setSelectedIndex(0);
57
- }
88
+ setFilter("");
89
+ setSelectedIndex(0);
58
90
  key.preventDefault();
59
91
  key.stopPropagation();
60
92
  return;
61
93
  }
62
94
  if (name === "backspace") {
63
- setFilter((f) => f.slice(0, -1));
95
+ const newFilter = filterRef.current.slice(0, -1);
96
+ filterRef.current = newFilter;
97
+ setFilter(newFilter);
64
98
  setSelectedIndex(0);
65
99
  key.preventDefault();
66
100
  key.stopPropagation();
@@ -68,15 +102,17 @@ export function SearchSelect(props) {
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
- setFilter((f) => f + name);
73
- setSelectedIndex(0);
74
- key.preventDefault();
75
- key.stopPropagation();
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 (key.sequence && key.sequence.length === 1 && /[a-z0-9 _\-./]/i.test(key.sequence)) {
79
- setFilter((f) => f + key.sequence);
112
+ if (char) {
113
+ const newFilter = filterRef.current + char;
114
+ filterRef.current = newFilter;
115
+ setFilter(newFilter);
80
116
  setSelectedIndex(0);
81
117
  key.preventDefault();
82
118
  key.stopPropagation();
@@ -84,16 +120,13 @@ export function SearchSelect(props) {
84
120
  }
85
121
  });
86
122
  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: " / " }), _jsx("input", { ref: inputRef, focused: focused, value: filter, placeholder: placeholder, onInput: (v) => {
88
- setFilter(v);
89
- setSelectedIndex(0);
90
- } })] }), _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) => {
91
124
  const isSelected = i === clampedSelected;
92
125
  const isActive = option.value === value;
93
126
  const prefix = isSelected ? "› " : " ";
94
127
  const colorIndicator = option.color ? `\u25cf ` : "";
95
128
  const bg = isSelected ? "#1e3a8a" : undefined;
96
129
  const fg = isSelected ? "#ffffff" : isActive ? "#38bdf8" : "#9ca3af";
97
- 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));
98
131
  }) })] }));
99
132
  }
@@ -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
- if (focusedItem != null && focusedItem !== "save" && focusedItem !== "cancel") {
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
- if (focusedItem === "save") {
54
+ const fi = focusedItemRef.current;
55
+ if (fi === "save") {
51
56
  void submit(valuesRef.current);
52
57
  key.preventDefault();
53
58
  }
54
- else if (focusedItem === "cancel") {
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
- 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 })) : (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { height: 3, 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: () => {
136
- if (index < taskFields.length - 1) {
137
- setFocusIndex(index + 1);
138
- }
139
- else {
140
- void submit(valuesRef.current);
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();
@@ -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/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.1",
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": {