loop-task 2.1.3 → 2.1.5

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 (148) hide show
  1. package/dist/app/App.js +119 -0
  2. package/dist/{tui → app}/index.js +4 -5
  3. package/dist/app/providers/index.js +1 -0
  4. package/dist/cli/import-writer.js +1 -1
  5. package/dist/cli.js +9 -9
  6. package/dist/client/cli-format.js +7 -0
  7. package/dist/client/commands.js +2 -164
  8. package/dist/client/ipc.js +3 -3
  9. package/dist/client/project-commands.js +159 -0
  10. package/dist/core/{command-runner.js → command/command-runner.js} +6 -13
  11. package/dist/core/{foreground-loop.js → foreground/index.js} +4 -4
  12. package/dist/core/{log-rotator.js → logging/log-rotator.js} +1 -1
  13. package/dist/core/loop/chain-executor.js +67 -0
  14. package/dist/core/loop/delay-utils.js +65 -0
  15. package/dist/core/loop/loop-controller.js +191 -0
  16. package/dist/core/loop/loop-runner.js +133 -0
  17. package/dist/core/loop/run-executor.js +90 -0
  18. package/dist/core/loop/types.js +1 -0
  19. package/dist/daemon/daemon-log.js +1 -1
  20. package/dist/daemon/http/helpers.js +81 -0
  21. package/dist/daemon/http/openapi.js +92 -0
  22. package/dist/daemon/http/route-loops.js +218 -0
  23. package/dist/daemon/http/route-misc.js +36 -0
  24. package/dist/daemon/http/route-projects.js +55 -0
  25. package/dist/daemon/http/route-tasks.js +53 -0
  26. package/dist/daemon/http/routes.js +17 -0
  27. package/dist/daemon/http/server.js +64 -0
  28. package/dist/daemon/http/sse.js +36 -0
  29. package/dist/daemon/index.js +7 -9
  30. package/dist/{ipc/handlers → daemon/ipc}/logs-stream.js +1 -1
  31. package/dist/daemon/managers/loop-entry.js +24 -0
  32. package/dist/daemon/{manager.js → managers/loop-manager.js} +32 -152
  33. package/dist/daemon/managers/loop-options.js +16 -0
  34. package/dist/daemon/managers/loop-serialization.js +43 -0
  35. package/dist/daemon/{projects.js → managers/project-manager.js} +6 -4
  36. package/dist/daemon/{task-manager.js → managers/task-manager.js} +5 -3
  37. package/dist/daemon/server/handlers/index.js +86 -0
  38. package/dist/daemon/server/handlers/log-handlers.js +130 -0
  39. package/dist/daemon/server/handlers/loop-handlers.js +54 -0
  40. package/dist/daemon/server/handlers/project-handlers.js +37 -0
  41. package/dist/daemon/server/handlers/task-handlers.js +20 -0
  42. package/dist/daemon/server/index.js +113 -0
  43. package/dist/daemon/{spawner.js → spawner/index.js} +4 -4
  44. package/dist/daemon/{state.js → state/index.js} +4 -4
  45. package/dist/daemon/{file-watcher.js → watcher/index.js} +8 -6
  46. package/dist/duration.js +1 -1
  47. package/dist/{tui/state.js → entities/loops/filters.js} +0 -76
  48. package/dist/entities/loops/index.js +1 -0
  49. package/dist/entities/projects/filters.js +69 -0
  50. package/dist/entities/projects/index.js +1 -0
  51. package/dist/entities/tasks/filters.js +41 -0
  52. package/dist/entities/tasks/index.js +1 -0
  53. package/dist/{tui/components → features/chain-editor}/ChainEditor.js +7 -5
  54. package/dist/features/code-editor/CodeEditorModal.js +84 -0
  55. package/dist/{tui/components → features/code-editor}/CodeEditorPreview.js +3 -3
  56. package/dist/features/code-editor/useEditorKeyboard.js +147 -0
  57. package/dist/features/code-editor/useModalDimensions.js +9 -0
  58. package/dist/{tui → features/commands}/commands.js +2 -2
  59. package/dist/features/commands/useCommandHandlers.js +147 -0
  60. package/dist/features/commands/useContextualActions.js +75 -0
  61. package/dist/features/commands/useGlobalShortcuts.js +152 -0
  62. package/dist/features/forms/FormRouter.js +44 -0
  63. package/dist/{tui/components → features/overlays}/ContextHelpModal.js +3 -3
  64. package/dist/{tui/components → features/overlays}/ExportModal.js +3 -3
  65. package/dist/{tui/components → features/overlays}/HelpGuideModal.js +3 -3
  66. package/dist/{tui/components → features/overlays}/HelpModal.js +3 -3
  67. package/dist/features/overlays/OverlayStack.js +9 -0
  68. package/dist/{tui/components → features/overlays}/ProjectsModal.js +2 -2
  69. package/dist/{tui/components → features/overlays}/TaskPickerModal.js +2 -2
  70. package/dist/{tui/components → features/overlays}/WelcomeScreen.js +5 -3
  71. package/dist/features/overlays/useOverlayStack.js +51 -0
  72. package/dist/features/state/useAppState.js +150 -0
  73. package/dist/logger.js +0 -1
  74. package/dist/loop-config.js +9 -4
  75. package/dist/{config → shared/config}/constants.js +1 -1
  76. package/dist/shared/container/index.js +22 -0
  77. package/dist/{tui → shared}/hooks/useBreakpoint.js +1 -1
  78. package/dist/shared/hooks/useInject.js +6 -0
  79. package/dist/{tui → shared}/hooks/useLogStream.js +7 -5
  80. package/dist/{hooks → shared/hooks}/useLoopFormValidation.js +2 -2
  81. package/dist/{tui → shared}/hooks/useLoopPolling.js +6 -4
  82. package/dist/shared/{useUndoRedo.js → hooks/useUndoRedo.js} +2 -4
  83. package/dist/shared/providers/InversifyProvider.js +6 -0
  84. package/dist/shared/services/export-service.js +43 -0
  85. package/dist/shared/services/log-service.js +28 -0
  86. package/dist/shared/services/loop-service.js +78 -0
  87. package/dist/shared/services/project-service.js +39 -0
  88. package/dist/shared/services/task-service.js +41 -0
  89. package/dist/shared/services/types.js +7 -0
  90. package/dist/{tui/components → shared/ui}/Button.js +1 -1
  91. package/dist/{tui/components → shared/ui}/DebugPanel.js +1 -1
  92. package/dist/{tui/components → shared/ui}/FocusableButton.js +1 -1
  93. package/dist/{tui/components → shared/ui}/FocusableInput.js +1 -1
  94. package/dist/{tui/components → shared/ui}/FocusableList.js +1 -1
  95. package/dist/{tui/components → shared/ui}/Modal.js +1 -1
  96. package/dist/{tui/components → shared/ui}/SelectModal.js +2 -2
  97. package/dist/{tui/components → shared/ui}/Toast.js +1 -1
  98. package/dist/shared/ui/state.js +9 -0
  99. package/dist/{tui → shared}/utils/paste.js +2 -2
  100. package/dist/{tui → shared}/utils/validation.js +1 -1
  101. package/dist/widgets/command-input/CommandDropdown.js +69 -0
  102. package/dist/widgets/command-input/CommandInput.js +129 -0
  103. package/dist/widgets/command-input/HintBar.js +9 -0
  104. package/dist/widgets/command-input/InputModes.js +68 -0
  105. package/dist/{tui/components → widgets/commands-browser}/CommandsBrowserModal.js +3 -3
  106. package/dist/{tui/components → widgets/header}/Header.js +3 -3
  107. package/dist/{tui/components → widgets/header}/TabBar.js +1 -1
  108. package/dist/{tui/components → widgets/left-panel}/LeftPanel.js +3 -3
  109. package/dist/{tui/components → widgets/left-panel}/Navigator.js +3 -3
  110. package/dist/{tui/components → widgets/left-panel}/ProjectsPage.js +9 -34
  111. package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
  112. package/dist/{tui/components → widgets/left-panel}/TaskBrowser.js +4 -4
  113. package/dist/{tui/components → widgets/log-modal}/LogModal.js +8 -6
  114. package/dist/widgets/loop-form/CreateForm.js +75 -0
  115. package/dist/widgets/loop-form/TextField.js +9 -0
  116. package/dist/{tui/components → widgets/loop-form}/WizardForm.js +5 -10
  117. package/dist/widgets/loop-form/useCreateSteps.js +107 -0
  118. package/dist/widgets/loop-form/useHandleComplete.js +74 -0
  119. package/dist/{tui/components → widgets/project-form}/ProjectForm.js +9 -7
  120. package/dist/{tui/components → widgets/right-panel}/Inspector.js +4 -4
  121. package/dist/{tui/components → widgets/right-panel}/RightPanel.js +4 -4
  122. package/dist/{tui/components → widgets/right-panel}/RunHistory.js +52 -4
  123. package/dist/{tui/components → widgets/task-form}/TaskForm.js +11 -9
  124. package/package.json +2 -1
  125. package/dist/core/loop-controller.js +0 -511
  126. package/dist/daemon/http-server.js +0 -599
  127. package/dist/daemon/server.js +0 -374
  128. package/dist/tui/App.js +0 -680
  129. package/dist/tui/components/CodeEditorModal.js +0 -244
  130. package/dist/tui/components/CommandInput.js +0 -273
  131. package/dist/tui/components/CreateForm.js +0 -224
  132. package/dist/tui/daemon.js +0 -142
  133. package/dist/tui/format.js +0 -111
  134. /package/dist/{tui/router.js → app/router/index.js} +0 -0
  135. /package/dist/{tui → app}/types.js +0 -0
  136. /package/dist/core/{resolve-cwd.js → command/resolve-cwd.js} +0 -0
  137. /package/dist/core/{context-parser.js → context/context-parser.js} +0 -0
  138. /package/dist/core/{template.js → context/template.js} +0 -0
  139. /package/dist/core/{log-parser.js → logging/log-parser.js} +0 -0
  140. /package/dist/core/{scheduling.js → scheduling/index.js} +0 -0
  141. /package/dist/{ipc → daemon/ipc}/send.js +0 -0
  142. /package/dist/{config → shared/config}/paths.js +0 -0
  143. /package/dist/{i18n → shared/i18n}/en.json +0 -0
  144. /package/dist/{i18n → shared/i18n}/index.js +0 -0
  145. /package/dist/{board → shared/ui}/format.js +0 -0
  146. /package/dist/{tui → shared/ui}/hooks/useHoverState.js +0 -0
  147. /package/dist/{tui → shared/ui}/theme.js +0 -0
  148. /package/dist/{tui → shared}/utils/syntax.js +0 -0
package/dist/tui/App.js DELETED
@@ -1,680 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useMemo, useCallback } from "react";
3
- import { Box, useInput, useApp } from "ink";
4
- import { useLoopPolling } from "./hooks/useLoopPolling.js";
5
- import { useLogStream } from "./hooks/useLogStream.js";
6
- import { useBreakpoint } from "./hooks/useBreakpoint.js";
7
- import { ToastStack, useToasts } from "./components/Toast.js";
8
- import { Header } from "./components/Header.js";
9
- import { LeftPanel } from "./components/LeftPanel.js";
10
- import { RightPanel } from "./components/RightPanel.js";
11
- import { CommandInput } from "./components/CommandInput.js";
12
- import { TaskForm } from "./components/TaskForm.js";
13
- import { CreateView } from "./components/CreateForm.js";
14
- import { ProjectFormView } from "./components/ProjectForm.js";
15
- import { LogModal } from "./components/LogModal.js";
16
- import { CommandsBrowserModal } from "./components/CommandsBrowserModal.js";
17
- import { DebugPanel, MAX_ENTRIES } from "./components/DebugPanel.js";
18
- import { ContextHelpModal } from "./components/ContextHelpModal.js";
19
- import { ExportModal } from "./components/ExportModal.js";
20
- import { fetchRunLog, deleteLoop, pauseLoop, resumeLoop, stopLoop, playLoop, triggerLoop, listTasks, deleteTask, listProjects, exportConfig } from "./daemon.js";
21
- import { applyLoopFilters, applyProjectFilters, cycleSortMode, cycleStatusFilter, cycleProjectSortMode, cycleProjectHasLoopsFilter, cycleProjectIsSystemFilter, defaultFilters, defaultProjectFilters, resolveInputOwner } from "./state.js";
22
- import { t } from "../i18n/index.js";
23
- import { copyToClipboard } from "../shared/clipboard.js";
24
- import { POLL_MS } from "../config/constants.js";
25
- import { darkTheme as theme } from "./theme.js";
26
- import { useRouter } from "./router.js";
27
- import { commandLine } from "./format.js";
28
- const TASK_FORM_VIEWS = new Set(["task-create", "task-edit"]);
29
- const FORM_VIEWS = ["create", "task-create", "task-edit", "project-create", "project-edit"];
30
- function viewKey(view, editTarget, editTask) {
31
- if (view === "create")
32
- return `${view}:${editTarget?.id ?? "new"}`;
33
- if (view === "task-edit")
34
- return `${view}:${editTask?.id ?? "new"}`;
35
- return view;
36
- }
37
- function isBoardView(view) {
38
- return view === "board";
39
- }
40
- export function App(props) {
41
- const { onQuit } = props;
42
- const { exit } = useApp();
43
- const { loops, daemonStatus, refresh } = useLoopPolling();
44
- const { view, push, pop } = useRouter("board");
45
- const [activeTab, setActiveTab] = useState("loops");
46
- const [focusedPanel, setFocusedPanel] = useState("left");
47
- const [filters, setFilters] = useState(defaultFilters);
48
- const [sort, setSort] = useState("description");
49
- const [selectedIndex, setSelectedIndex] = useState(0);
50
- const [editTarget, setEditTarget] = useState(null);
51
- const [cloneMode, setCloneMode] = useState(false);
52
- const [editTask, setEditTask] = useState(null);
53
- const [editProject, setEditProject] = useState(null);
54
- const [pendingTaskSelection, setPendingTaskSelection] = useState(null);
55
- const [selectedRunIndex, setSelectedRunIndex] = useState(0);
56
- const [logModalRun, setLogModalRun] = useState(null);
57
- const [logModalLoopId, setLogModalLoopId] = useState(null);
58
- const [logModalLines, setLogModalLines] = useState([]);
59
- const [logModalLoading, setLogModalLoading] = useState(false);
60
- const [tasks, setTasks] = useState([]);
61
- const [taskSelectedIndex, setTaskSelectedIndex] = useState(0);
62
- const [taskQuery, setTaskQuery] = useState("");
63
- const [projects, setProjects] = useState([]);
64
- const [currentProjectId, setCurrentProjectId] = useState("all");
65
- const [projectSelectedIndex, setProjectSelectedIndex] = useState(0);
66
- const [projectFilters, setProjectFilters] = useState(defaultProjectFilters);
67
- const [commandsBrowserOpen, setCommandsBrowserOpen] = useState(false);
68
- const [contextHelpOpen, setContextHelpOpen] = useState(false);
69
- const [exportModal, setExportModal] = useState(null);
70
- const [confirmState, setConfirmState] = useState(null);
71
- const [searchState, setSearchState] = useState(null);
72
- const [searchValue, setSearchValue] = useState("");
73
- const [debugMode, setDebugMode] = useState(false);
74
- const [debugEntries, setDebugEntries] = useState([]);
75
- const [chordState, setChordState] = useState(null);
76
- const [commandBarHasText, setCommandBarHasText] = useState(false);
77
- const [commandBarDropdownOpen, setCommandBarDropdownOpen] = useState(false);
78
- const { toasts, push: pushToast } = useToasts();
79
- const breakpoint = useBreakpoint();
80
- const visible = useMemo(() => applyLoopFilters(currentProjectId === "all" ? loops : loops.filter((l) => (l.projectId ?? "default") === currentProjectId), filters, sort), [loops, filters, sort, currentProjectId]);
81
- const clampedIndex = Math.min(selectedIndex, Math.max(0, visible.length - 1));
82
- const selected = visible[clampedIndex] ?? null;
83
- const selectedId = selected?.id ?? null;
84
- useEffect(() => { setSelectedRunIndex(0); }, [selected?.id]);
85
- useLogStream(selectedId, view, (error) => pushToast("error", error.message));
86
- const filteredTasks = useMemo(() => {
87
- const sorted = [...tasks].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
88
- if (!taskQuery)
89
- return sorted;
90
- const q = taskQuery.toLowerCase();
91
- return sorted.filter((t) => `${t.id} ${t.name} ${t.command}`.toLowerCase().includes(q));
92
- }, [tasks, taskQuery]);
93
- const taskClampedIndex = Math.min(taskSelectedIndex, Math.max(0, filteredTasks.length - 1));
94
- const selectedTask = filteredTasks[taskClampedIndex] ?? null;
95
- const filteredProjects = useMemo(() => applyProjectFilters(projects, loops, projectFilters), [projects, loops, projectFilters]);
96
- const projectClampedIndex = Math.min(projectSelectedIndex, Math.max(0, filteredProjects.length - 1));
97
- const selectedProjectEntity = filteredProjects[projectClampedIndex] ?? null;
98
- const projectLoopCount = selectedProjectEntity
99
- ? loops.filter((l) => (l.projectId ?? "default") === selectedProjectEntity.id).length
100
- : 0;
101
- async function refreshTasks() {
102
- try {
103
- setTasks(await listTasks());
104
- }
105
- catch { /* ignore */ }
106
- }
107
- async function refreshProjects() {
108
- try {
109
- setProjects(await listProjects());
110
- }
111
- catch { /* ignore */ }
112
- }
113
- useEffect(() => { void refreshTasks(); void refreshProjects(); }, []);
114
- useEffect(() => {
115
- const timer = setInterval(() => { void refreshTasks(); void refreshProjects(); }, POLL_MS);
116
- return () => clearInterval(timer);
117
- }, []);
118
- function runAction(label, action) {
119
- return async () => {
120
- try {
121
- await action();
122
- void refresh();
123
- pushToast("success", label);
124
- }
125
- catch (error) {
126
- pushToast("error", error instanceof Error ? error.message : String(error));
127
- }
128
- };
129
- }
130
- function handleOpenRunLog(run) {
131
- if (!selectedId)
132
- return;
133
- setLogModalRun(run);
134
- setLogModalLoopId(selectedId);
135
- if (run.status === "running") {
136
- setLogModalLoading(false);
137
- setLogModalLines([]);
138
- return;
139
- }
140
- setLogModalLoading(true);
141
- setLogModalLines([]);
142
- fetchRunLog(selectedId, run.runNumber)
143
- .then((log) => {
144
- const lines = log
145
- ? log.split("\n").map((l) => l.replace(/\r$/, ""))
146
- : [];
147
- setLogModalLines(lines);
148
- setLogModalLoading(false);
149
- })
150
- .catch((err) => {
151
- setLogModalLines([]);
152
- setLogModalLoading(false);
153
- pushToast("error", err.message ?? "Failed to load log");
154
- });
155
- }
156
- const handleContextualCopy = useCallback(() => {
157
- const copyHandlers = {
158
- loops: () => {
159
- if (!selected)
160
- return;
161
- const text = selected.commandRaw
162
- ? selected.commandRaw.split("\n").map((l) => l.trim()).filter(Boolean).join(" ")
163
- : commandLine(selected.command, selected.commandArgs);
164
- copyToClipboard(text);
165
- pushToast("info", t("board.toastTextCopied"));
166
- },
167
- tasks: () => {
168
- if (!selectedTask)
169
- return;
170
- const text = selectedTask.commandRaw
171
- ? selectedTask.commandRaw.split("\n").map((l) => l.trim()).filter(Boolean).join(" ")
172
- : commandLine(selectedTask.command, selectedTask.commandArgs);
173
- copyToClipboard(text);
174
- pushToast("info", t("board.toastTextCopied"));
175
- },
176
- projects: () => {
177
- if (!selectedProjectEntity)
178
- return;
179
- copyToClipboard(selectedProjectEntity.name);
180
- pushToast("info", t("board.toastTextCopied"));
181
- },
182
- };
183
- const handler = copyHandlers[activeTab];
184
- if (handler)
185
- handler();
186
- }, [activeTab, selected, selectedTask, selectedProjectEntity, pushToast]);
187
- const commandHandlers = {
188
- edit: () => {
189
- if (activeTab === "loops" && selected) {
190
- setCloneMode(false);
191
- setEditTarget(selected);
192
- if (selected.taskId) {
193
- const task = tasks.find((t) => t.id === selected.taskId);
194
- setPendingTaskSelection(task ? { id: task.id, name: task.name } : null);
195
- }
196
- else {
197
- setPendingTaskSelection(null);
198
- }
199
- push("create");
200
- }
201
- else if (activeTab === "tasks" && selectedTask) {
202
- setEditTask(selectedTask);
203
- push("task-edit");
204
- }
205
- else if (activeTab === "projects" && selectedProjectEntity && !selectedProjectEntity.isSystem) {
206
- setEditProject(selectedProjectEntity);
207
- push("project-edit");
208
- }
209
- },
210
- clone: () => {
211
- if (activeTab === "loops" && selected) {
212
- setCloneMode(true);
213
- setEditTarget(selected);
214
- push("create");
215
- }
216
- },
217
- delete: () => {
218
- if (activeTab === "loops" && selected) {
219
- setConfirmState({
220
- prompt: t("confirm.deleteLoop", { name: selected.description || selected.id }),
221
- onConfirm: () => { void deleteLoop(selected.id).then(() => { void refresh(); }); },
222
- });
223
- }
224
- else if (activeTab === "tasks" && selectedTask) {
225
- setConfirmState({
226
- prompt: t("confirm.deleteTask", { id: selectedTask.id }),
227
- onConfirm: () => { void deleteTask(selectedTask.id).then(() => { void refreshTasks(); }); },
228
- });
229
- }
230
- else if (activeTab === "projects" && selectedProjectEntity && !selectedProjectEntity.isSystem) {
231
- setConfirmState({
232
- prompt: t("confirm.deleteProject", { name: selectedProjectEntity.name }),
233
- onConfirm: async () => {
234
- try {
235
- const { deleteProject: dp } = await import("./daemon.js");
236
- await dp(selectedProjectEntity.id);
237
- pushToast("success", t("project.toastDeleted", { name: selectedProjectEntity.name }));
238
- setProjectSelectedIndex(0);
239
- void refreshProjects();
240
- }
241
- catch (e) {
242
- pushToast("error", e.message);
243
- }
244
- },
245
- });
246
- }
247
- },
248
- pause: () => {
249
- if (activeTab === "loops" && selected) {
250
- void runAction(t("board.toastPaused", { desc: selected.description }), () => pauseLoop(selected.id))();
251
- }
252
- },
253
- play: () => {
254
- if (activeTab === "loops" && selected) {
255
- const isPaused = selected.status === "paused";
256
- const toastKey = isPaused ? "board.toastResumed" : "board.toastPlayed";
257
- const fn = isPaused ? () => resumeLoop(selected.id) : () => playLoop(selected.id);
258
- void runAction(t(toastKey, { desc: selected.description }), fn)();
259
- }
260
- },
261
- stop: () => {
262
- if (activeTab === "loops" && selected) {
263
- setConfirmState({
264
- prompt: t("confirm.stopLoop", { name: selected.description || selected.id }),
265
- onConfirm: () => { void stopLoop(selected.id).then(() => { void refresh(); }); },
266
- });
267
- }
268
- },
269
- trigger: () => {
270
- if (activeTab === "loops" && selected) {
271
- void runAction(t("board.toastTriggered", { desc: selected.description }), () => triggerLoop(selected.id))();
272
- }
273
- },
274
- "new-loop": () => { setEditTarget(null); push("create"); },
275
- "new-task": () => { setEditTask(null); push("task-create"); },
276
- "new-project": () => { setEditProject(null); setActiveTab("projects"); push("project-create"); },
277
- "project-filter-loops": () => { setProjectFilters((prev) => ({ ...prev, hasLoops: cycleProjectHasLoopsFilter(prev.hasLoops) })); },
278
- "project-filter-type": () => { setProjectFilters((prev) => ({ ...prev, isSystem: cycleProjectIsSystemFilter(prev.isSystem) })); },
279
- "project-sort": () => { setProjectFilters((prev) => ({ ...prev, sort: cycleProjectSortMode(prev.sort) })); },
280
- "all-commands": () => { setCommandsBrowserOpen(true); },
281
- help: () => { setCommandsBrowserOpen(true); },
282
- search: () => { setSearchValue(""); setSearchState({ active: true }); },
283
- "filter-status": () => { setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })); },
284
- sort: () => { setSort((prev) => cycleSortMode(prev)); },
285
- "filter-project": () => {
286
- const ids = ["all", ...projects.map((p) => p.id)];
287
- const idx = ids.indexOf(currentProjectId);
288
- const nextId = ids[(idx + 1) % ids.length] ?? "all";
289
- setCurrentProjectId(nextId);
290
- },
291
- debug: () => { setDebugMode((prev) => !prev); },
292
- logs: () => {
293
- if (activeTab === "loops" && selected) {
294
- const runs = selected.runHistory;
295
- if (runs && runs.length > 0) {
296
- handleOpenRunLog(runs[runs.length - 1]);
297
- }
298
- }
299
- },
300
- select: () => {
301
- if (selectedTask) {
302
- setPendingTaskSelection({ id: selectedTask.id, name: selectedTask.name });
303
- pop();
304
- }
305
- },
306
- api: () => {
307
- const port = process.env.LOOP_CLI_HTTP_PORT ?? "8845";
308
- const baseUrl = `http://127.0.0.1:${port}`;
309
- pushToast("info", `API: ${baseUrl} | Swagger: ${baseUrl}/api/docs | OpenAPI: ${baseUrl}/api/openapi.json`);
310
- },
311
- status: () => { pushToast("info", `Command "status" coming soon`); },
312
- export: () => {
313
- exportConfig()
314
- .then(({ json, filePath }) => setExportModal({ json, filePath, error: null }))
315
- .catch((e) => setExportModal({ json: "", filePath: null, error: e instanceof Error ? e.message : String(e) }));
316
- },
317
- import: () => { pushToast("info", `Command "import" coming soon`); },
318
- };
319
- function handleCommand(value) {
320
- const handler = commandHandlers[value];
321
- if (handler) {
322
- handler();
323
- }
324
- else {
325
- pushToast("error", `Unknown command: ${value}`);
326
- }
327
- }
328
- const handleConfirmYes = useCallback(() => {
329
- if (confirmState) {
330
- confirmState.onConfirm();
331
- setConfirmState(null);
332
- }
333
- }, [confirmState]);
334
- const handleConfirmCancel = useCallback(() => {
335
- setConfirmState(null);
336
- }, []);
337
- const cancelCreate = () => { setEditTarget(null); setCloneMode(false); setPendingTaskSelection(null); pop(); };
338
- const cancelTask = () => { setEditTask(null); pop(); };
339
- const cancelProject = () => { setEditProject(null); pop(); };
340
- const handleChooseTask = (task) => {
341
- setPendingTaskSelection(task);
342
- };
343
- const onCreateDone = (updated, _id, desc) => {
344
- setEditTarget(null);
345
- setCloneMode(false);
346
- setPendingTaskSelection(null);
347
- pop();
348
- pushToast("success", updated ? t("board.toastUpdated", { desc }) : t("board.toastStarted", { desc }));
349
- void refresh();
350
- };
351
- const onTaskDone = (updated, id) => {
352
- setEditTask(null);
353
- void refreshTasks();
354
- pop();
355
- pushToast("success", updated ? t("board.toastTaskUpdated", { id }) : t("board.toastTaskCreated", { id }));
356
- };
357
- const onProjectDone = (updated, name) => {
358
- setEditProject(null);
359
- void refreshProjects();
360
- pop();
361
- pushToast("success", updated ? t("project.toastUpdated", { name }) : t("project.toastCreated", { name }));
362
- };
363
- const leftPanelQuery = searchState?.active
364
- ? searchValue
365
- : activeTab === "tasks" ? taskQuery : filters.query;
366
- const handleSearchChange = useCallback((value) => {
367
- setSearchValue(value);
368
- if (activeTab === "tasks") {
369
- setTaskQuery(value);
370
- }
371
- else {
372
- setFilters((prev) => ({ ...prev, query: value }));
373
- }
374
- }, [activeTab]);
375
- const handleSearchSubmit = useCallback(() => {
376
- setSearchState(null);
377
- }, []);
378
- const handleSearchCancel = useCallback(() => {
379
- setSearchValue("");
380
- if (activeTab === "tasks") {
381
- setTaskQuery("");
382
- }
383
- else {
384
- setFilters((prev) => ({ ...prev, query: "" }));
385
- }
386
- setSearchState(null);
387
- }, [activeTab]);
388
- const commandContext = useMemo(() => ({ activeTab, selectedLoop: selected, selectedTask, selectedProject: selectedProjectEntity }), [activeTab, selected, selectedTask]);
389
- // Contextual action for the focused panel: shared by Ctrl+Enter and, for
390
- // terminals that collapse Ctrl+Enter to plain Enter, by Enter on an empty
391
- // command bar. Loops-left/tasks/projects -> edit; loops-right -> latest log.
392
- const triggerContextualAction = () => {
393
- if (!isBoardView(view) || logModalRun || commandsBrowserOpen || confirmState || searchState?.active)
394
- return;
395
- if (chordState)
396
- setChordState(null);
397
- const editSelectedLoop = () => {
398
- if (!selected)
399
- return;
400
- setCloneMode(false);
401
- setEditTarget(selected);
402
- const task = selected.taskId ? tasks.find((t) => t.id === selected.taskId) : null;
403
- setPendingTaskSelection(task ? { id: task.id, name: task.name } : null);
404
- push("create");
405
- };
406
- const handlers = {
407
- "tasks:": () => { if (selectedTask)
408
- handleCommand("edit"); },
409
- "projects:": () => { if (selectedProjectEntity)
410
- handleCommand("edit"); },
411
- "loops:right": () => {
412
- if (!selected)
413
- return;
414
- const runs = selected.runHistory;
415
- if (runs && runs.length > 0)
416
- handleOpenRunLog(runs[runs.length - 1]);
417
- else
418
- editSelectedLoop();
419
- },
420
- "loops:left": editSelectedLoop,
421
- };
422
- const handlerKey = activeTab !== "loops" ? `${activeTab}:` : `loops:${focusedPanel}`;
423
- handlers[handlerKey]?.();
424
- };
425
- const popLayer = () => {
426
- if (confirmState) {
427
- setConfirmState(null);
428
- return true;
429
- }
430
- if (searchState?.active) {
431
- setSearchState(null);
432
- setSearchValue("");
433
- return true;
434
- }
435
- if (logModalRun) {
436
- setLogModalRun(null);
437
- setLogModalLoopId(null);
438
- return true;
439
- }
440
- if (commandsBrowserOpen) {
441
- setCommandsBrowserOpen(false);
442
- return true;
443
- }
444
- if (exportModal) {
445
- setExportModal(null);
446
- return true;
447
- }
448
- if (contextHelpOpen) {
449
- setContextHelpOpen(false);
450
- return true;
451
- }
452
- if (view !== "board") {
453
- pop();
454
- return true;
455
- }
456
- // Bare board: open quit confirm
457
- setConfirmState({
458
- prompt: t("confirm.quit"),
459
- onConfirm: () => { onQuit(); exit(); },
460
- });
461
- return true;
462
- };
463
- useInput((input, key) => {
464
- // Ctrl+C always quits if no modal open
465
- if (key.ctrl && input === "c") {
466
- if (!logModalRun && !confirmState && !commandsBrowserOpen && !exportModal) {
467
- setConfirmState({
468
- prompt: t("confirm.quit"),
469
- onConfirm: () => { onQuit(); exit(); },
470
- });
471
- return;
472
- }
473
- }
474
- // Debug: capture key info if debug mode is on
475
- if (debugMode) {
476
- const entry = {
477
- id: Date.now(),
478
- input,
479
- len: input.length,
480
- ctrl: key.ctrl,
481
- return: key.return,
482
- shift: key.shift,
483
- meta: key.meta,
484
- tab: key.tab,
485
- upArrow: key.upArrow,
486
- downArrow: key.downArrow,
487
- leftArrow: key.leftArrow,
488
- rightArrow: key.rightArrow,
489
- escape: key.escape,
490
- codes: Array.from(input).map((c) => c.charCodeAt(0)).join(","),
491
- };
492
- setDebugEntries((prev) => [entry, ...prev].slice(0, MAX_ENTRIES));
493
- }
494
- // Ctrl+Enter: open log (right panel) or edit (left panel)
495
- // Detection across terminals:
496
- // - proper terminals: key.ctrl && key.return
497
- // - Linux: input === "\x0e"
498
- // - VS Code: a multi-char sequence containing \r (13) or \n (10),
499
- // e.g. "\\\r" (codes 92,13) - all key flags zero. Plain Enter is "\r" (len 1).
500
- const isMultiCharEnter = input.length > 1 && (input.includes("\r") || input.includes("\n"));
501
- const isCtrlEnter = (key.ctrl && key.return) || input === "\x0e" || isMultiCharEnter;
502
- if (isCtrlEnter && isBoardView(view) && !logModalRun && !commandsBrowserOpen && !confirmState && !searchState?.active) {
503
- triggerContextualAction();
504
- return;
505
- }
506
- // Ctrl shortcuts: dispatch to command handler (bottom input ignores all ctrl)
507
- const canShortcut = key.ctrl && isBoardView(view) && !logModalRun && !commandsBrowserOpen && !confirmState && !searchState?.active && !exportModal;
508
- if (canShortcut) {
509
- const globalShortcuts = {
510
- p: () => setCommandsBrowserOpen(true),
511
- b: () => handleCommand("debug"),
512
- g: () => handleCommand("api"),
513
- x: () => handleCommand("export"),
514
- i: () => handleCommand("import"),
515
- y: () => handleCommand("status"),
516
- };
517
- const filterShortcuts = {
518
- s: () => handleCommand("search"),
519
- t: () => handleCommand("filter-status"),
520
- o: () => handleCommand("sort"),
521
- p: () => handleCommand("filter-project"),
522
- };
523
- const actionShortcuts = {
524
- n: () => handleCommand(activeTab === "loops" ? "new-loop" : activeTab === "tasks" ? "new-task" : "new-project"),
525
- e: () => handleCommand("edit"),
526
- d: () => handleCommand("delete"),
527
- ...(activeTab === "loops" ? {
528
- p: () => handleCommand("pause"),
529
- r: () => handleCommand("play"),
530
- s: () => handleCommand("stop"),
531
- t: () => handleCommand("trigger"),
532
- c: () => handleCommand("clone"),
533
- o: () => handleCommand("logs"),
534
- } : {}),
535
- };
536
- if (chordState === "ctrl+f" && filterShortcuts[input]) {
537
- filterShortcuts[input]();
538
- setChordState(null);
539
- return;
540
- }
541
- if (chordState === "ctrl+a" && actionShortcuts[input]) {
542
- actionShortcuts[input]();
543
- setChordState(null);
544
- return;
545
- }
546
- if (chordState) {
547
- setChordState(null);
548
- }
549
- if (input === "f") {
550
- setChordState("ctrl+f");
551
- return;
552
- }
553
- if (input === "a") {
554
- setChordState("ctrl+a");
555
- return;
556
- }
557
- const globalHandler = globalShortcuts[input];
558
- if (globalHandler) {
559
- globalHandler();
560
- return;
561
- }
562
- }
563
- // Confirm mode is handled by CommandInput component, not here (non-Escape only)
564
- if (confirmState && !key.escape)
565
- return;
566
- // Search mode is handled by CommandInput component, not here (non-Escape only)
567
- if (searchState?.active && !key.escape)
568
- return;
569
- if (key.escape) {
570
- popLayer();
571
- return;
572
- }
573
- if (logModalRun) {
574
- if (input === "q") {
575
- setLogModalRun(null);
576
- setLogModalLoopId(null);
577
- }
578
- return;
579
- }
580
- if (commandsBrowserOpen) {
581
- return;
582
- }
583
- if (contextHelpOpen) {
584
- // Escape handled by popLayer above; dismiss on any other key
585
- setContextHelpOpen(false);
586
- return;
587
- }
588
- // Any other modal open: block all input from reaching panels
589
- if (logModalRun)
590
- return;
591
- // Full-screen form views: escape handled by popLayer above
592
- if (FORM_VIEWS.includes(view))
593
- return;
594
- // Board view: Tab cycles panels (8.2), 1/2/3 switch tabs (8.1)
595
- if (isBoardView(view)) {
596
- // Ctrl+Arrow: cycle tabs in a loop (loops -> tasks -> projects -> loops)
597
- if (key.ctrl && key.rightArrow) {
598
- setActiveTab((prev) => prev === "loops" ? "tasks" : prev === "tasks" ? "projects" : "loops");
599
- return;
600
- }
601
- if (key.ctrl && key.leftArrow) {
602
- setActiveTab((prev) => prev === "loops" ? "projects" : prev === "tasks" ? "loops" : "tasks");
603
- return;
604
- }
605
- if (key.tab) {
606
- if (key.shift) {
607
- setFocusedPanel((prev) => prev === "left" ? "right" : "left");
608
- }
609
- else {
610
- setFocusedPanel((prev) => prev === "left" ? "right" : "left");
611
- }
612
- return;
613
- }
614
- if (input === "1") {
615
- setActiveTab("loops");
616
- return;
617
- }
618
- if (input === "2") {
619
- setActiveTab("tasks");
620
- return;
621
- }
622
- if (input === "3") {
623
- setActiveTab("projects");
624
- return;
625
- }
626
- }
627
- });
628
- // Modals disable panel input behind them; CommandInput stays active for confirm/search
629
- const anyModalOpen = !!(logModalRun || commandsBrowserOpen || exportModal);
630
- const commandInputDisabled = anyModalOpen;
631
- const inputOwner = useMemo(() => resolveInputOwner({
632
- modalOpen: !!(logModalRun || commandsBrowserOpen || exportModal || contextHelpOpen || confirmState || searchState?.active),
633
- commandBarHasText,
634
- commandBarDropdownOpen,
635
- }), [logModalRun, commandsBrowserOpen, exportModal, contextHelpOpen, confirmState, searchState?.active, commandBarHasText, commandBarDropdownOpen]);
636
- const counts = {
637
- total: loops.length,
638
- running: loops.filter((l) => l.status === "running").length,
639
- waiting: loops.filter((l) => l.status === "waiting").length,
640
- paused: loops.filter((l) => l.status === "paused").length,
641
- idle: loops.filter((l) => l.status === "idle").length,
642
- };
643
- const tabCounts = { loops: loops.length, tasks: tasks.length, projects: projects.length };
644
- return (_jsxs(Box, { flexDirection: "column", width: "100%", height: process.stdout.rows || 24, backgroundColor: theme.bg.base, children: [_jsx(Header, { daemonStatus: daemonStatus, counts: counts, activeTab: activeTab, onTabChange: setActiveTab, tabCounts: tabCounts }), _jsx(Box, { flexGrow: 1, 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, tasks: tasks, 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 === "project-create" || view === "project-edit" ? (_jsx(ProjectFormView, { mode: view === "project-edit" ? "edit" : "create", editProject: view === "project-edit" ? editProject : null, onCancel: cancelProject, onDone: onProjectDone })) : (
645
- // Board view: left panel + right panel (+ optional debug panel)
646
- _jsxs(Box, { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, children: [_jsx(LeftPanel, { isFocused: focusedPanel === "left" && !anyModalOpen, navActive: inputOwner === "panel", activeTab: activeTab, query: leftPanelQuery, loops: visible, selectedIndex: clampedIndex, filters: filters, sort: sort, breakpoint: breakpoint, projects: projects, onSelect: (index) => setSelectedIndex(index), onActivate: (index) => { setSelectedIndex(index); }, tasks: filteredTasks, taskSelectedIndex: taskClampedIndex, onTaskSelect: (index) => setTaskSelectedIndex(index), onTaskActivate: (index) => { setTaskSelectedIndex(index); setEditTask(filteredTasks[index] ?? null); push("task-edit"); }, onStatusCycle: () => setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })), onSortCycle: () => setSort(cycleSortMode(sort)), onSelectProject: () => setActiveTab("projects"), currentProjectName: currentProjectId === "all" ? t("project.showAll") : (projects.find(p => p.id === currentProjectId)?.name ?? "Default"), projectFilters: projectFilters, projectSelectedIndex: projectClampedIndex, onProjectSelect: (index) => setProjectSelectedIndex(index), onProjectActivate: (index) => { setProjectSelectedIndex(index); }, projectLoops: loops }), _jsx(RightPanel, { isFocused: focusedPanel === "right" && !anyModalOpen, navActive: inputOwner === "panel", activeTab: activeTab, loop: selected, selectedRunIndex: selectedRunIndex, onSelectRun: (index) => setSelectedRunIndex(index), onOpenRun: handleOpenRunLog, selectedTask: selectedTask, allTasks: tasks, selectedProject: selectedProjectEntity, projectLoopCount: projectLoopCount, projects: projects, onProjectEdit: () => { if (selectedProjectEntity && !selectedProjectEntity.isSystem) {
647
- commandHandlers["edit"]?.();
648
- } }, onProjectDelete: () => { if (selectedProjectEntity && !selectedProjectEntity.isSystem) {
649
- commandHandlers["delete"]?.();
650
- } } }), debugMode ? _jsx(DebugPanel, { entries: debugEntries }) : null] })) }, viewKey(view, editTarget, editTask)), isBoardView(view) ? (_jsx(CommandInput, { context: commandContext, onCommand: handleCommand, confirmState: confirmState, searchState: searchState, searchValue: searchValue, onSearchChange: handleSearchChange, onSearchSubmit: handleSearchSubmit, onSearchCancel: handleSearchCancel, onConfirmYes: handleConfirmYes, onConfirmCancel: handleConfirmCancel, onCopy: handleContextualCopy, onPanelAction: triggerContextualAction, disabled: commandInputDisabled, navOwner: inputOwner, onInputStateChange: (hasText, dropdownOpen) => {
651
- setCommandBarHasText(hasText);
652
- setCommandBarDropdownOpen(dropdownOpen);
653
- } })) : null, commandsBrowserOpen ? (_jsx(CommandsBrowserModal, { context: commandContext, onClose: () => setCommandsBrowserOpen(false), onExecute: (value) => { setCommandsBrowserOpen(false); handleCommand(value); } })) : null, contextHelpOpen ? _jsx(ContextHelpModal, { onClose: () => setContextHelpOpen(false) }) : null, exportModal ? (_jsx(ExportModal, { json: exportModal.json, filePath: exportModal.filePath, error: exportModal.error, onClose: () => setExportModal(null), onCopy: () => pushToast("success", t("board.toastCopied")) })) : null, logModalRun ? (_jsx(LogModal, { loopId: logModalLoopId, run: logModalRun, logLines: logModalLines, loading: logModalLoading, onClose: () => { setLogModalRun(null); setLogModalLoopId(null); }, onCopy: () => pushToast("success", t("board.toastCopied")) })) : null, _jsx(ToastStack, { toasts: toasts })] }));
654
- }
655
- function createInitialValues(editTarget, currentProjectId) {
656
- if (!editTarget) {
657
- return {
658
- interval: "30m",
659
- taskMode: "inline",
660
- command: "",
661
- cwd: process.cwd(),
662
- taskId: "",
663
- description: "",
664
- runNow: "y",
665
- maxRuns: "",
666
- project: currentProjectId,
667
- };
668
- }
669
- return {
670
- interval: editTarget.intervalHuman ?? "30m",
671
- taskMode: editTarget.taskId ? "existing" : "inline",
672
- command: editTarget.commandRaw ?? [editTarget.command, ...editTarget.commandArgs].join(" "),
673
- cwd: editTarget.cwd ?? "",
674
- taskId: editTarget.taskId ?? "",
675
- description: editTarget.description,
676
- runNow: "y",
677
- maxRuns: editTarget.maxRuns?.toString() ?? "",
678
- project: editTarget.projectId ?? "default",
679
- };
680
- }