loop-task 1.3.0 → 1.4.1

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.
Files changed (43) hide show
  1. package/README.md +279 -195
  2. package/dist/board/App.js +225 -87
  3. package/dist/board/components/ActionButtons.js +28 -8
  4. package/dist/board/components/CreateForm.js +230 -70
  5. package/dist/board/components/CreateProjectModal.js +104 -0
  6. package/dist/board/components/DeleteProjectConfirm.js +53 -0
  7. package/dist/board/components/EditProjectModal.js +106 -0
  8. package/dist/board/components/FilterBar.js +5 -4
  9. package/dist/board/components/Footer.js +19 -6
  10. package/dist/board/components/Header.js +33 -3
  11. package/dist/board/components/HelpModal.js +11 -10
  12. package/dist/board/components/Inspector.js +1 -1
  13. package/dist/board/components/LogModal.js +39 -8
  14. package/dist/board/components/Navigator.js +30 -14
  15. package/dist/board/components/ProjectsModal.js +70 -0
  16. package/dist/board/components/ProjectsPage.js +105 -0
  17. package/dist/board/components/RunHistory.js +48 -9
  18. package/dist/board/components/TaskBrowser.js +96 -0
  19. package/dist/board/components/TaskFilterBar.js +6 -0
  20. package/dist/board/components/TaskForm.js +175 -0
  21. package/dist/board/daemon.js +56 -0
  22. package/dist/board/format.js +19 -12
  23. package/dist/board/hooks/useBoardKeybindings.js +264 -156
  24. package/dist/board/hooks/useTaskKeybindings.js +130 -0
  25. package/dist/board/router.js +16 -0
  26. package/dist/board/state.js +18 -14
  27. package/dist/cli.js +61 -6
  28. package/dist/client/commands.js +157 -1
  29. package/dist/config/constants.js +15 -0
  30. package/dist/config/paths.js +6 -0
  31. package/dist/core/command-runner.js +2 -0
  32. package/dist/core/foreground-loop.js +4 -1
  33. package/dist/core/loop-controller.js +210 -32
  34. package/dist/daemon/index.js +5 -2
  35. package/dist/daemon/manager.js +113 -34
  36. package/dist/daemon/projects.js +104 -0
  37. package/dist/daemon/server.js +177 -6
  38. package/dist/daemon/state.js +32 -1
  39. package/dist/daemon/task-manager.js +58 -0
  40. package/dist/i18n/en.json +140 -21
  41. package/dist/loop-config.js +13 -4
  42. package/dist/shared/clipboard.js +19 -0
  43. package/package.json +13 -3
@@ -1,191 +1,299 @@
1
1
  import { useKeyboard } from "@opentui/react";
2
2
  import { cycleSortMode, cycleStatusFilter } from "../state.js";
3
- const PANEL_ORDER = ["search", "status", "sort", "new", "loops", "runs", "actions"];
4
- const ACTION_COUNT = 4;
3
+ import { copyToClipboard } from "../../shared/clipboard.js";
4
+ import { getActionCount, getActionKeys } from "../components/ActionButtons.js";
5
+ const PANEL_ORDER = ["search", "project-filter", "status", "sort", "header-tasks", "header-projects", "header-new", "loops", "runs", "actions"];
6
+ const PANEL_LEFT = {
7
+ search: "actions",
8
+ "project-filter": "search",
9
+ status: "project-filter",
10
+ sort: "status",
11
+ "header-tasks": "sort",
12
+ "header-projects": "header-tasks",
13
+ "header-new": "header-projects",
14
+ loops: "header-new",
15
+ runs: "loops",
16
+ actions: "runs",
17
+ projects: "header-new",
18
+ };
19
+ const PANEL_RIGHT = {
20
+ search: "project-filter",
21
+ "project-filter": "status",
22
+ status: "sort",
23
+ sort: "header-tasks",
24
+ "header-tasks": "header-projects",
25
+ "header-projects": "header-new",
26
+ "header-new": "loops",
27
+ loops: "runs",
28
+ runs: "actions",
29
+ actions: "search",
30
+ projects: "loops",
31
+ };
5
32
  function nextPanel(current, direction) {
6
- const idx = PANEL_ORDER.indexOf(current);
7
- return PANEL_ORDER[(idx + (direction === "right" ? 1 : PANEL_ORDER.length - 1)) % PANEL_ORDER.length];
33
+ return direction === "left" ? PANEL_LEFT[current] : PANEL_RIGHT[current];
8
34
  }
35
+ const CONFIRM_KEYS = {
36
+ left: (p) => p.setConfirmChoice((c) => (c === 1 ? 0 : 1)),
37
+ right: (p) => p.setConfirmChoice((c) => (c === 1 ? 0 : 1)),
38
+ tab: (p) => p.setConfirmChoice((c) => (c === 1 ? 0 : 1)),
39
+ y: (p) => { const a = p.confirm.action; p.setConfirm(null); void a(); },
40
+ n: (p) => p.setConfirm(null),
41
+ escape: (p) => p.setConfirm(null),
42
+ return: (p) => {
43
+ if (p.confirmChoice === 1) {
44
+ const a = p.confirm.action;
45
+ p.setConfirm(null);
46
+ void a();
47
+ }
48
+ else
49
+ p.setConfirm(null);
50
+ },
51
+ enter: (p) => {
52
+ if (p.confirmChoice === 1) {
53
+ const a = p.confirm.action;
54
+ p.setConfirm(null);
55
+ void a();
56
+ }
57
+ else
58
+ p.setConfirm(null);
59
+ },
60
+ };
61
+ const VIEW_ESCAPE = {
62
+ create: (p) => { p.setEditTarget(null); p.onBack(); },
63
+ "task-create": (p) => { p.setEditTask(null); p.onBack(); },
64
+ "task-edit": (p) => { p.setEditTask(null); p.onBack(); },
65
+ "task-list": (p) => p.onBack(),
66
+ projects: (p) => p.onBack(),
67
+ };
68
+ const OVERLAY_DISMISS = {
69
+ log: (p) => { p.setLogModalRun(null); return true; },
70
+ help: (p) => { p.setHelpOpen(false); return true; },
71
+ search: (p, key) => {
72
+ p.setSearchActive(false);
73
+ p.setFocusedPanel(key === "return" || key === "enter" ? "loops" : "search");
74
+ return true;
75
+ },
76
+ };
77
+ const GLOBAL_KEYS = {
78
+ escape: (p) => { p.destroyLogSocket(); p.onQuit(); },
79
+ h: (p) => p.setHelpOpen(true),
80
+ n: (p) => { p.setEditTarget(null); p.push("create"); },
81
+ t: (p) => { p.setEditTask(null); p.push("task-create"); },
82
+ e: (p) => p.onAction("edit"),
83
+ d: (p) => p.onAction("delete"),
84
+ delete: (p) => p.onAction("delete"),
85
+ p: (p) => p.onAction("pause-or-play"),
86
+ s: (p) => p.onAction("stop"),
87
+ f: (p) => p.onAction("trigger"),
88
+ };
89
+ const panelHandlers = {
90
+ search: (key, p) => {
91
+ if (key === "return" || key === "enter") {
92
+ p.setSearchActive(true);
93
+ return true;
94
+ }
95
+ return false;
96
+ },
97
+ "project-filter": (key, p) => {
98
+ if (key === "return" || key === "enter") {
99
+ p.onSelectProject?.();
100
+ return true;
101
+ }
102
+ return false;
103
+ },
104
+ status: (key, p) => {
105
+ if (key === "return" || key === "enter") {
106
+ p.setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) }));
107
+ return true;
108
+ }
109
+ return false;
110
+ },
111
+ sort: (key, p) => {
112
+ if (key === "return" || key === "enter") {
113
+ p.setSort((prev) => cycleSortMode(prev));
114
+ return true;
115
+ }
116
+ return false;
117
+ },
118
+ "header-tasks": (key, p) => {
119
+ if (key === "return" || key === "enter") {
120
+ p.onViewTasks?.();
121
+ return true;
122
+ }
123
+ return false;
124
+ },
125
+ "header-projects": (key, p) => {
126
+ if (key === "return" || key === "enter") {
127
+ p.onViewProjects?.();
128
+ return true;
129
+ }
130
+ return false;
131
+ },
132
+ "header-new": (key, p) => {
133
+ if (key === "return" || key === "enter") {
134
+ p.onAddLoop?.();
135
+ return true;
136
+ }
137
+ return false;
138
+ },
139
+ loops: (key, p) => {
140
+ if (key === "up" || key === "k") {
141
+ p.setSelectedIndex((i) => Math.max(0, i - 1));
142
+ return true;
143
+ }
144
+ if (key === "down" || key === "j") {
145
+ p.setSelectedIndex((i) => Math.min(p.selected ? p.selected.runHistory.length - 1 : 0, i + 1));
146
+ return true;
147
+ }
148
+ if (key === "return" || key === "enter") {
149
+ p.setFocusedPanel("actions");
150
+ return true;
151
+ }
152
+ return false;
153
+ },
154
+ runs: (key, p) => {
155
+ if (key === "up" || key === "k") {
156
+ p.setSelectedRunIndex((i) => Math.max(0, i - 1));
157
+ return true;
158
+ }
159
+ if (key === "down" || key === "j") {
160
+ p.setSelectedRunIndex((i) => Math.min(p.selectedRunCount - 1, i + 1));
161
+ return true;
162
+ }
163
+ if ((key === "return" || key === "enter") && p.selected && p.selected.runHistory.length > 0) {
164
+ const runs = p.selected.runHistory;
165
+ const idx = Math.min(p.selectedRunIndex, runs.length - 1);
166
+ p.onOpenRunLog(runs[runs.length - 1 - idx]);
167
+ return true;
168
+ }
169
+ return false;
170
+ },
171
+ actions: (key, p) => {
172
+ const count = p.selected ? getActionCount(p.selected.status) : 0;
173
+ const keys = p.selected ? getActionKeys(p.selected.status) : [];
174
+ if (key === "up" || key === "k") {
175
+ p.setSelectedAction((i) => Math.max(0, Math.min(i, count - 1)));
176
+ return true;
177
+ }
178
+ if (key === "down" || key === "j") {
179
+ p.setSelectedAction((i) => Math.min(count - 1, i + 1));
180
+ return true;
181
+ }
182
+ if (key === "return" || key === "enter") {
183
+ p.onAction(keys[p.selectedAction] ?? "edit");
184
+ return true;
185
+ }
186
+ return false;
187
+ },
188
+ projects: () => false,
189
+ };
190
+ const BOARD_SHORTCUTS = {
191
+ "/": (p) => p.setSearchActive(true),
192
+ o: (p) => p.setSort((prev) => cycleSortMode(prev)),
193
+ x: (p) => p.setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })),
194
+ r: (p) => p.onSelectProject?.(),
195
+ c: (p) => p.onSelectProject?.(),
196
+ };
9
197
  export function useBoardKeybindings(params) {
10
- const { confirm, confirmChoice, setConfirm, setConfirmChoice, helpOpen, setHelpOpen, searchActive, setSearchActive, view, setView, setEditTarget, selected, visibleCount, setSelectedIndex, setFilters, setSort, onQuit, destroyLogSocket, logModalRun, setLogModalRun, selectedRunIndex, setSelectedRunIndex, selectedRunCount, focusedPanel, setFocusedPanel, selectedAction, setSelectedAction, onAction, onOpenRunLog, } = params;
198
+ const { confirm, confirmChoice, setConfirm, setConfirmChoice, helpOpen, setHelpOpen, searchActive, setSearchActive, view, push, pop, setEditTarget, setEditTask, selected, visibleCount, setSelectedIndex, setFilters, setSort, onQuit, destroyLogSocket, logModalRun, setLogModalRun, logModalLines, selectedRunIndex, setSelectedRunIndex, selectedRunCount, focusedPanel, setFocusedPanel, selectedAction, setSelectedAction, onAction, onOpenRunLog, refreshTasks, onViewTasks, onViewProjects, onAddLoop, onSelectProject, } = params;
11
199
  useKeyboard((key) => {
12
200
  const name = key.name;
13
201
  if (confirm) {
14
- if (name === "left" || name === "right" || name === "tab") {
15
- setConfirmChoice((c) => (c === 1 ? 0 : 1));
16
- return;
17
- }
18
- if (name === "y") {
19
- const action = confirm.action;
20
- setConfirm(null);
21
- void action();
22
- }
23
- else if (name === "n" || name === "escape") {
24
- setConfirm(null);
25
- }
26
- else if (name === "return" || name === "enter") {
27
- if (confirmChoice === 1) {
28
- const action = confirm.action;
29
- setConfirm(null);
30
- void action();
31
- }
32
- else {
33
- setConfirm(null);
34
- }
35
- }
202
+ CONFIRM_KEYS[name]?.({ confirm, confirmChoice, setConfirm, setConfirmChoice });
36
203
  return;
37
204
  }
38
205
  if (logModalRun) {
39
206
  if (name === "escape" || name === "q") {
40
- setLogModalRun(null);
41
- }
42
- return;
43
- }
44
- if (helpOpen) {
45
- if (name === "h" || name === "escape") {
46
- setHelpOpen(false);
47
- }
48
- return;
49
- }
50
- if (searchActive) {
51
- if (name === "escape") {
52
- setSearchActive(false);
53
- setFocusedPanel("search");
54
- }
55
- else if (name === "return" || name === "enter") {
56
- setSearchActive(false);
57
- setFocusedPanel("loops");
207
+ OVERLAY_DISMISS.log({ setLogModalRun, setHelpOpen, setSearchActive, setFocusedPanel }, name);
208
+ return;
58
209
  }
59
- return;
60
- }
61
- if (view === "create") {
62
- if (name === "escape") {
63
- setEditTarget(null);
64
- setView("board");
210
+ if (key.ctrl && name === "c") {
211
+ copyToClipboard(logModalLines.join("\n"));
212
+ return;
65
213
  }
66
214
  return;
67
215
  }
68
- if (name === "escape") {
69
- destroyLogSocket();
70
- onQuit();
216
+ if (helpOpen && (name === "h" || name === "escape")) {
217
+ OVERLAY_DISMISS.help({ setLogModalRun, setHelpOpen, setSearchActive, setFocusedPanel }, name);
71
218
  return;
72
219
  }
73
- if (name === "h") {
74
- setHelpOpen(true);
220
+ if (helpOpen)
75
221
  return;
76
- }
77
- if (name === "n") {
78
- setEditTarget(null);
79
- setView("create");
222
+ if (searchActive && (name === "escape" || name === "return" || name === "enter")) {
223
+ OVERLAY_DISMISS.search({ setLogModalRun, setHelpOpen, setSearchActive, setFocusedPanel }, name);
80
224
  return;
81
225
  }
82
- if (name === "r") {
83
- onAction("run");
84
- return;
85
- }
86
- if (name === "p") {
87
- onAction("pause-resume");
88
- return;
89
- }
90
- if (name === "delete") {
91
- onAction("delete");
92
- return;
93
- }
94
- if (view === "board") {
95
- if (name === "left" || name === "right") {
96
- if (focusedPanel === "actions") {
97
- setSelectedAction((i) => name === "right"
98
- ? Math.min(ACTION_COUNT - 1, i + 1)
99
- : Math.max(0, i - 1));
100
- }
101
- else {
102
- setFocusedPanel((p) => nextPanel(p, name === "right" ? "right" : "left"));
103
- }
226
+ if (searchActive) {
227
+ if (name === "left") {
228
+ setSearchActive(false);
229
+ setFocusedPanel("actions");
230
+ setSelectedAction((selected ? getActionCount(selected.status) : 0) - 1);
104
231
  return;
105
232
  }
106
- if (focusedPanel === "search") {
107
- if (name === "return" || name === "enter") {
108
- setSearchActive(true);
109
- return;
110
- }
111
- }
112
- else if (focusedPanel === "status") {
113
- if (name === "return" || name === "enter") {
114
- setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) }));
115
- return;
116
- }
117
- }
118
- else if (focusedPanel === "sort") {
119
- if (name === "return" || name === "enter") {
120
- setSort((prev) => cycleSortMode(prev));
121
- return;
122
- }
123
- }
124
- else if (focusedPanel === "new") {
125
- if (name === "return" || name === "enter") {
126
- setEditTarget(null);
127
- setView("create");
128
- return;
129
- }
233
+ if (name === "backspace") {
234
+ setFilters((prev) => ({ ...prev, query: prev.query.slice(0, -1) }));
235
+ return;
130
236
  }
131
- else if (focusedPanel === "loops") {
132
- if (name === "up" || name === "k") {
133
- setSelectedIndex((i) => Math.max(0, i - 1));
134
- return;
135
- }
136
- if (name === "down" || name === "j") {
137
- setSelectedIndex((i) => Math.min(visibleCount - 1, i + 1));
138
- return;
139
- }
140
- if ((name === "return" || name === "enter") && selected) {
141
- setEditTarget(selected);
142
- setView("create");
143
- return;
144
- }
237
+ if (key.sequence && key.sequence.length === 1 && key.sequence >= " " && key.sequence <= "~") {
238
+ setFilters((prev) => ({ ...prev, query: prev.query + key.sequence }));
239
+ return;
145
240
  }
146
- else if (focusedPanel === "runs") {
147
- if (name === "up" || name === "k") {
148
- setSelectedRunIndex((i) => Math.max(0, i - 1));
149
- return;
241
+ return;
242
+ }
243
+ if (name === "left" || name === "right") {
244
+ if (focusedPanel === "actions") {
245
+ const actionCount = selected ? getActionCount(selected.status) : 0;
246
+ if (name === "left" && selectedAction === 0) {
247
+ setFocusedPanel((p) => nextPanel(p, "left"));
150
248
  }
151
- if (name === "down" || name === "j") {
152
- setSelectedRunIndex((i) => Math.min(selectedRunCount - 1, i + 1));
153
- return;
249
+ else if (name === "right" && selectedAction === actionCount - 1) {
250
+ setFocusedPanel((p) => nextPanel(p, "right"));
154
251
  }
155
- if ((name === "return" || name === "enter") && selected && selected.runHistory.length > 0) {
156
- const runs = selected.runHistory;
157
- const idx = Math.min(selectedRunIndex, runs.length - 1);
158
- onOpenRunLog(runs[runs.length - 1 - idx]);
159
- return;
252
+ else {
253
+ setSelectedAction((i) => name === "right"
254
+ ? Math.min(actionCount - 1, i + 1)
255
+ : Math.max(0, i - 1));
160
256
  }
161
257
  }
162
- else if (focusedPanel === "actions") {
163
- if (name === "up" || name === "k") {
164
- setSelectedAction((i) => Math.max(0, i - 1));
165
- return;
166
- }
167
- if (name === "down" || name === "j") {
168
- setSelectedAction((i) => Math.min(ACTION_COUNT - 1, i + 1));
169
- return;
258
+ else {
259
+ const next = nextPanel(focusedPanel, name === "right" ? "right" : "left");
260
+ if (next === "actions") {
261
+ setFocusedPanel("actions");
262
+ setSelectedAction(0);
170
263
  }
171
- if (name === "return" || name === "enter") {
172
- const actionKeys = ["pause-resume", "run", "edit", "delete"];
173
- onAction(actionKeys[selectedAction] ?? "edit");
174
- return;
264
+ else {
265
+ setFocusedPanel(next);
175
266
  }
176
267
  }
177
- if (name === "/") {
178
- setSearchActive(true);
179
- return;
180
- }
181
- if (name === "f") {
182
- setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) }));
183
- return;
184
- }
185
- if (name === "s") {
186
- setSort((prev) => cycleSortMode(prev));
187
- return;
188
- }
268
+ return;
269
+ }
270
+ if (view !== "board" && name === "escape") {
271
+ VIEW_ESCAPE[view]?.({ setEditTarget, setEditTask, onBack: pop });
272
+ return;
273
+ }
274
+ if (view !== "board")
275
+ return;
276
+ const globalHandler = GLOBAL_KEYS[name];
277
+ if (globalHandler) {
278
+ globalHandler({ destroyLogSocket, onQuit, setHelpOpen, setEditTarget, setEditTask, push, onAction });
279
+ return;
280
+ }
281
+ const panelHandler = panelHandlers[focusedPanel];
282
+ if (panelHandler?.(name, {
283
+ setSearchActive, setFilters, setSort, setEditTarget, push,
284
+ setSelectedIndex, setSelectedRunIndex, setFocusedPanel, selectedRunCount, selected,
285
+ selectedRunIndex, setSelectedAction, selectedAction, onAction, onOpenRunLog,
286
+ refreshTasks, onViewTasks, onViewProjects, onAddLoop,
287
+ }))
288
+ return;
289
+ const shortcut = BOARD_SHORTCUTS[name];
290
+ if (shortcut) {
291
+ shortcut({
292
+ setSearchActive, setFilters, setSort, setEditTarget, push,
293
+ setSelectedIndex, setSelectedRunIndex, setFocusedPanel, selectedRunCount, selected,
294
+ selectedRunIndex, setSelectedAction, selectedAction, onAction, onOpenRunLog,
295
+ onSelectProject,
296
+ });
189
297
  }
190
298
  });
191
299
  }
@@ -0,0 +1,130 @@
1
+ import { useKeyboard } from "@opentui/react";
2
+ import { nextTaskPanel } from "../components/TaskBrowser.js";
3
+ export function useTaskKeybindings(params) {
4
+ const { confirm, view, tasks, taskSelectedIndex, setTaskSelectedIndex, taskSelectedAction, setTaskSelectedAction, taskFocusedPanel, setTaskFocusedPanel, taskSearchActive, setTaskSearchActive, taskQuery, setTaskQuery, onTaskAction, onCancel, onCreateTask, selectable = true, } = params;
5
+ const taskActions = selectable
6
+ ? ["select", "edit", "delete"]
7
+ : ["edit", "delete"];
8
+ const taskActionCount = taskActions.length;
9
+ useKeyboard((key) => {
10
+ if (view !== "task-list")
11
+ return;
12
+ const name = key.name;
13
+ if (confirm)
14
+ return;
15
+ if (taskSearchActive) {
16
+ if (name === "escape") {
17
+ setTaskSearchActive(false);
18
+ setTaskFocusedPanel("tasks");
19
+ return;
20
+ }
21
+ if (name === "return" || name === "enter") {
22
+ setTaskSearchActive(false);
23
+ setTaskFocusedPanel("tasks");
24
+ return;
25
+ }
26
+ if (name === "left") {
27
+ setTaskSearchActive(false);
28
+ setTaskFocusedPanel("actions");
29
+ setTaskSelectedAction(taskActionCount - 1);
30
+ return;
31
+ }
32
+ if (name === "backspace") {
33
+ setTaskQuery((q) => q.slice(0, -1));
34
+ return;
35
+ }
36
+ if (key.sequence && key.sequence.length === 1 && key.sequence >= " " && key.sequence <= "~") {
37
+ setTaskQuery((q) => q + key.sequence);
38
+ return;
39
+ }
40
+ return;
41
+ }
42
+ if (name === "escape") {
43
+ onCancel();
44
+ return;
45
+ }
46
+ if (name === "n") {
47
+ onCreateTask();
48
+ return;
49
+ }
50
+ if (name === "/" && taskFocusedPanel === "tasks") {
51
+ setTaskSearchActive(true);
52
+ return;
53
+ }
54
+ if (name === "tab") {
55
+ setTaskFocusedPanel((p) => nextTaskPanel(p, "right"));
56
+ return;
57
+ }
58
+ if (name === "left" || name === "right") {
59
+ if (taskFocusedPanel === "actions") {
60
+ if (name === "left" && taskSelectedAction === 0) {
61
+ setTaskFocusedPanel((p) => nextTaskPanel(p, "left"));
62
+ }
63
+ else if (name === "right" && taskSelectedAction === taskActionCount - 1) {
64
+ setTaskFocusedPanel((p) => nextTaskPanel(p, "right"));
65
+ }
66
+ else {
67
+ setTaskSelectedAction((i) => name === "right"
68
+ ? Math.min(taskActionCount - 1, i + 1)
69
+ : Math.max(0, i - 1));
70
+ }
71
+ }
72
+ else {
73
+ setTaskFocusedPanel((p) => nextTaskPanel(p, name === "right" ? "right" : "left"));
74
+ }
75
+ return;
76
+ }
77
+ if (taskFocusedPanel === "tasks") {
78
+ if (name === "up" || name === "k") {
79
+ setTaskSelectedIndex((i) => Math.max(0, i - 1));
80
+ return;
81
+ }
82
+ if (name === "down" || name === "j") {
83
+ setTaskSelectedIndex((i) => Math.min(tasks.length - 1, i + 1));
84
+ return;
85
+ }
86
+ if (name === "return" || name === "enter") {
87
+ setTaskFocusedPanel("actions");
88
+ return;
89
+ }
90
+ if (name === "e" && tasks[taskSelectedIndex]) {
91
+ onTaskAction("edit");
92
+ return;
93
+ }
94
+ if (name === "d" && tasks[taskSelectedIndex]) {
95
+ onTaskAction("delete");
96
+ return;
97
+ }
98
+ if (name === "delete" && tasks[taskSelectedIndex]) {
99
+ onTaskAction("delete");
100
+ return;
101
+ }
102
+ if (selectable && name === "s" && tasks[taskSelectedIndex]) {
103
+ onTaskAction("select");
104
+ return;
105
+ }
106
+ }
107
+ if (taskFocusedPanel === "actions") {
108
+ if (name === "up" || name === "k") {
109
+ setTaskSelectedAction((i) => Math.max(0, i - 1));
110
+ return;
111
+ }
112
+ if (name === "down" || name === "j") {
113
+ setTaskSelectedAction((i) => Math.min(taskActionCount - 1, i + 1));
114
+ return;
115
+ }
116
+ if (name === "return" || name === "enter") {
117
+ onTaskAction(taskActions[taskSelectedAction] ?? "select");
118
+ return;
119
+ }
120
+ if (name === "d") {
121
+ onTaskAction("delete");
122
+ return;
123
+ }
124
+ if (selectable && name === "s") {
125
+ onTaskAction("select");
126
+ return;
127
+ }
128
+ }
129
+ });
130
+ }
@@ -0,0 +1,16 @@
1
+ import { useState, useCallback } from "react";
2
+ export function useRouter(initial = "board") {
3
+ const [stack, setStack] = useState([initial]);
4
+ const view = stack[stack.length - 1] ?? initial;
5
+ const canGoBack = stack.length > 1;
6
+ const push = useCallback((next) => {
7
+ setStack((s) => [...s, next]);
8
+ }, []);
9
+ const replace = useCallback((next) => {
10
+ setStack((s) => [...s.slice(0, -1), next]);
11
+ }, []);
12
+ const pop = useCallback(() => {
13
+ setStack((s) => (s.length > 1 ? s.slice(0, -1) : s));
14
+ }, []);
15
+ return { view, stack, push, replace, pop, canGoBack };
16
+ }
@@ -8,7 +8,8 @@ const statusOrder = {
8
8
  running: 0,
9
9
  waiting: 1,
10
10
  paused: 2,
11
- stopped: 3,
11
+ idle: 3,
12
+ stopped: 4,
12
13
  };
13
14
  function intervalBucketOf(interval) {
14
15
  if (interval <= 60_000)
@@ -76,20 +77,23 @@ export function applyLoopFilters(loops, filters, sort) {
76
77
  .filter((loop) => matches(loop, filters))
77
78
  .sort((left, right) => compare(left, right, sort));
78
79
  }
80
+ const SORT_CYCLE = {
81
+ description: "status",
82
+ status: "recent",
83
+ recent: "created",
84
+ created: "description",
85
+ };
79
86
  export function cycleSortMode(mode) {
80
- return mode === "description" ? "status" : mode === "status" ? "recent" : mode === "recent" ? "created" : "description";
87
+ return SORT_CYCLE[mode];
81
88
  }
89
+ const STATUS_CYCLE = {
90
+ all: "running",
91
+ running: "waiting",
92
+ waiting: "paused",
93
+ paused: "idle",
94
+ idle: "stopped",
95
+ stopped: "all",
96
+ };
82
97
  export function cycleStatusFilter(status) {
83
- switch (status) {
84
- case "all":
85
- return "running";
86
- case "running":
87
- return "waiting";
88
- case "waiting":
89
- return "paused";
90
- case "paused":
91
- return "stopped";
92
- default:
93
- return "all";
94
- }
98
+ return STATUS_CYCLE[status];
95
99
  }