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
@@ -0,0 +1,150 @@
1
+ import { useState, useMemo, useEffect, useCallback } from "react";
2
+ import { applyLoopFilters, defaultFilters } from "../../entities/loops/filters.js";
3
+ import { applyProjectFilters, defaultProjectFilters } from "../../entities/projects/filters.js";
4
+ import { POLL_MS } from "../../shared/config/constants.js";
5
+ export function useAppState(loops, pushToast, refresh, loopService, taskService, projectService, logService, view, push, pop) {
6
+ const [activeTab, setActiveTab] = useState("loops");
7
+ const [focusedPanel, setFocusedPanel] = useState("left");
8
+ const [filters, setFilters] = useState(defaultFilters);
9
+ const [sort, setSort] = useState("description");
10
+ const [selectedIndex, setSelectedIndex] = useState(0);
11
+ const [editTarget, setEditTarget] = useState(null);
12
+ const [cloneMode, setCloneMode] = useState(false);
13
+ const [editTask, setEditTask] = useState(null);
14
+ const [editProject, setEditProject] = useState(null);
15
+ const [pendingTaskSelection, setPendingTaskSelection] = useState(null);
16
+ const [selectedRunIndex, setSelectedRunIndex] = useState(0);
17
+ const [logModalRun, setLogModalRun] = useState(null);
18
+ const [logModalLoopId, setLogModalLoopId] = useState(null);
19
+ const [logModalLines, setLogModalLines] = useState([]);
20
+ const [logModalLoading, setLogModalLoading] = useState(false);
21
+ const [tasks, setTasks] = useState([]);
22
+ const [taskSelectedIndex, setTaskSelectedIndex] = useState(0);
23
+ const [taskQuery, setTaskQuery] = useState("");
24
+ const [projects, setProjects] = useState([]);
25
+ const [currentProjectId, setCurrentProjectId] = useState("all");
26
+ const [projectSelectedIndex, setProjectSelectedIndex] = useState(0);
27
+ const [projectFilters, setProjectFilters] = useState(defaultProjectFilters);
28
+ const [commandsBrowserOpen, setCommandsBrowserOpen] = useState(false);
29
+ const [contextHelpOpen, setContextHelpOpen] = useState(false);
30
+ const [exportModal, setExportModal] = useState(null);
31
+ const [confirmState, setConfirmState] = useState(null);
32
+ const [searchState, setSearchState] = useState(null);
33
+ const [searchValue, setSearchValue] = useState("");
34
+ const [debugMode, setDebugMode] = useState(false);
35
+ const [debugEntries, setDebugEntries] = useState([]);
36
+ const [chordState, setChordState] = useState(null);
37
+ const [commandBarHasText, setCommandBarHasText] = useState(false);
38
+ const [commandBarDropdownOpen, setCommandBarDropdownOpen] = useState(false);
39
+ const visible = useMemo(() => applyLoopFilters(currentProjectId === "all" ? loops : loops.filter((l) => (l.projectId ?? "default") === currentProjectId), filters, sort), [loops, filters, sort, currentProjectId]);
40
+ const clampedIndex = Math.min(selectedIndex, Math.max(0, visible.length - 1));
41
+ const selected = visible[clampedIndex] ?? null;
42
+ const selectedId = selected?.id ?? null;
43
+ useEffect(() => { setSelectedRunIndex(0); }, [selected?.id]);
44
+ const filteredTasks = useMemo(() => {
45
+ const sorted = [...tasks].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
46
+ if (!taskQuery)
47
+ return sorted;
48
+ const q = taskQuery.toLowerCase();
49
+ return sorted.filter((t) => `${t.id} ${t.name} ${t.command}`.toLowerCase().includes(q));
50
+ }, [tasks, taskQuery]);
51
+ const taskClampedIndex = Math.min(taskSelectedIndex, Math.max(0, filteredTasks.length - 1));
52
+ const selectedTask = filteredTasks[taskClampedIndex] ?? null;
53
+ const filteredProjects = useMemo(() => applyProjectFilters(projects, loops, projectFilters), [projects, loops, projectFilters]);
54
+ const projectClampedIndex = Math.min(projectSelectedIndex, Math.max(0, filteredProjects.length - 1));
55
+ const selectedProjectEntity = filteredProjects[projectClampedIndex] ?? null;
56
+ const projectLoopCount = selectedProjectEntity
57
+ ? loops.filter((l) => (l.projectId ?? "default") === selectedProjectEntity.id).length
58
+ : 0;
59
+ async function refreshTasks() {
60
+ try {
61
+ setTasks(await taskService.list());
62
+ }
63
+ catch { /* ignore */ }
64
+ }
65
+ async function refreshProjects() {
66
+ try {
67
+ setProjects(await projectService.list());
68
+ }
69
+ catch { /* ignore */ }
70
+ }
71
+ useEffect(() => { void refreshTasks(); void refreshProjects(); }, []);
72
+ useEffect(() => {
73
+ const timer = setInterval(() => { void refreshTasks(); void refreshProjects(); }, POLL_MS);
74
+ return () => clearInterval(timer);
75
+ }, []);
76
+ function runAction(label, action) {
77
+ return async () => {
78
+ try {
79
+ await action();
80
+ void refresh();
81
+ pushToast("success", label);
82
+ }
83
+ catch (error) {
84
+ pushToast("error", error instanceof Error ? error.message : String(error));
85
+ }
86
+ };
87
+ }
88
+ function handleOpenRunLog(run) {
89
+ if (!selectedId)
90
+ return;
91
+ setLogModalRun(run);
92
+ setLogModalLoopId(selectedId);
93
+ if (run.status === "running") {
94
+ setLogModalLoading(false);
95
+ setLogModalLines([]);
96
+ return;
97
+ }
98
+ setLogModalLoading(true);
99
+ setLogModalLines([]);
100
+ logService.fetchRunLog(selectedId, run.runNumber)
101
+ .then((log) => { setLogModalLines(log ? log.split("\n").map((l) => l.replace(/\r$/, "")) : []); setLogModalLoading(false); })
102
+ .catch((err) => { setLogModalLines([]); setLogModalLoading(false); pushToast("error", err.message ?? "Failed to load log"); });
103
+ }
104
+ const leftPanelQuery = searchState?.active ? searchValue : activeTab === "tasks" ? taskQuery : filters.query;
105
+ const handleSearchChange = useCallback((value) => {
106
+ setSearchValue(value);
107
+ if (activeTab === "tasks")
108
+ setTaskQuery(value);
109
+ else
110
+ setFilters((prev) => ({ ...prev, query: value }));
111
+ }, [activeTab]);
112
+ const handleSearchSubmit = useCallback(() => { setSearchState(null); }, []);
113
+ const handleSearchCancel = useCallback(() => {
114
+ setSearchValue("");
115
+ if (activeTab === "tasks")
116
+ setTaskQuery("");
117
+ else
118
+ setFilters((prev) => ({ ...prev, query: "" }));
119
+ setSearchState(null);
120
+ }, [activeTab]);
121
+ const counts = {
122
+ total: loops.length, running: loops.filter((l) => l.status === "running").length,
123
+ waiting: loops.filter((l) => l.status === "waiting").length, paused: loops.filter((l) => l.status === "paused").length,
124
+ idle: loops.filter((l) => l.status === "idle").length,
125
+ };
126
+ const tabCounts = { loops: loops.length, tasks: tasks.length, projects: projects.length };
127
+ return {
128
+ activeTab, setActiveTab, focusedPanel, setFocusedPanel,
129
+ filters, setFilters, sort, setSort, selectedIndex, setSelectedIndex,
130
+ editTarget, setEditTarget, cloneMode, setCloneMode,
131
+ editTask, setEditTask, editProject, setEditProject,
132
+ pendingTaskSelection, setPendingTaskSelection, selectedRunIndex, setSelectedRunIndex,
133
+ logModalRun, setLogModalRun, logModalLoopId, setLogModalLoopId,
134
+ logModalLines, setLogModalLines, logModalLoading, setLogModalLoading,
135
+ tasks, setTasks, taskSelectedIndex, setTaskSelectedIndex, taskQuery, setTaskQuery,
136
+ projects, setProjects, currentProjectId, setCurrentProjectId,
137
+ projectSelectedIndex, setProjectSelectedIndex, projectFilters, setProjectFilters,
138
+ commandsBrowserOpen, setCommandsBrowserOpen, contextHelpOpen, setContextHelpOpen,
139
+ exportModal, setExportModal, confirmState, setConfirmState,
140
+ searchState, setSearchState, searchValue, setSearchValue,
141
+ debugMode, setDebugMode, debugEntries, setDebugEntries,
142
+ chordState, setChordState, commandBarHasText, setCommandBarHasText, commandBarDropdownOpen, setCommandBarDropdownOpen,
143
+ visible, clampedIndex, selected, selectedId,
144
+ filteredTasks, taskClampedIndex, selectedTask,
145
+ filteredProjects, projectClampedIndex, selectedProjectEntity, projectLoopCount,
146
+ refreshTasks, refreshProjects, runAction, handleOpenRunLog,
147
+ leftPanelQuery, handleSearchChange, handleSearchSubmit, handleSearchCancel,
148
+ counts, tabCounts,
149
+ };
150
+ }
package/dist/logger.js CHANGED
@@ -1,5 +1,4 @@
1
1
  export class Logger {
2
- verbose;
3
2
  constructor(verbose = false) {
4
3
  this.verbose = verbose;
5
4
  }
@@ -1,5 +1,5 @@
1
1
  import { parseDuration } from "./duration.js";
2
- import { t } from "./i18n/index.js";
2
+ import { t } from "./shared/i18n/index.js";
3
3
  export function parseMaxRuns(value) {
4
4
  if (value === null || value === undefined || value === "") {
5
5
  return null;
@@ -21,9 +21,14 @@ export function parseCommandLine(input) {
21
21
  if (char === quote) {
22
22
  quote = null;
23
23
  }
24
- else if (char === "\\" && quote === '"' && i + 1 < input.length) {
25
- i += 1;
26
- current += input[i];
24
+ else if (char === "\\" && quote === '"') {
25
+ if (i + 1 < input.length) {
26
+ i += 1;
27
+ current += input[i];
28
+ }
29
+ else {
30
+ current += char;
31
+ }
27
32
  }
28
33
  else {
29
34
  current += char;
@@ -74,7 +74,7 @@ export const CTRL_SHORTCUT_DELETE = "Ctrl+D";
74
74
  export const CODE_EDITOR_MAX_VISIBLE = 2;
75
75
  export const CODE_EDITOR_MODAL_HEIGHT = 40;
76
76
  export const CODE_EDITOR_MODAL_WIDTH = 120;
77
- export const CODE_EDITOR_WRAP_LENGTH = 40;
77
+ export const CODE_EDITOR_WRAP_LENGTH = 80;
78
78
  export const CODE_EDITOR_UNDO_LIMIT = 50;
79
79
  export const CODE_EDITOR_SYNTAX_COLORS = {
80
80
  flag: "#38bdf8",
@@ -0,0 +1,22 @@
1
+ import { Container } from "inversify";
2
+ import { TYPES } from "../services/types.js";
3
+ import { IpcLoopService } from "../services/loop-service.js";
4
+ import { IpcTaskService } from "../services/task-service.js";
5
+ import { IpcProjectService } from "../services/project-service.js";
6
+ import { IpcLogService } from "../services/log-service.js";
7
+ import { IpcExportService } from "../services/export-service.js";
8
+ export function createContainer() {
9
+ const container = new Container();
10
+ container.bind(TYPES.LoopService).to(IpcLoopService);
11
+ container.bind(TYPES.TaskService).to(IpcTaskService);
12
+ container.bind(TYPES.ProjectService).to(IpcProjectService);
13
+ container.bind(TYPES.LogService).to(IpcLogService);
14
+ container.bind(TYPES.ExportService).toDynamicValue(() => {
15
+ const loopService = container.get(TYPES.LoopService);
16
+ const taskService = container.get(TYPES.TaskService);
17
+ const projectService = container.get(TYPES.ProjectService);
18
+ return new IpcExportService(loopService, taskService, projectService);
19
+ });
20
+ return container;
21
+ }
22
+ export const container = createContainer();
@@ -1,5 +1,5 @@
1
1
  import { useStdout } from "ink";
2
- import { BOARD_BREAKPOINT_WIDTH } from "../../config/constants.js";
2
+ import { BOARD_BREAKPOINT_WIDTH } from "../config/constants.js";
3
3
  export function useBreakpoint() {
4
4
  const { stdout } = useStdout();
5
5
  const width = stdout?.columns ?? 80;
@@ -0,0 +1,6 @@
1
+ import { useContext } from "react";
2
+ import { InversifyContext } from "../providers/InversifyProvider.js";
3
+ export function useInject(identifier) {
4
+ const { container } = useContext(InversifyContext);
5
+ return container.get(identifier);
6
+ }
@@ -1,8 +1,10 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
- import { streamLogs } from "../daemon.js";
3
- import { t } from "../../i18n/index.js";
4
- import { LOG_LINES_MAX } from "../../config/constants.js";
2
+ import { useInject } from "./useInject.js";
3
+ import { TYPES } from "../services/types.js";
4
+ import { t } from "../i18n/index.js";
5
+ import { LOG_LINES_MAX } from "../config/constants.js";
5
6
  export function useLogStream(selectedId, view, onError) {
7
+ const logService = useInject(TYPES.LogService);
6
8
  const [logLines, setLogLines] = useState([]);
7
9
  const logSocket = useRef(null);
8
10
  useEffect(() => {
@@ -13,7 +15,7 @@ export function useLogStream(selectedId, view, onError) {
13
15
  return;
14
16
  }
15
17
  setLogLines([t("board.logWaiting")]);
16
- const socket = streamLogs(selectedId, (line) => setLogLines((prev) => {
18
+ const socket = logService.streamLogs(selectedId, (line) => setLogLines((prev) => {
17
19
  const next = prev[0] === t("board.logWaiting") ? [] : prev;
18
20
  return [...next, line].slice(-LOG_LINES_MAX);
19
21
  }), onError);
@@ -24,7 +26,7 @@ export function useLogStream(selectedId, view, onError) {
24
26
  logSocket.current = null;
25
27
  }
26
28
  };
27
- }, [selectedId, view]);
29
+ }, [selectedId, view, logService]);
28
30
  return {
29
31
  logLines,
30
32
  destroy: () => logSocket.current?.destroy(),
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
- import { parseDuration } from "../duration.js";
3
- import { parseMaxRuns } from "../loop-config.js";
2
+ import { parseDuration } from "../../duration.js";
3
+ import { parseMaxRuns } from "../../loop-config.js";
4
4
  import { t } from "../i18n/index.js";
5
5
  export const createFields = [
6
6
  "interval",
@@ -1,13 +1,15 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import { listLoops } from "../daemon.js";
3
- import { POLL_MS } from "../../config/constants.js";
2
+ import { useInject } from "./useInject.js";
3
+ import { TYPES } from "../services/types.js";
4
+ import { POLL_MS } from "../config/constants.js";
4
5
  export function useLoopPolling() {
6
+ const loopService = useInject(TYPES.LoopService);
5
7
  const [loops, setLoops] = useState([]);
6
8
  const [daemonStatus, setDaemonStatus] = useState("starting");
7
9
  const lastSerialized = useRef("");
8
10
  const refresh = useCallback(async () => {
9
11
  try {
10
- const next = await listLoops();
12
+ const next = await loopService.list();
11
13
  const serialized = JSON.stringify(next);
12
14
  if (serialized !== lastSerialized.current) {
13
15
  lastSerialized.current = serialized;
@@ -18,7 +20,7 @@ export function useLoopPolling() {
18
20
  catch {
19
21
  setDaemonStatus("error");
20
22
  }
21
- }, []);
23
+ }, [loopService]);
22
24
  useEffect(() => {
23
25
  void refresh();
24
26
  const timer = setInterval(() => void refresh(), POLL_MS);
@@ -4,13 +4,11 @@ import { useState, useRef, useCallback } from "react";
4
4
  * without needing React DOM or renderHook.
5
5
  */
6
6
  export class UndoRedoStack {
7
- value;
8
- limit;
9
- history = [];
10
- future = [];
11
7
  constructor(value, limit = 50) {
12
8
  this.value = value;
13
9
  this.limit = limit;
10
+ this.history = [];
11
+ this.future = [];
14
12
  }
15
13
  get canUndo() {
16
14
  return this.history.length > 0;
@@ -0,0 +1,6 @@
1
+ import React, { createContext } from "react";
2
+ import { container } from "../container/index.js";
3
+ export const InversifyContext = createContext({ container });
4
+ export function InversifyProvider(props) {
5
+ return React.createElement(InversifyContext.Provider, { value: { container } }, props.children);
6
+ }
@@ -0,0 +1,43 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { injectable } from "inversify";
8
+ import fs from "node:fs/promises";
9
+ import path from "node:path";
10
+ import { getDataDir } from "../config/paths.js";
11
+ let IpcExportService = class IpcExportService {
12
+ constructor(loopService, taskService, projectService) {
13
+ this.loopService = loopService;
14
+ this.taskService = taskService;
15
+ this.projectService = projectService;
16
+ }
17
+ async exportConfig() {
18
+ const [loops, tasks, projects] = await Promise.all([
19
+ this.loopService.list().catch(() => []),
20
+ this.taskService.list().catch(() => []),
21
+ this.projectService.list().catch(() => []),
22
+ ]);
23
+ const exportData = {
24
+ version: 2,
25
+ exportedAt: new Date().toISOString(),
26
+ loops,
27
+ tasks,
28
+ projects,
29
+ };
30
+ const json = JSON.stringify(exportData, null, 2);
31
+ const dataDir = getDataDir();
32
+ const exportsDir = path.join(dataDir, "exports");
33
+ await fs.mkdir(exportsDir, { recursive: true });
34
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
35
+ const filePath = path.join(exportsDir, `loop-export-${ts}.json`);
36
+ await fs.writeFile(filePath, json, "utf-8");
37
+ return { json, filePath };
38
+ }
39
+ };
40
+ IpcExportService = __decorate([
41
+ injectable()
42
+ ], IpcExportService);
43
+ export { IpcExportService };
@@ -0,0 +1,28 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { injectable } from "inversify";
8
+ import { sendRequest, streamRequest } from "../../client/ipc.js";
9
+ import { LOG_TAIL_DEFAULT } from "../config/constants.js";
10
+ let IpcLogService = class IpcLogService {
11
+ async fetchRunLog(id, runNumber) {
12
+ const response = await sendRequest({ type: "run-log", payload: { id, runNumber } });
13
+ if (response.type !== "ok") {
14
+ throw new Error(response.message ?? "Failed to fetch run log");
15
+ }
16
+ return response.data ?? "";
17
+ }
18
+ streamRunLog(id, runNumber, onLine, onEnd, onError) {
19
+ return streamRequest({ type: "run-log-stream", payload: { id, runNumber } }, onLine, onEnd, onError);
20
+ }
21
+ streamLogs(id, onLine, onError) {
22
+ return streamRequest({ type: "logs", payload: { id, follow: true, tail: LOG_TAIL_DEFAULT } }, onLine, () => { }, onError);
23
+ }
24
+ };
25
+ IpcLogService = __decorate([
26
+ injectable()
27
+ ], IpcLogService);
28
+ export { IpcLogService };
@@ -0,0 +1,78 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { injectable } from "inversify";
8
+ import { sendRequest } from "../../client/ipc.js";
9
+ import { t } from "../i18n/index.js";
10
+ let IpcLoopService = class IpcLoopService {
11
+ async list() {
12
+ const response = await sendRequest({ type: "list" });
13
+ if (response.type !== "ok") {
14
+ throw new Error(response.message);
15
+ }
16
+ return response.data;
17
+ }
18
+ async create(options, intervalHuman) {
19
+ const response = await sendRequest({
20
+ type: "start",
21
+ payload: { ...options, intervalHuman },
22
+ });
23
+ if (response.type !== "ok") {
24
+ throw new Error(response.message);
25
+ }
26
+ return response.data.id;
27
+ }
28
+ async update(id, options, intervalHuman) {
29
+ const response = await sendRequest({
30
+ type: "update",
31
+ payload: { id, ...options, intervalHuman },
32
+ });
33
+ if (response.type !== "ok") {
34
+ throw new Error(response.message);
35
+ }
36
+ return response.data.id;
37
+ }
38
+ async pause(id) {
39
+ const response = await sendRequest({ type: "pause", payload: { id } });
40
+ if (response.type !== "ok") {
41
+ throw new Error(response.message ?? t("errors.pauseFailed"));
42
+ }
43
+ }
44
+ async resume(id) {
45
+ const response = await sendRequest({ type: "resume", payload: { id } });
46
+ if (response.type !== "ok") {
47
+ throw new Error(response.message ?? t("errors.resumeFailed"));
48
+ }
49
+ }
50
+ async stop(id) {
51
+ const response = await sendRequest({ type: "stop-loop", payload: { id } });
52
+ if (response.type !== "ok") {
53
+ throw new Error(response.message ?? "Stop failed");
54
+ }
55
+ }
56
+ async play(id) {
57
+ const response = await sendRequest({ type: "play-loop", payload: { id } });
58
+ if (response.type !== "ok") {
59
+ throw new Error(response.message ?? "Play failed");
60
+ }
61
+ }
62
+ async trigger(id) {
63
+ const response = await sendRequest({ type: "trigger", payload: { id } });
64
+ if (response.type !== "ok") {
65
+ throw new Error(response.message ?? t("errors.forceRunFailed"));
66
+ }
67
+ }
68
+ async delete(id) {
69
+ const response = await sendRequest({ type: "delete", payload: { id } });
70
+ if (response.type !== "ok") {
71
+ throw new Error(response.message ?? t("errors.deleteFailed"));
72
+ }
73
+ }
74
+ };
75
+ IpcLoopService = __decorate([
76
+ injectable()
77
+ ], IpcLoopService);
78
+ export { IpcLoopService };
@@ -0,0 +1,39 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { injectable } from "inversify";
8
+ import { sendRequest } from "../../client/ipc.js";
9
+ import { t } from "../i18n/index.js";
10
+ let IpcProjectService = class IpcProjectService {
11
+ async list() {
12
+ const response = await sendRequest({ type: "project-list" });
13
+ if (response.type !== "ok")
14
+ throw new Error(response.message);
15
+ return response.data;
16
+ }
17
+ async create(name, color, directory) {
18
+ const response = await sendRequest({ type: "project-create", payload: { name, color, directory } });
19
+ if (response.type !== "ok")
20
+ throw new Error(response.message);
21
+ return response.data;
22
+ }
23
+ async update(id, name, color, directory) {
24
+ const response = await sendRequest({ type: "project-update", payload: { id, name, color, directory } });
25
+ if (response.type !== "ok") {
26
+ throw new Error(response.message ?? t("project.error.updateFailed"));
27
+ }
28
+ }
29
+ async delete(id) {
30
+ const response = await sendRequest({ type: "project-delete", payload: { id } });
31
+ if (response.type !== "ok") {
32
+ throw new Error(response.message ?? t("project.error.deleteFailed"));
33
+ }
34
+ }
35
+ };
36
+ IpcProjectService = __decorate([
37
+ injectable()
38
+ ], IpcProjectService);
39
+ export { IpcProjectService };
@@ -0,0 +1,41 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { injectable } from "inversify";
8
+ import { sendRequest } from "../../client/ipc.js";
9
+ let IpcTaskService = class IpcTaskService {
10
+ async list() {
11
+ const response = await sendRequest({ type: "task-list" });
12
+ if (response.type !== "ok") {
13
+ throw new Error(response.message);
14
+ }
15
+ return response.data;
16
+ }
17
+ async create(payload) {
18
+ const response = await sendRequest({ type: "task-create", payload });
19
+ if (response.type !== "ok") {
20
+ throw new Error(response.message);
21
+ }
22
+ return response.data;
23
+ }
24
+ async update(id, payload) {
25
+ const response = await sendRequest({ type: "task-update", payload: { id, ...payload } });
26
+ if (response.type !== "ok") {
27
+ throw new Error(response.message);
28
+ }
29
+ return response.data;
30
+ }
31
+ async delete(id) {
32
+ const response = await sendRequest({ type: "task-delete", payload: { id } });
33
+ if (response.type !== "ok") {
34
+ throw new Error(response.message ?? "Task delete failed");
35
+ }
36
+ }
37
+ };
38
+ IpcTaskService = __decorate([
39
+ injectable()
40
+ ], IpcTaskService);
41
+ export { IpcTaskService };
@@ -0,0 +1,7 @@
1
+ export const TYPES = {
2
+ LoopService: Symbol.for("LoopService"),
3
+ TaskService: Symbol.for("TaskService"),
4
+ ProjectService: Symbol.for("ProjectService"),
5
+ LogService: Symbol.for("LogService"),
6
+ ExportService: Symbol.for("ExportService"),
7
+ };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { darkTheme as theme } from "../theme.js";
3
+ import { darkTheme as theme } from "./theme.js";
4
4
  export function Button(props) {
5
5
  const { label, focused, variant = "default" } = props;
6
6
  const bgColor = focused
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { darkTheme as theme } from "../theme.js";
3
+ import { darkTheme as theme } from "./theme.js";
4
4
  const MAX_ENTRIES = 12;
5
5
  export function DebugPanel(props) {
6
6
  return (_jsxs(Box, { flexDirection: "column", width: "22%", flexShrink: 0, borderStyle: "single", borderColor: theme.semantic.warning, backgroundColor: theme.bg.surface, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.semantic.warning, bold: true, children: "DEBUG" }) }), _jsx(Box, { paddingLeft: 1, flexDirection: "column", children: props.entries.length === 0 ? (_jsx(Text, { color: theme.text.muted, children: "press keys..." })) : (props.entries.map((e) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.text.primary, children: ["len=", e.len, " codes=[", e.codes, "]"] }), _jsxs(Text, { color: theme.text.muted, children: ["c=", e.ctrl ? 1 : 0, " r=", e.return ? 1 : 0, " t=", e.tab ? 1 : 0, " e=", e.escape ? 1 : 0] })] }, e.id)))) })] }));
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text, useFocus, useInput } from "ink";
3
- import { darkTheme as theme } from "../theme.js";
3
+ import { darkTheme as theme } from "./theme.js";
4
4
  export function FocusableButton(props) {
5
5
  const { label, color, onPress, variant = "default" } = props;
6
6
  const { isFocused } = useFocus();
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useCallback } from "react";
3
3
  import { Box, Text, useFocus, useInput } from "ink";
4
- import { darkTheme as theme } from "../theme.js";
4
+ import { darkTheme as theme } from "./theme.js";
5
5
  import { sanitizePaste } from "../utils/paste.js";
6
6
  export function FocusableInput(props) {
7
7
  const { value, onChange, onSubmit } = props;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
- import { darkTheme as theme } from "../theme.js";
4
+ import { darkTheme as theme } from "./theme.js";
5
5
  export function FocusableList(props) {
6
6
  const { items, renderItem, onSelect, onActivate, selectedIndex, isFocused, navActive = true, limit, } = props;
7
7
  const n = items.length;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text, useInput } from "ink";
3
- import { darkTheme as theme } from "../theme.js";
3
+ import { darkTheme as theme } from "./theme.js";
4
4
  export function Modal(props) {
5
5
  useInput((_input, key) => {
6
6
  if (key.escape) {
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useMemo, useCallback } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
- import { darkTheme as theme } from "../theme.js";
5
- import { t } from "../../i18n/index.js";
4
+ import { darkTheme as theme } from "./theme.js";
5
+ import { t } from "../i18n/index.js";
6
6
  const MAX_VISIBLE = 10;
7
7
  /**
8
8
  * Filterable modal picker: type to narrow, up/down to navigate, enter to
@@ -1,7 +1,7 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState, useCallback } from "react";
3
3
  import { Box, Text } from "ink";
4
- import { darkTheme as theme } from "../theme.js";
4
+ import { darkTheme as theme } from "./theme.js";
5
5
  const TOAST_TIMEOUT_MS = 3500;
6
6
  const TOAST_MAX = 4;
7
7
  function toastColor(kind) {