loop-task 1.4.1 → 1.4.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 CHANGED
@@ -61,6 +61,7 @@ export function App(props) {
61
61
  const [confirm, setConfirm] = useState(null);
62
62
  const [confirmChoice, setConfirmChoice] = useState(0);
63
63
  const [editTarget, setEditTarget] = useState(null);
64
+ const [cloneMode, setCloneMode] = useState(false);
64
65
  const [editTask, setEditTask] = useState(null);
65
66
  const [pendingTaskSelection, setPendingTaskSelection] = useState(null);
66
67
  const [selectedRunIndex, setSelectedRunIndex] = useState(0);
@@ -175,6 +176,17 @@ export function App(props) {
175
176
  push("create");
176
177
  return;
177
178
  }
179
+ if (action === "clone") {
180
+ const copy = {
181
+ ...selected,
182
+ id: "",
183
+ description: `${selected.description} - Copy`,
184
+ };
185
+ setEditTarget(copy);
186
+ setCloneMode(true);
187
+ push("create");
188
+ return;
189
+ }
178
190
  if (action === "delete") {
179
191
  confirmAction(t("board.confirmDelete", { desc: selectedDesc }), t("board.toastDeleted", { desc: selectedDesc }), () => deleteLoop(selectedId));
180
192
  return;
@@ -237,7 +249,7 @@ export function App(props) {
237
249
  });
238
250
  }
239
251
  }
240
- const cancelCreate = () => { setEditTarget(null); setPendingTaskSelection(null); pop(); };
252
+ const cancelCreate = () => { setEditTarget(null); setCloneMode(false); setPendingTaskSelection(null); pop(); };
241
253
  const cancelTask = () => { setEditTask(null); pop(); };
242
254
  const cancelTaskList = () => pop();
243
255
  function handleChooseTask() {
@@ -250,6 +262,7 @@ export function App(props) {
250
262
  }
251
263
  const onCreateDone = (updated, _id, desc) => {
252
264
  setEditTarget(null);
265
+ setCloneMode(false);
253
266
  setPendingTaskSelection(null);
254
267
  pop();
255
268
  pushToast("success", updated ? t("board.toastUpdated", { desc }) : t("board.toastStarted", { desc }));
@@ -332,9 +345,9 @@ export function App(props) {
332
345
  }
333
346
  else {
334
347
  push("projects");
335
- } } }), 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") })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: taskFocusedPanel })) : null, _jsx("box", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: view === "create" ? (_jsx(CreateView, { mode: editTarget ? "edit" : "create", editId: 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; } })) : (_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) {
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") })) : view === "task-list" ? (_jsx(TaskFilterBar, { query: taskQuery, searchActive: taskSearchActive, focusedPanel: taskFocusedPanel })) : 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; } })) : (_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) {
336
349
  setEditTarget(loop);
337
350
  push("create");
338
- } } }), _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, {}) : 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 })] }));
351
+ } } }), _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 })] }));
339
352
  }
340
353
  const TASK_FORM_VIEWS = new Set(["task-create", "task-edit"]);
@@ -19,6 +19,7 @@ export function getActions(status) {
19
19
  actions.push({ key: "play", label: t("board.actionPlay") });
20
20
  }
21
21
  if (status !== "running") {
22
+ actions.push({ key: "clone", label: t("board.actionClone") });
22
23
  actions.push({ key: "trigger", label: t("board.actionTrigger") });
23
24
  }
24
25
  return actions;
@@ -40,7 +40,7 @@ export function createInitialValues(loop, currentProjectId) {
40
40
  export function CreateView(props) {
41
41
  const [values, setValues] = useState({
42
42
  ...props.initial,
43
- project: props.currentProjectId ?? props.initial.project ?? "default",
43
+ project: props.initial.project ?? props.currentProjectId ?? "default",
44
44
  });
45
45
  const valuesRef = useRef(values);
46
46
  const [focusIndex, setFocusIndex] = useState(0);
@@ -1,11 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
2
  import { useTerminalDimensions } from "@opentui/react";
3
3
  import { t } from "../../i18n/index.js";
4
- export function HelpModal() {
4
+ export function HelpModal(props) {
5
+ const { view } = props;
5
6
  const { width } = useTerminalDimensions();
6
- const rows = [
7
+ const loopRows = [
7
8
  ["e", "edit loop"],
8
9
  ["d/del", "delete loop"],
10
+ ["c", "clone loop"],
9
11
  ["f", "force run"],
10
12
  ["p", "pause/play (contextual)"],
11
13
  ["s", "stop loop (resets schedule)"],
@@ -13,11 +15,35 @@ export function HelpModal() {
13
15
  ["n", t("board.helpCreate")],
14
16
  ["t", t("board.helpCreateTask")],
15
17
  ["o", t("board.helpCycleSort")],
18
+ ["x", "cycle status filter"],
19
+ ["r", "project filter"],
16
20
  ["←/→", t("board.helpSwitchPanel")],
17
21
  ["/", t("board.helpSearch")],
18
22
  ["h", t("board.helpToggleHelp")],
19
23
  ["esc", t("board.helpQuit")],
20
24
  ];
25
+ const taskRows = [
26
+ ["e", "edit task"],
27
+ ["d/del", "delete task"],
28
+ ["s", "select task (when creating/editing a loop)"],
29
+ ["enter", "focus actions panel"],
30
+ ["n", t("board.helpCreateTask")],
31
+ ["←/→", t("board.helpSwitchPanel")],
32
+ ["/", t("board.helpSearch")],
33
+ ["h", t("board.helpToggleHelp")],
34
+ ["esc", t("board.helpQuit")],
35
+ ];
36
+ const projectRows = [
37
+ ["enter", "focus actions"],
38
+ ["n", "new project"],
39
+ ["e", "edit project"],
40
+ ["d", "delete project"],
41
+ ["←/→", "switch panel (list/actions)"],
42
+ ["h", t("board.helpToggleHelp")],
43
+ ["esc", t("board.helpQuit")],
44
+ ];
45
+ const rows = view === "task-list" ? taskRows : view === "projects" ? projectRows : loopRows;
46
+ const title = view === "task-list" ? "Task List Shortcuts" : view === "projects" ? "Projects Shortcuts" : t("board.helpTitle");
21
47
  return (_jsx("box", { style: {
22
48
  position: "absolute",
23
49
  top: 0,
@@ -27,7 +53,7 @@ export function HelpModal() {
27
53
  justifyContent: "center",
28
54
  alignItems: "center",
29
55
  zIndex: 90,
30
- }, children: _jsx("box", { title: t("board.helpTitle"), border: true, style: {
56
+ }, children: _jsx("box", { title: title, border: true, style: {
31
57
  flexDirection: "column",
32
58
  padding: 1,
33
59
  minWidth: Math.min(52, width - 4),
@@ -80,17 +80,16 @@ export function TaskActionButtons(props) {
80
80
  return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, backgroundColor: "#0b0b0b", justifyContent: "center", alignItems: "center" }, children: _jsx("text", { fg: "#9ca3af", children: t("board.noActions") }) }));
81
81
  }
82
82
  const allActions = [
83
- { key: "select", label: t("board.taskActionSelect"), hotkey: "s" },
84
- { key: "edit", label: t("board.taskActionEdit"), hotkey: "e" },
85
- { key: "delete", label: t("board.taskActionDelete"), hotkey: "d" },
83
+ { key: "select", label: t("board.taskActionSelect") },
84
+ { key: "edit", label: t("board.taskActionEdit") },
85
+ { key: "delete", label: t("board.taskActionDelete") },
86
86
  ];
87
87
  const actions = selectable ? allActions : allActions.filter((a) => a.key !== "select");
88
- return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, paddingLeft: 1, backgroundColor: "#0b0b0b", alignItems: "center" }, children: actions.map((action, i) => (_jsx(TaskActionButton, { label: action.label, hotkey: action.hotkey, selected: focused && selectedAction === i, onMouseDown: () => onAction(action.key) }, action.key))) }));
88
+ return (_jsx("box", { border: true, borderColor: focused ? "#38bdf8" : undefined, style: { flexDirection: "row", height: 3, flexShrink: 0, paddingLeft: 1, backgroundColor: "#0b0b0b", alignItems: "center" }, children: actions.map((action, i) => (_jsx(TaskActionButton, { label: action.label, selected: focused && selectedAction === i, onMouseDown: () => onAction(action.key) }, action.key))) }));
89
89
  }
90
90
  function TaskActionButton(props) {
91
91
  const { isHovered, hoverProps } = useHoverState();
92
92
  const bg = props.selected ? "#1e3a8a" : isHovered ? HOVER_BG : undefined;
93
93
  const fg = props.selected ? "#ffffff" : isHovered ? "#e5e7eb" : "#9ca3af";
94
- const keycapFg = props.selected ? "#93c5fd" : "#6b7280";
95
- return (_jsxs("box", { onMouseDown: props.onMouseDown, style: { backgroundColor: bg, paddingLeft: 1, paddingRight: 1, marginRight: 1, flexDirection: "row", alignItems: "center" }, ...hoverProps, children: [_jsxs("text", { fg: keycapFg, children: ["[", props.hotkey, "] "] }), _jsx("text", { fg: fg, children: _jsx("strong", { children: props.label }) })] }));
94
+ 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 }) }) }));
96
95
  }
@@ -82,6 +82,7 @@ const GLOBAL_KEYS = {
82
82
  e: (p) => p.onAction("edit"),
83
83
  d: (p) => p.onAction("delete"),
84
84
  delete: (p) => p.onAction("delete"),
85
+ c: (p) => p.onAction("clone"),
85
86
  p: (p) => p.onAction("pause-or-play"),
86
87
  s: (p) => p.onAction("stop"),
87
88
  f: (p) => p.onAction("trigger"),
@@ -192,7 +193,6 @@ const BOARD_SHORTCUTS = {
192
193
  o: (p) => p.setSort((prev) => cycleSortMode(prev)),
193
194
  x: (p) => p.setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })),
194
195
  r: (p) => p.onSelectProject?.(),
195
- c: (p) => p.onSelectProject?.(),
196
196
  };
197
197
  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, onAddLoop, onSelectProject, } = params;
package/dist/i18n/en.json CHANGED
@@ -275,6 +275,7 @@
275
275
  "board.actionPause": "Pause",
276
276
  "board.actionStop": "Stop",
277
277
  "board.actionPlay": "Play",
278
+ "board.actionClone": "Clone",
278
279
  "board.actionTrigger": "Force Run",
279
280
  "board.noActions": "Select a loop to see actions",
280
281
  "board.confirmPause": "Pause loop \"{desc}\"?",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loop-task",
3
- "version": "1.4.1",
3
+ "version": "1.4.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": {