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
package/dist/board/App.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useMemo, useRef, useState } from "react";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
3
  import { applyLoopFilters, cycleSortMode, cycleStatusFilter, defaultFilters, } from "./state.js";
4
4
  import { ToastStack, useToasts } from "./toast.js";
5
5
  import { t } from "../i18n/index.js";
6
6
  import { useLoopPolling } from "./hooks/useLoopPolling.js";
7
7
  import { useLogStream } from "./hooks/useLogStream.js";
8
8
  import { useBoardKeybindings } from "./hooks/useBoardKeybindings.js";
9
+ import { useTaskKeybindings } from "./hooks/useTaskKeybindings.js";
9
10
  import { Header } from "./components/Header.js";
10
11
  import { FilterBar } from "./components/FilterBar.js";
11
12
  import { Navigator } from "./components/Navigator.js";
@@ -16,13 +17,42 @@ import { HelpModal } from "./components/HelpModal.js";
16
17
  import { Footer } from "./components/Footer.js";
17
18
  import { ConfirmModal } from "./components/ConfirmModal.js";
18
19
  import { CreateView, createInitialValues } from "./components/CreateForm.js";
20
+ import { TaskForm } from "./components/TaskForm.js";
21
+ import { TaskNavigator, TaskInspector, TaskActionButtons } from "./components/TaskBrowser.js";
22
+ import { TaskFilterBar } from "./components/TaskFilterBar.js";
19
23
  import { LogModal } from "./components/LogModal.js";
20
- import { fetchRunLog, deleteLoop, pauseLoop, resumeLoop, triggerLoop } from "./daemon.js";
24
+ import { ProjectsModal } from "./components/ProjectsModal.js";
25
+ import { ProjectsPage } from "./components/ProjectsPage.js";
26
+ import { fetchRunLog, deleteLoop, pauseLoop, resumeLoop, stopLoop, playLoop, triggerLoop, listTasks, deleteTask, listProjects } from "./daemon.js";
21
27
  import { useBreakpoint } from "./hooks/useBreakpoint.js";
28
+ import { useRouter } from "./router.js";
22
29
  const BOARD_REFRESH_DELAY_MS = 150;
30
+ const VIEW_TO_MODE = {
31
+ create: "create",
32
+ "task-create": "task",
33
+ "task-edit": "task",
34
+ "task-list": "task",
35
+ projects: "projects",
36
+ };
37
+ function resolveMode(confirm, searchActive, helpOpen, view) {
38
+ if (confirm)
39
+ return "confirm";
40
+ if (searchActive)
41
+ return "search";
42
+ if (helpOpen)
43
+ return "help";
44
+ return VIEW_TO_MODE[view] ?? "normal";
45
+ }
46
+ function viewKey(view, editTarget, editTask) {
47
+ if (view === "create")
48
+ return `${view}:${editTarget?.id ?? "new"}`;
49
+ if (view === "task-edit")
50
+ return `${view}:${editTask?.id ?? "new"}`;
51
+ return view;
52
+ }
23
53
  export function App(props) {
24
54
  const { loops, daemonStatus, refresh } = useLoopPolling();
25
- const [view, setView] = useState("board");
55
+ const { view, stack, push, replace, pop } = useRouter("board");
26
56
  const [filters, setFilters] = useState(defaultFilters);
27
57
  const [sort, setSort] = useState("description");
28
58
  const [selectedIndex, setSelectedIndex] = useState(0);
@@ -31,35 +61,84 @@ export function App(props) {
31
61
  const [confirm, setConfirm] = useState(null);
32
62
  const [confirmChoice, setConfirmChoice] = useState(0);
33
63
  const [editTarget, setEditTarget] = useState(null);
64
+ const [editTask, setEditTask] = useState(null);
65
+ const [pendingTaskSelection, setPendingTaskSelection] = useState(null);
34
66
  const [selectedRunIndex, setSelectedRunIndex] = useState(0);
35
67
  const [selectedAction, setSelectedAction] = useState(0);
36
68
  const [focusedPanel, setFocusedPanel] = useState("loops");
37
69
  const [logModalRun, setLogModalRun] = useState(null);
38
70
  const [logModalLines, setLogModalLines] = useState([]);
39
71
  const [logModalLoading, setLogModalLoading] = useState(false);
40
- const { toasts, push } = useToasts();
72
+ const [tasks, setTasks] = useState([]);
73
+ const [taskSelectedIndex, setTaskSelectedIndex] = useState(0);
74
+ const [taskSelectedAction, setTaskSelectedAction] = useState(0);
75
+ const [taskFocusedPanel, setTaskFocusedPanel] = useState("tasks");
76
+ const [taskSearchActive, setTaskSearchActive] = useState(false);
77
+ const [taskQuery, setTaskQuery] = useState("");
78
+ const [projects, setProjects] = useState([]);
79
+ const createProjectTriggerRef = useRef(null);
80
+ const [currentProjectId, setCurrentProjectId] = useState(() => {
81
+ try {
82
+ return localStorage.getItem("loop-current-project") ?? "all";
83
+ }
84
+ catch {
85
+ return "all";
86
+ }
87
+ });
88
+ const [projectsModalOpen, setProjectsModalOpen] = useState(false);
89
+ const { toasts, push: pushToast } = useToasts();
41
90
  const breakpoint = useBreakpoint();
42
- const visible = useMemo(() => applyLoopFilters(loops, filters, sort), [loops, filters, sort]);
91
+ const visible = useMemo(() => applyLoopFilters(currentProjectId === "all" ? loops : loops.filter((l) => (l.projectId ?? "default") === currentProjectId), filters, sort), [loops, filters, sort, currentProjectId]);
43
92
  const clampedIndex = Math.min(selectedIndex, Math.max(0, visible.length - 1));
44
93
  const selected = visible[clampedIndex] ?? null;
45
94
  const selectedId = selected?.id ?? null;
95
+ const selectedDesc = selected?.description ?? "";
46
96
  const prevSelectedId = useRef(null);
47
97
  if (selectedId !== prevSelectedId.current) {
48
98
  prevSelectedId.current = selectedId;
49
99
  setSelectedRunIndex(0);
50
100
  }
51
- const { destroy: destroyLogSocket } = useLogStream(selectedId, view, (error) => push("error", t("board.logStreamError", { message: error.message })));
101
+ useEffect(() => {
102
+ setSelectedAction(0);
103
+ }, [selected?.id]);
104
+ const { destroy: destroyLogSocket } = useLogStream(selectedId, view, (error) => pushToast("error", t("board.logStreamError", { message: error.message })));
105
+ const filteredTasks = useMemo(() => {
106
+ if (!taskQuery)
107
+ return tasks;
108
+ const q = taskQuery.toLowerCase();
109
+ return tasks.filter((t) => `${t.id} ${t.name} ${t.command}`.toLowerCase().includes(q));
110
+ }, [tasks, taskQuery]);
111
+ const taskClampedIndex = Math.min(taskSelectedIndex, Math.max(0, filteredTasks.length - 1));
112
+ const selectedTask = filteredTasks[taskClampedIndex] ?? null;
113
+ async function refreshTasks() {
114
+ try {
115
+ setTasks(await listTasks());
116
+ }
117
+ catch { /* ignore */ }
118
+ }
119
+ useState(() => { void refreshTasks(); });
120
+ async function refreshProjects() {
121
+ try {
122
+ setProjects(await listProjects());
123
+ }
124
+ catch { /* ignore */ }
125
+ }
126
+ useState(() => { void refreshProjects(); });
127
+ useEffect(() => {
128
+ try {
129
+ localStorage.setItem("loop-current-project", currentProjectId);
130
+ }
131
+ catch { }
132
+ }, [currentProjectId]);
52
133
  function runAction(label, action) {
53
134
  return async () => {
54
135
  try {
55
136
  await action();
56
- setTimeout(() => {
57
- void refresh();
58
- }, BOARD_REFRESH_DELAY_MS);
59
- push("success", label);
137
+ setTimeout(() => { void refresh(); }, BOARD_REFRESH_DELAY_MS);
138
+ pushToast("success", label);
60
139
  }
61
140
  catch (error) {
62
- push("error", error instanceof Error ? error.message : String(error));
141
+ pushToast("error", error instanceof Error ? error.message : String(error));
63
142
  }
64
143
  };
65
144
  }
@@ -67,6 +146,11 @@ export function App(props) {
67
146
  if (!selectedId)
68
147
  return;
69
148
  setLogModalRun(run);
149
+ if (run.status === "running") {
150
+ setLogModalLoading(false);
151
+ setLogModalLines([]);
152
+ return;
153
+ }
70
154
  setLogModalLoading(true);
71
155
  setLogModalLines([]);
72
156
  fetchRunLog(selectedId, run.runNumber)
@@ -79,46 +163,104 @@ export function App(props) {
79
163
  setLogModalLoading(false);
80
164
  });
81
165
  }
166
+ function confirmAction(msg, toast, fn) {
167
+ setConfirmChoice(0);
168
+ setConfirm({ message: msg, action: runAction(toast, fn) });
169
+ }
82
170
  function handleAction(action) {
83
171
  if (!selected || !selectedId)
84
172
  return;
85
- switch (action) {
86
- case "pause-resume": {
87
- const actionLabel = selected.status === "paused" ? t("board.actionResume") : t("board.actionPause");
88
- const actionVerb = selected.status === "paused" ? t("board.actionResumed") : t("board.actionPaused");
89
- const actionFn = selected.status === "paused"
90
- ? () => resumeLoop(selectedId)
91
- : () => pauseLoop(selectedId);
92
- setConfirmChoice(0);
93
- setConfirm({
94
- message: t("board.confirmPauseResume", { action: actionLabel, id: selectedId }),
95
- action: runAction(t("board.toastActionId", { verb: actionVerb, id: selectedId }), actionFn),
96
- });
97
- break;
98
- }
99
- case "run": {
100
- setConfirmChoice(0);
101
- setConfirm({
102
- message: t("board.confirmForceRun", { id: selectedId }),
103
- action: runAction(t("board.toastTriggered", { id: selectedId }), () => triggerLoop(selectedId)),
104
- });
105
- break;
173
+ if (action === "edit") {
174
+ setEditTarget(selected);
175
+ push("create");
176
+ return;
177
+ }
178
+ if (action === "delete") {
179
+ confirmAction(t("board.confirmDelete", { desc: selectedDesc }), t("board.toastDeleted", { desc: selectedDesc }), () => deleteLoop(selectedId));
180
+ return;
181
+ }
182
+ if (action === "pause-or-play") {
183
+ if (selected.status === "waiting") {
184
+ confirmAction(t("board.confirmPause", { desc: selectedDesc }), t("board.toastPaused", { desc: selectedDesc }), () => pauseLoop(selectedId));
106
185
  }
107
- case "edit": {
108
- setEditTarget(selected);
109
- setView("create");
110
- break;
186
+ else if (selected.status === "paused") {
187
+ confirmAction(t("board.confirmResume", { desc: selectedDesc }), t("board.toastResumed", { desc: selectedDesc }), () => resumeLoop(selectedId));
111
188
  }
112
- case "delete": {
113
- setConfirmChoice(0);
114
- setConfirm({
115
- message: t("board.confirmDelete", { id: selectedId }),
116
- action: runAction(t("board.toastDeleted", { id: selectedId }), () => deleteLoop(selectedId)),
117
- });
118
- break;
189
+ else {
190
+ confirmAction(t("board.confirmPlay", { desc: selectedDesc }), t("board.toastPlayed", { desc: selectedDesc }), () => playLoop(selectedId));
119
191
  }
192
+ return;
120
193
  }
194
+ if (action === "stop") {
195
+ confirmAction(t("board.confirmStop", { desc: selectedDesc }), t("board.toastStopped", { desc: selectedDesc }), () => stopLoop(selectedId));
196
+ return;
197
+ }
198
+ if (action === "play") {
199
+ const isPaused = selected.status === "paused";
200
+ const msgKey = isPaused ? "board.confirmResume" : "board.confirmPlay";
201
+ const toastKey = isPaused ? "board.toastResumed" : "board.toastPlayed";
202
+ const fn = isPaused ? () => resumeLoop(selectedId) : () => playLoop(selectedId);
203
+ confirmAction(t(msgKey, { desc: selectedDesc }), t(toastKey, { desc: selectedDesc }), fn);
204
+ return;
205
+ }
206
+ if (action === "trigger") {
207
+ confirmAction(t("board.confirmTrigger", { desc: selectedDesc }), t("board.toastTriggered", { desc: selectedDesc }), () => triggerLoop(selectedId));
208
+ return;
209
+ }
210
+ }
211
+ function handleTaskAction(action) {
212
+ if (!selectedTask)
213
+ return;
214
+ if (action === "select") {
215
+ if (!stack.includes("create") && !stack.includes("task-edit"))
216
+ return;
217
+ setPendingTaskSelection({ id: selectedTask.id, name: selectedTask.name });
218
+ pop();
219
+ pushToast("success", t("board.toastTaskSelected", { desc: selectedTask.name }));
220
+ return;
221
+ }
222
+ if (action === "edit") {
223
+ setEditTask(selectedTask);
224
+ push("task-edit");
225
+ return;
226
+ }
227
+ if (action === "delete") {
228
+ setConfirmChoice(0);
229
+ setConfirm({
230
+ message: t("board.confirmDeleteTask", { desc: selectedTask.name }),
231
+ action: async () => {
232
+ await deleteTask(selectedTask.id);
233
+ await refreshTasks();
234
+ setTaskSelectedIndex((i) => Math.max(0, i - 1));
235
+ pushToast("success", t("board.toastTaskDeleted", { desc: selectedTask.name }));
236
+ },
237
+ });
238
+ }
239
+ }
240
+ const cancelCreate = () => { setEditTarget(null); setPendingTaskSelection(null); pop(); };
241
+ const cancelTask = () => { setEditTask(null); pop(); };
242
+ const cancelTaskList = () => pop();
243
+ function handleChooseTask() {
244
+ void refreshTasks();
245
+ push("task-list");
246
+ }
247
+ function handleCreateTask() {
248
+ setEditTask(null);
249
+ push("task-create");
121
250
  }
251
+ const onCreateDone = (updated, _id, desc) => {
252
+ setEditTarget(null);
253
+ setPendingTaskSelection(null);
254
+ pop();
255
+ pushToast("success", updated ? t("board.toastUpdated", { desc }) : t("board.toastStarted", { desc }));
256
+ setTimeout(() => { void refresh(); }, BOARD_REFRESH_DELAY_MS);
257
+ };
258
+ const onTaskDone = (updated, id) => {
259
+ setEditTask(null);
260
+ void refreshTasks();
261
+ pop();
262
+ pushToast("success", updated ? t("board.toastTaskUpdated", { id }) : t("board.toastTaskCreated", { id }));
263
+ };
122
264
  useBoardKeybindings({
123
265
  confirm,
124
266
  confirmChoice,
@@ -129,8 +271,10 @@ export function App(props) {
129
271
  searchActive,
130
272
  setSearchActive,
131
273
  view,
132
- setView,
274
+ push,
275
+ pop,
133
276
  setEditTarget,
277
+ setEditTask,
134
278
  selected,
135
279
  visibleCount: visible.length,
136
280
  setSelectedIndex,
@@ -140,6 +284,7 @@ export function App(props) {
140
284
  destroyLogSocket,
141
285
  logModalRun,
142
286
  setLogModalRun,
287
+ logModalLines,
143
288
  selectedRunIndex,
144
289
  setSelectedRunIndex,
145
290
  selectedRunCount: selected?.runHistory?.length ?? 0,
@@ -149,54 +294,47 @@ export function App(props) {
149
294
  setSelectedAction,
150
295
  onAction: handleAction,
151
296
  onOpenRunLog: handleOpenRunLog,
297
+ refreshTasks,
298
+ onViewTasks: () => { void refreshTasks(); push("task-list"); },
299
+ onViewProjects: () => push("projects"),
300
+ onAddLoop: () => { setEditTarget(null); push("create"); },
301
+ onSelectProject: () => setProjectsModalOpen(true),
302
+ });
303
+ useTaskKeybindings({
304
+ confirm,
305
+ view,
306
+ tasks: filteredTasks,
307
+ taskSelectedIndex,
308
+ setTaskSelectedIndex,
309
+ taskSelectedAction,
310
+ setTaskSelectedAction,
311
+ taskFocusedPanel,
312
+ setTaskFocusedPanel,
313
+ taskSearchActive,
314
+ setTaskSearchActive,
315
+ taskQuery,
316
+ setTaskQuery,
317
+ onTaskAction: handleTaskAction,
318
+ onCancel: cancelTaskList,
319
+ onCreateTask: handleCreateTask,
320
+ selectable: stack.includes("create") || stack.includes("task-edit"),
152
321
  });
153
322
  const counts = {
154
323
  total: loops.length,
155
324
  running: loops.filter((l) => l.status === "running").length,
156
325
  waiting: loops.filter((l) => l.status === "waiting").length,
157
326
  paused: loops.filter((l) => l.status === "paused").length,
327
+ idle: loops.filter((l) => l.status === "idle").length,
158
328
  };
159
- const mode = confirm
160
- ? "confirm"
161
- : searchActive
162
- ? "search"
163
- : helpOpen
164
- ? "help"
165
- : view === "create"
166
- ? "create"
167
- : "normal";
168
- return (_jsxs("box", { style: { flexDirection: "column", width: "100%", height: "100%", backgroundColor: "#0b0b0b" }, children: [_jsx(Header, { daemonStatus: daemonStatus, counts: counts }), view === "board" ? (_jsx(FilterBar, { filters: filters, sort: sort, searchActive: searchActive, focusedPanel: focusedPanel, onSearch: (q) => {
169
- setFilters((prev) => ({ ...prev, query: q }));
170
- setSelectedIndex(0);
171
- }, onStatusCycle: () => setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })), onSortCycle: () => setSort(cycleSortMode(sort)), onNewLoop: () => {
172
- setEditTarget(null);
173
- setView("create");
174
- } })) : null, _jsx("box", { style: { flexGrow: 1, backgroundColor: "#0b0b0b" }, children: view === "create" ? (_jsx(CreateView, { mode: editTarget ? "edit" : "create", editId: editTarget?.id ?? null, initial: createInitialValues(editTarget), onCancel: () => {
175
- setEditTarget(null);
176
- setView("board");
177
- }, onDone: (updated, id) => {
178
- setEditTarget(null);
179
- setView("board");
180
- push("success", updated ? t("board.toastUpdated", { id }) : t("board.toastStarted", { id }));
181
- setTimeout(() => {
182
- void refresh();
183
- }, BOARD_REFRESH_DELAY_MS);
184
- } })) : (_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", onSelect: (index) => {
185
- setSelectedIndex(index);
186
- setFocusedPanel("loops");
187
- }, onActivate: (index) => {
188
- setSelectedIndex(index);
189
- const loop = visible[index];
190
- if (loop) {
191
- setEditTarget(loop);
192
- setView("create");
193
- }
194
- } }, `nav-${visible.length}-${visible[0]?.id ?? ""}`), _jsxs("box", { style: { flexDirection: "column", flexGrow: 1, backgroundColor: "#0b0b0b", overflow: "hidden" }, children: [_jsx(Inspector, { loop: selected }, `insp-${selected?.id}-${selected?.status}`), _jsx(RunHistory, { loop: selected, selectedRunIndex: selectedRunIndex, focused: focusedPanel === "runs", onSelectRun: (index) => {
195
- setSelectedRunIndex(index);
196
- setFocusedPanel("runs");
197
- }, onOpenRun: handleOpenRunLog }, `rh-${selected?.id}-${selected?.runHistory?.length ?? 0}`), _jsx(ActionButtons, { loop: selected, focused: focusedPanel === "actions", selectedAction: selectedAction, onAction: handleAction }, `ab-${selected?.id}`)] })] })) }, view === "create" ? `${view}:${editTarget?.id ?? "new"}` : view), _jsx(Footer, { mode: mode }), confirm ? (_jsx(ConfirmModal, { message: confirm.message, choice: confirmChoice, onYes: () => {
198
- const action = confirm.action;
199
- setConfirm(null);
200
- void action();
201
- }, onNo: () => setConfirm(null) })) : null, helpOpen ? _jsx(HelpModal, {}) : null, logModalRun ? (_jsx(LogModal, { run: logModalRun, logLines: logModalLines, loading: logModalLoading, onClose: () => setLogModalRun(null) })) : null, _jsx(ToastStack, { toasts: toasts })] }));
329
+ const mode = resolveMode(confirm, searchActive || taskSearchActive, helpOpen, view);
330
+ return (_jsxs("box", { style: { flexDirection: "column", width: "100%", height: "100%", backgroundColor: "#0b0b0b" }, children: [_jsx(Header, { daemonStatus: daemonStatus, counts: counts, view: view, focusedPanel: focusedPanel, onViewLoops: () => replace("board"), onViewTasks: () => { void refreshTasks(); push("task-list"); }, onViewProjects: () => push("projects"), onAddLoop: () => { setEditTarget(null); push("create"); }, onAddTask: () => { setEditTask(null); push("task-create"); }, onAddProject: () => { if (view === "projects") {
331
+ createProjectTriggerRef.current?.();
332
+ }
333
+ else {
334
+ 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) {
336
+ setEditTarget(loop);
337
+ 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 })] }));
202
339
  }
340
+ const TASK_FORM_VIEWS = new Set(["task-create", "task-edit"]);
@@ -2,19 +2,39 @@ import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
2
  import { t } from "../../i18n/index.js";
3
3
  import { useHoverState } from "../hooks/useHoverState.js";
4
4
  import { HOVER_BG } from "../../config/constants.js";
5
+ export function getActions(status) {
6
+ const actions = [
7
+ { key: "edit", label: t("board.actionEdit") },
8
+ { key: "delete", label: t("board.actionDelete") },
9
+ ];
10
+ if (status === "waiting") {
11
+ actions.push({ key: "pause", label: t("board.actionPause") });
12
+ actions.push({ key: "stop", label: t("board.actionStop") });
13
+ }
14
+ else if (status === "paused") {
15
+ actions.push({ key: "stop", label: t("board.actionStop") });
16
+ actions.push({ key: "play", label: t("board.actionPlay") });
17
+ }
18
+ else if (status === "idle" || status === "stopped") {
19
+ actions.push({ key: "play", label: t("board.actionPlay") });
20
+ }
21
+ if (status !== "running") {
22
+ actions.push({ key: "trigger", label: t("board.actionTrigger") });
23
+ }
24
+ return actions;
25
+ }
26
+ export function getActionKeys(status) {
27
+ return getActions(status).map((a) => a.key);
28
+ }
29
+ export function getActionCount(status) {
30
+ return getActions(status).length;
31
+ }
5
32
  export function ActionButtons(props) {
6
33
  const { loop, focused, selectedAction, onAction } = props;
7
34
  if (!loop) {
8
35
  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") }) }));
9
36
  }
10
- const isPaused = loop.status === "paused";
11
- const pauseResumeLabel = isPaused ? t("board.actionResume") : t("board.actionPause");
12
- const actions = [
13
- { key: "pause-resume", label: pauseResumeLabel },
14
- { key: "run", label: t("board.actionRun") },
15
- { key: "edit", label: t("board.actionEdit") },
16
- { key: "delete", label: t("board.actionDelete") },
17
- ];
37
+ const actions = getActions(loop.status);
18
38
  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(ActionButton, { label: action.label, selected: focused && selectedAction === i, onMouseDown: () => onAction(action.key) }, action.key))) }));
19
39
  }
20
40
  function ActionButton(props) {