@plainconceptsplatform/loop-task 2.6.0

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 (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +534 -0
  3. package/dist/app/App.js +132 -0
  4. package/dist/app/BoardLayout.js +5 -0
  5. package/dist/app/index.js +48 -0
  6. package/dist/app/providers/index.js +1 -0
  7. package/dist/app/router/index.js +16 -0
  8. package/dist/app/types.js +1 -0
  9. package/dist/cli/import-validator.js +152 -0
  10. package/dist/cli/import-writer.js +81 -0
  11. package/dist/cli.js +573 -0
  12. package/dist/client/cli-format.js +7 -0
  13. package/dist/client/commands.js +162 -0
  14. package/dist/client/ipc.js +96 -0
  15. package/dist/client/project-commands.js +164 -0
  16. package/dist/core/command/command-runner.js +300 -0
  17. package/dist/core/command/process-tree.js +73 -0
  18. package/dist/core/command/resolve-cwd.js +8 -0
  19. package/dist/core/command/stdout-capture-transform.js +73 -0
  20. package/dist/core/context/context-parser.js +65 -0
  21. package/dist/core/context/log-context.js +3 -0
  22. package/dist/core/context/template.js +20 -0
  23. package/dist/core/context/validate-context.js +19 -0
  24. package/dist/core/foreground/index.js +81 -0
  25. package/dist/core/logging/bounded-log-reader.js +368 -0
  26. package/dist/core/logging/log-follower.js +26 -0
  27. package/dist/core/logging/log-parser.js +29 -0
  28. package/dist/core/logging/log-rotator.js +34 -0
  29. package/dist/core/logging/rotating-log-stream.js +137 -0
  30. package/dist/core/loop/chain-executor.js +171 -0
  31. package/dist/core/loop/delay-utils.js +65 -0
  32. package/dist/core/loop/loop-controller.js +240 -0
  33. package/dist/core/loop/loop-runner.js +194 -0
  34. package/dist/core/loop/run-executor.js +144 -0
  35. package/dist/core/loop/types.js +1 -0
  36. package/dist/core/scheduling/index.js +16 -0
  37. package/dist/daemon/daemon-log.js +11 -0
  38. package/dist/daemon/diagnostics.js +85 -0
  39. package/dist/daemon/http/helpers.js +81 -0
  40. package/dist/daemon/http/openapi.js +119 -0
  41. package/dist/daemon/http/route-loops.js +311 -0
  42. package/dist/daemon/http/route-misc.js +60 -0
  43. package/dist/daemon/http/route-projects.js +55 -0
  44. package/dist/daemon/http/route-tasks.js +141 -0
  45. package/dist/daemon/http/routes.js +19 -0
  46. package/dist/daemon/http/server.js +82 -0
  47. package/dist/daemon/http/sse.js +36 -0
  48. package/dist/daemon/index.js +189 -0
  49. package/dist/daemon/ipc/send.js +5 -0
  50. package/dist/daemon/managers/loop-entry.js +27 -0
  51. package/dist/daemon/managers/loop-manager.js +346 -0
  52. package/dist/daemon/managers/loop-options.js +17 -0
  53. package/dist/daemon/managers/loop-serialization.js +53 -0
  54. package/dist/daemon/managers/project-manager.js +170 -0
  55. package/dist/daemon/managers/task-manager.js +94 -0
  56. package/dist/daemon/mcp/index.js +2 -0
  57. package/dist/daemon/mcp/openapi-sync.js +50 -0
  58. package/dist/daemon/mcp/server.js +167 -0
  59. package/dist/daemon/mcp/tools.js +382 -0
  60. package/dist/daemon/recipe/deferred-reload.js +32 -0
  61. package/dist/daemon/recipe/diagram-reader.js +26 -0
  62. package/dist/daemon/recipe/file-writer.js +52 -0
  63. package/dist/daemon/recipe/id-remapper.js +46 -0
  64. package/dist/daemon/recipe/runtime-state.js +23 -0
  65. package/dist/daemon/recipe/scanner.js +165 -0
  66. package/dist/daemon/recipe/task-store.js +33 -0
  67. package/dist/daemon/recipe/validator.js +35 -0
  68. package/dist/daemon/server/handlers/diagnostics-handlers.js +7 -0
  69. package/dist/daemon/server/handlers/index.js +101 -0
  70. package/dist/daemon/server/handlers/log-handlers.js +129 -0
  71. package/dist/daemon/server/handlers/loop-handlers.js +64 -0
  72. package/dist/daemon/server/handlers/project-handlers.js +42 -0
  73. package/dist/daemon/server/handlers/settings-handlers.js +8 -0
  74. package/dist/daemon/server/handlers/task-handlers.js +20 -0
  75. package/dist/daemon/server/handlers/telemetry-handlers.js +5 -0
  76. package/dist/daemon/server/index.js +117 -0
  77. package/dist/daemon/settings-manager.js +73 -0
  78. package/dist/daemon/spawner/index.js +204 -0
  79. package/dist/daemon/state/index.js +209 -0
  80. package/dist/daemon/telemetry/agent-integrations/agent-integration.js +1 -0
  81. package/dist/daemon/telemetry/agent-integrations/claude-code-integration.js +55 -0
  82. package/dist/daemon/telemetry/agent-integrations/detect-agent-integration.js +20 -0
  83. package/dist/daemon/telemetry/agent-integrations/index.js +3 -0
  84. package/dist/daemon/telemetry/agent-integrations/opencode-integration.js +72 -0
  85. package/dist/daemon/telemetry/index.js +6 -0
  86. package/dist/daemon/telemetry/noop-telemetry-adapter.js +53 -0
  87. package/dist/daemon/telemetry/open-telemetry-adapter.js +464 -0
  88. package/dist/daemon/telemetry/telemetry-manager.js +147 -0
  89. package/dist/daemon/telemetry/telemetry-redaction.js +67 -0
  90. package/dist/daemon/telemetry/telemetry-types.js +45 -0
  91. package/dist/daemon/telemetry/telemetry.js +1 -0
  92. package/dist/daemon/watcher/index.js +250 -0
  93. package/dist/duration.js +24 -0
  94. package/dist/entities/loops/filters.js +109 -0
  95. package/dist/entities/loops/index.js +1 -0
  96. package/dist/entities/projects/filters.js +69 -0
  97. package/dist/entities/projects/index.js +1 -0
  98. package/dist/entities/tasks/filters.js +41 -0
  99. package/dist/entities/tasks/index.js +1 -0
  100. package/dist/entry.js +16 -0
  101. package/dist/esm-loader.js +39 -0
  102. package/dist/features/chain-editor/ChainEditor.js +116 -0
  103. package/dist/features/chain-editor/mermaidToAscii.js +375 -0
  104. package/dist/features/chain-editor/renderChainDiagram.js +106 -0
  105. package/dist/features/code-editor/CodeEditorModal.js +84 -0
  106. package/dist/features/code-editor/CodeEditorPreview.js +14 -0
  107. package/dist/features/code-editor/useEditorKeyboard.js +150 -0
  108. package/dist/features/code-editor/useModalDimensions.js +9 -0
  109. package/dist/features/commands/commands.js +132 -0
  110. package/dist/features/commands/useCommandHandlers.js +280 -0
  111. package/dist/features/commands/useContextualActions.js +78 -0
  112. package/dist/features/commands/useGlobalShortcuts.js +154 -0
  113. package/dist/features/forms/FormRouter.js +46 -0
  114. package/dist/features/overlays/ContextHelpModal.js +8 -0
  115. package/dist/features/overlays/DiagramModal.js +45 -0
  116. package/dist/features/overlays/ExportModal.js +51 -0
  117. package/dist/features/overlays/HelpGuideModal.js +23 -0
  118. package/dist/features/overlays/HelpModal.js +59 -0
  119. package/dist/features/overlays/OverlayStack.js +10 -0
  120. package/dist/features/overlays/ProjectsModal.js +42 -0
  121. package/dist/features/overlays/TaskPickerModal.js +62 -0
  122. package/dist/features/overlays/WelcomeScreen.js +74 -0
  123. package/dist/features/overlays/useOverlayStack.js +55 -0
  124. package/dist/features/state/useAppState.js +151 -0
  125. package/dist/logger.js +25 -0
  126. package/dist/loop-config.js +152 -0
  127. package/dist/shared/clipboard.js +111 -0
  128. package/dist/shared/config/constants.js +104 -0
  129. package/dist/shared/config/paths.js +67 -0
  130. package/dist/shared/container/index.js +24 -0
  131. package/dist/shared/fs-utils.js +44 -0
  132. package/dist/shared/hooks/useBreakpoint.js +11 -0
  133. package/dist/shared/hooks/useDaemonSettings.js +41 -0
  134. package/dist/shared/hooks/useInject.js +6 -0
  135. package/dist/shared/hooks/useLogStream.js +37 -0
  136. package/dist/shared/hooks/useLoopFormValidation.js +131 -0
  137. package/dist/shared/hooks/useLoopPolling.js +30 -0
  138. package/dist/shared/hooks/useMouseScroll.js +56 -0
  139. package/dist/shared/hooks/useUndoRedo.js +77 -0
  140. package/dist/shared/i18n/en.json +709 -0
  141. package/dist/shared/i18n/index.js +11 -0
  142. package/dist/shared/providers/InversifyProvider.js +6 -0
  143. package/dist/shared/services/export-service.js +43 -0
  144. package/dist/shared/services/log-service.js +28 -0
  145. package/dist/shared/services/loop-service.js +78 -0
  146. package/dist/shared/services/project-service.js +39 -0
  147. package/dist/shared/services/settings-service.js +102 -0
  148. package/dist/shared/services/task-service.js +41 -0
  149. package/dist/shared/services/types.js +8 -0
  150. package/dist/shared/sleep.js +17 -0
  151. package/dist/shared/tail.js +4 -0
  152. package/dist/shared/ui/Button.js +18 -0
  153. package/dist/shared/ui/DebugPanel.js +9 -0
  154. package/dist/shared/ui/FocusableButton.js +18 -0
  155. package/dist/shared/ui/FocusableInput.js +72 -0
  156. package/dist/shared/ui/FocusableList.js +48 -0
  157. package/dist/shared/ui/Modal.js +13 -0
  158. package/dist/shared/ui/SelectModal.js +78 -0
  159. package/dist/shared/ui/Toast.js +37 -0
  160. package/dist/shared/ui/format.js +117 -0
  161. package/dist/shared/ui/hooks/useHoverState.js +11 -0
  162. package/dist/shared/ui/state.js +9 -0
  163. package/dist/shared/ui/theme.js +96 -0
  164. package/dist/shared/utils/log-lines.js +13 -0
  165. package/dist/shared/utils/paste.js +37 -0
  166. package/dist/shared/utils/syntax.js +128 -0
  167. package/dist/shared/utils/validation.js +56 -0
  168. package/dist/types.js +1 -0
  169. package/dist/visual-evidence/capture.js +51 -0
  170. package/dist/visual-evidence/cli.js +85 -0
  171. package/dist/visual-evidence/evidence-required.js +71 -0
  172. package/dist/visual-evidence/index.js +10 -0
  173. package/dist/visual-evidence/launch.js +61 -0
  174. package/dist/visual-evidence/manifest.js +32 -0
  175. package/dist/visual-evidence/openspec-resolver.js +45 -0
  176. package/dist/visual-evidence/publish.js +126 -0
  177. package/dist/visual-evidence/run.js +121 -0
  178. package/dist/visual-evidence/scenario-registry.js +132 -0
  179. package/dist/widgets/command-input/CommandDropdown.js +69 -0
  180. package/dist/widgets/command-input/CommandInput.js +133 -0
  181. package/dist/widgets/command-input/HintBar.js +9 -0
  182. package/dist/widgets/command-input/InputModes.js +72 -0
  183. package/dist/widgets/commands-browser/CommandsBrowserModal.js +85 -0
  184. package/dist/widgets/header/Header.js +54 -0
  185. package/dist/widgets/header/TabBar.js +20 -0
  186. package/dist/widgets/left-panel/LeftPanel.js +34 -0
  187. package/dist/widgets/left-panel/Navigator.js +107 -0
  188. package/dist/widgets/left-panel/ProjectsPage.js +76 -0
  189. package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
  190. package/dist/widgets/left-panel/TaskBrowser.js +132 -0
  191. package/dist/widgets/log-modal/LogModal.js +211 -0
  192. package/dist/widgets/loop-form/CreateForm.js +82 -0
  193. package/dist/widgets/loop-form/TextField.js +9 -0
  194. package/dist/widgets/loop-form/WizardForm.js +199 -0
  195. package/dist/widgets/loop-form/useCreateSteps.js +135 -0
  196. package/dist/widgets/loop-form/useHandleComplete.js +94 -0
  197. package/dist/widgets/project-form/ProjectForm.js +83 -0
  198. package/dist/widgets/right-panel/Inspector.js +41 -0
  199. package/dist/widgets/right-panel/RightPanel.js +38 -0
  200. package/dist/widgets/right-panel/RunHistory.js +187 -0
  201. package/dist/widgets/task-form/TaskForm.js +272 -0
  202. package/package.json +119 -0
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { darkTheme as theme } from "../../shared/ui/theme.js";
5
+ import { t } from "../../shared/i18n/index.js";
6
+ import { EXPORT_MAX_PREVIEW_LINES } from "../../shared/config/constants.js";
7
+ import { copyToClipboard } from "../../shared/clipboard.js";
8
+ import { useMouseScroll } from "../../shared/hooks/useMouseScroll.js";
9
+ const VISIBLE_LINES = 20;
10
+ export function ExportModal(props) {
11
+ const lines = props.json.split("\n");
12
+ const totalLines = lines.length;
13
+ const truncated = totalLines > EXPORT_MAX_PREVIEW_LINES;
14
+ const displayLines = truncated ? lines.slice(0, EXPORT_MAX_PREVIEW_LINES) : lines;
15
+ const maxScroll = Math.max(0, displayLines.length - VISIBLE_LINES);
16
+ const [scrollOffset, setScrollOffset] = useState(0);
17
+ useInput((input, key) => {
18
+ // Bracketed paste: content wrapped in ESC[200~ ... ESC[201~. Must come
19
+ // before the escape check the leading ESC trips key.escape and would
20
+ // close the modal on a right-click paste.
21
+ if (input.includes("\x1b[200~")) {
22
+ return;
23
+ }
24
+ if (input.includes("[<")) {
25
+ return;
26
+ }
27
+ if (key.escape) {
28
+ props.onClose();
29
+ return;
30
+ }
31
+ if (key.downArrow) {
32
+ setScrollOffset((o) => Math.min(o + 1, maxScroll));
33
+ return;
34
+ }
35
+ if (key.upArrow) {
36
+ setScrollOffset((o) => Math.max(0, o - 1));
37
+ return;
38
+ }
39
+ if (input === "c" && !key.ctrl) {
40
+ copyToClipboard(props.json);
41
+ props.onCopy?.();
42
+ return;
43
+ }
44
+ });
45
+ useMouseScroll({
46
+ onScrollUp: () => setScrollOffset((o) => Math.max(0, o - 1)),
47
+ onScrollDown: () => setScrollOffset((o) => Math.min(o + 1, maxScroll)),
48
+ });
49
+ const visible = displayLines.slice(scrollOffset, scrollOffset + VISIBLE_LINES);
50
+ return (_jsxs(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", flexDirection: "column", backgroundColor: theme.bg.elevated, borderStyle: "round", borderColor: theme.accent.brand, paddingX: 2, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.accent.brand, bold: true, children: t("export.modalTitle") }) }), props.error ? (_jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: theme.semantic.danger, children: t("export.error", { message: props.error }) }) })) : (_jsxs(_Fragment, { children: [props.filePath ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.accent.brand, children: t("export.filePath", { path: props.filePath }) }) })) : null, _jsxs(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: [visible.map((line, i) => (_jsx(Text, { color: theme.text.primary, wrap: "truncate", children: line }, scrollOffset + i))), truncated ? (_jsx(Text, { color: theme.text.muted, children: t("export.truncated", { total: String(totalLines) }) })) : null] })] })), _jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [_jsx(Text, { color: theme.text.muted, children: t("export.hint") }), _jsxs(Text, { color: theme.text.muted, children: ["[", scrollOffset, "-", scrollOffset + visible.length, "/", totalLines, "]"] })] })] }));
51
+ }
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { darkTheme as theme } from "../../shared/ui/theme.js";
4
+ import { Modal } from "../../shared/ui/Modal.js";
5
+ import { t } from "../../shared/i18n/index.js";
6
+ export function HelpGuideModal(props) {
7
+ useInput((_input, key) => {
8
+ if (_input.includes("[<"))
9
+ return;
10
+ if (key.escape || _input === "q") {
11
+ props.onClose();
12
+ }
13
+ });
14
+ const sections = [
15
+ { title: "", body: t("helpGuide.intro") },
16
+ { title: t("helpGuide.commands"), body: t("helpGuide.examples") },
17
+ { title: t("helpGuide.allCommands"), body: t("helpGuide.allCommands") },
18
+ { title: t("helpGuide.confirm"), body: t("helpGuide.confirm") },
19
+ { title: t("helpGuide.navigation"), body: t("helpGuide.navigation") },
20
+ { title: t("helpGuide.ctrl"), body: t("helpGuide.ctrl") },
21
+ ];
22
+ return (_jsxs(Modal, { title: t("helpGuide.title"), onClose: props.onClose, width: "70%", children: [sections.map((section, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [section.title.length > 0 ? (_jsx(Text, { color: theme.accent.brand, bold: true, children: section.title })) : null, _jsx(Text, { color: theme.text.primary, children: section.body })] }, i))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.text.muted, children: t("helpGuide.esc") }) })] }));
23
+ }
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { darkTheme as theme } from "../../shared/ui/theme.js";
4
+ import { Modal } from "../../shared/ui/Modal.js";
5
+ import { t } from "../../shared/i18n/index.js";
6
+ function helpEntries(view) {
7
+ switch (view) {
8
+ case "board":
9
+ return [
10
+ { key: t("board.helpKeyMove"), desc: t("board.helpMoveSelection") },
11
+ { key: t("board.helpKeyEnter"), desc: t("board.helpToggleDetail") },
12
+ { key: t("board.helpKeyN"), desc: t("board.helpCreate") },
13
+ { key: t("board.helpKeyE"), desc: t("board.helpEdit") },
14
+ { key: t("board.helpKeyP"), desc: t("board.helpPauseResume") },
15
+ { key: t("board.helpKeyX"), desc: t("board.helpForceRun") },
16
+ { key: t("board.helpKeyD"), desc: t("board.helpDelete") },
17
+ { key: t("board.helpKeySlash"), desc: t("board.helpSearch") },
18
+ { key: t("board.helpKeyF"), desc: t("board.helpCycleFilter") },
19
+ { key: t("board.helpKeyS"), desc: t("board.helpCycleSort") },
20
+ { key: t("board.helpKeyLeftRight"), desc: t("board.helpSwitchPanel") },
21
+ { key: t("board.helpKeyH"), desc: t("board.helpToggleHelp") },
22
+ { key: t("board.helpKeyEsc"), desc: t("board.helpBack") },
23
+ { key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
24
+ ];
25
+ case "tasks":
26
+ return [
27
+ { key: t("board.helpKeyMove"), desc: t("board.helpMoveSelection") },
28
+ { key: t("board.helpKeyEnter"), desc: t("board.taskActionSelect") },
29
+ { key: t("board.helpKeyN"), desc: t("board.helpCreateTask") },
30
+ { key: t("board.helpKeyE"), desc: t("board.taskActionEdit") },
31
+ { key: t("board.helpKeyD"), desc: t("board.actionDelete") },
32
+ { key: t("board.helpKeyH"), desc: t("board.helpToggleHelp") },
33
+ { key: t("board.helpKeyEsc"), desc: t("board.helpBack") },
34
+ { key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
35
+ ];
36
+ case "projects":
37
+ return [
38
+ { key: t("board.helpKeyMove"), desc: t("board.helpMoveSelection") },
39
+ { key: t("board.helpKeyEnter"), desc: t("project.editProjectLabel") },
40
+ { key: t("board.helpKeyN"), desc: t("project.keyNewHint") },
41
+ { key: t("board.helpKeyE"), desc: t("project.keyEditHint") },
42
+ { key: t("board.helpKeyD"), desc: t("project.keyDeleteHint") },
43
+ { key: t("board.helpKeyH"), desc: t("board.helpToggleHelp") },
44
+ { key: t("board.helpKeyEsc"), desc: t("board.helpBack") },
45
+ { key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
46
+ ];
47
+ default:
48
+ return [
49
+ { key: t("board.hintKeyTab"), desc: t("board.hintNextField") },
50
+ { key: t("board.helpKeyEnter"), desc: t("board.hintApply") },
51
+ { key: t("board.helpKeyEsc"), desc: t("board.hintCancel") },
52
+ { key: t("board.helpKeyQ"), desc: t("board.helpQuit") },
53
+ ];
54
+ }
55
+ }
56
+ export function HelpModal(props) {
57
+ const entries = helpEntries(props.view);
58
+ return (_jsx(Modal, { title: "Shortcuts", onClose: props.onClose, width: "60%", children: entries.map((entry, i) => (_jsxs(Box, { children: [_jsx(Text, { color: theme.accent.brand, children: entry.key.padEnd(16) }), _jsx(Text, { color: theme.text.primary, children: entry.desc })] }, i))) }));
59
+ }
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { LogModal } from "../../widgets/log-modal/LogModal.js";
3
+ import { CommandsBrowserModal } from "../../widgets/commands-browser/CommandsBrowserModal.js";
4
+ import { ContextHelpModal } from "./ContextHelpModal.js";
5
+ import { ExportModal } from "./ExportModal.js";
6
+ import { DiagramModal } from "./DiagramModal.js";
7
+ import { ToastStack } from "../../shared/ui/Toast.js";
8
+ export function OverlayStack(props) {
9
+ return (_jsxs(_Fragment, { children: [props.commandsBrowserOpen ? (_jsx(CommandsBrowserModal, { context: props.commandContext, onClose: props.onCommandsBrowserClose, onExecute: props.onCommandsBrowserExecute })) : null, props.contextHelpOpen ? _jsx(ContextHelpModal, { onClose: props.onContextHelpClose }) : null, props.exportModal ? (_jsx(ExportModal, { json: props.exportModal.json, filePath: props.exportModal.filePath, error: props.exportModal.error, onClose: props.onExportModalClose, onCopy: props.onExportCopy })) : null, props.diagramModal ? (_jsx(DiagramModal, { diagramText: props.diagramModal, onClose: props.onDiagramModalClose })) : null, props.logModalRun ? (_jsx(LogModal, { loopId: props.logModalLoopId, run: props.logModalRun, logLines: props.logModalLines, loading: props.logModalLoading, onClose: props.onLogModalClose, onCopy: props.onLogCopy })) : null, _jsx(ToastStack, { toasts: props.toasts })] }));
10
+ }
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { darkTheme as theme } from "../../shared/ui/theme.js";
5
+ import { t } from "../../shared/i18n/index.js";
6
+ export function ProjectsModal(props) {
7
+ const { projects, loops, currentProjectId, onSelect, onClose } = props;
8
+ const [selectedIndex, setSelectedIndex] = React.useState(() => {
9
+ const allIdx = projects.findIndex((p) => p.id === "all");
10
+ if (allIdx >= 0)
11
+ return allIdx;
12
+ const currentIdx = projects.findIndex((p) => p.id === currentProjectId);
13
+ return currentIdx >= 0 ? currentIdx : 0;
14
+ });
15
+ useInput((input, key) => {
16
+ if (input.includes("[<"))
17
+ return;
18
+ if (key.upArrow) {
19
+ setSelectedIndex((i) => i <= 0 ? projects.length - 1 : i - 1);
20
+ }
21
+ else if (key.downArrow) {
22
+ setSelectedIndex((i) => i >= projects.length - 1 ? 0 : i + 1);
23
+ }
24
+ else if (key.return) {
25
+ const project = projects[selectedIndex];
26
+ if (project)
27
+ onSelect(project.id);
28
+ }
29
+ else if (key.escape) {
30
+ onClose();
31
+ }
32
+ });
33
+ const loopCount = (pid) => loops.filter((l) => (l.projectId ?? "default") === pid).length;
34
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", children: _jsxs(Box, { borderStyle: "round", borderColor: theme.accent.brand, backgroundColor: theme.bg.elevated, flexDirection: "column", paddingX: 2, paddingY: 1, minWidth: 40, maxWidth: 60, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: t("project.filterTitle") }), projects.map((project, i) => {
35
+ const isSelected = i === selectedIndex;
36
+ const isActive = project.id === currentProjectId;
37
+ const prefix = isSelected ? "\u203a " : " ";
38
+ const bg = isSelected ? theme.bg.active : undefined;
39
+ const fg = isSelected ? theme.text.inverse : isActive ? theme.accent.brand : theme.text.primary;
40
+ return (_jsx(Box, { backgroundColor: bg, children: _jsxs(Text, { color: fg, children: [prefix, _jsx(Text, { color: project.color ?? theme.text.primary, children: "\u25cf " }), project.name, " ", _jsxs(Text, { color: theme.text.muted, children: ["(", loopCount(project.id), ")"] })] }) }, project.id));
41
+ }), _jsxs(Text, { color: theme.text.muted, children: ["\n", t("board.logModalEscClose")] })] }) }));
42
+ }
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useMemo, useCallback } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { darkTheme as theme } from "../../shared/ui/theme.js";
5
+ import { t } from "../../shared/i18n/index.js";
6
+ const MAX_VISIBLE = 10;
7
+ export function TaskPickerModal(props) {
8
+ const [query, setQuery] = useState("");
9
+ const [cursor, setCursor] = useState(0);
10
+ const filtered = useMemo(() => {
11
+ const q = query.trim().toLowerCase();
12
+ if (!q)
13
+ return props.tasks;
14
+ return props.tasks.filter((task) => task.name.toLowerCase().includes(q) ||
15
+ task.command.toLowerCase().includes(q));
16
+ }, [props.tasks, query]);
17
+ const clampedCursor = filtered.length > 0 ? cursor % filtered.length : 0;
18
+ const selectAt = useCallback((idx) => {
19
+ const task = filtered[idx];
20
+ if (task)
21
+ props.onSelect(task);
22
+ }, [filtered, props]);
23
+ useInput((input, key) => {
24
+ if (input.includes("[<"))
25
+ return;
26
+ if (key.escape) {
27
+ props.onClose();
28
+ return;
29
+ }
30
+ if (key.return) {
31
+ selectAt(clampedCursor);
32
+ return;
33
+ }
34
+ if (key.upArrow) {
35
+ setCursor((prev) => prev <= 0 ? Math.max(0, filtered.length - 1) : prev - 1);
36
+ return;
37
+ }
38
+ if (key.downArrow) {
39
+ setCursor((prev) => prev >= filtered.length - 1 ? 0 : prev + 1);
40
+ return;
41
+ }
42
+ if (key.backspace || key.delete) {
43
+ setQuery((prev) => prev.slice(0, -1));
44
+ setCursor(0);
45
+ return;
46
+ }
47
+ if (key.ctrl)
48
+ return;
49
+ if (input.length === 1 && input >= " " && input <= "~") {
50
+ setQuery((prev) => prev + input);
51
+ setCursor(0);
52
+ }
53
+ });
54
+ const start = Math.max(0, clampedCursor - Math.floor(MAX_VISIBLE / 2));
55
+ const end = Math.min(filtered.length, start + MAX_VISIBLE);
56
+ const visible = filtered.slice(start, end);
57
+ return (_jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: _jsxs(Box, { width: 60, height: Math.min(20, visible.length + 8), flexDirection: "column", backgroundColor: theme.bg.elevated, borderStyle: "round", borderColor: theme.accent.task, paddingX: 1, paddingY: 0, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: theme.accent.task, bold: true, children: t("taskPicker.title") }), _jsx(Text, { color: theme.text.muted, children: "esc" })] }), _jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: theme.text.muted, children: "\u203a " }), _jsx(Text, { color: query ? theme.text.primary : theme.text.muted, children: query || t("taskPicker.searchPlaceholder") }), _jsx(Text, { inverse: true, children: " " })] }), _jsx(Box, { flexDirection: "column", marginTop: 0, children: visible.length === 0 ? (_jsx(Text, { color: theme.text.muted, children: t("taskPicker.empty") })) : (visible.map((task, visIdx) => {
58
+ const realIdx = start + visIdx;
59
+ const isSelected = realIdx === clampedCursor;
60
+ return (_jsxs(Box, { backgroundColor: isSelected ? theme.bg.active : undefined, children: [_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.muted, children: isSelected ? "\u276f " : " " }), _jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.primary, children: task.name }), _jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.muted, children: " " + task.command })] }, task.id));
61
+ })) }), _jsxs(Box, { marginTop: 1, justifyContent: "space-between", children: [_jsxs(Text, { color: theme.text.muted, children: [filtered.length, "/", props.tasks.length] }), _jsx(Text, { color: theme.text.muted, children: t("taskPicker.hint") })] })] }) }));
62
+ }
@@ -0,0 +1,74 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import { darkTheme as theme } from "../../shared/ui/theme.js";
5
+ import { useInject } from "../../shared/hooks/useInject.js";
6
+ import { TYPES } from "../../shared/services/types.js";
7
+ import { parseDuration } from "../../duration.js";
8
+ const EXAMPLES = [
9
+ { label: "Run tests every 30m", interval: "30m", command: "npm", args: ["test"] },
10
+ { label: "Watch for GitHub issues every 5m", interval: "5m", command: "gh", args: ["issue", "list"] },
11
+ { label: "Sync deps every hour", interval: "1h", command: "npm", args: ["outdated"] },
12
+ ];
13
+ export function WelcomeScreen(props) {
14
+ const loopService = useInject(TYPES.LoopService);
15
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
16
+ const [creating, setCreating] = React.useState(null);
17
+ const items = [...EXAMPLES.map((e) => e.label), "Create empty loop"];
18
+ useInput((_input, key) => {
19
+ if (_input.includes("[<"))
20
+ return;
21
+ if (creating)
22
+ return;
23
+ if (key.upArrow) {
24
+ setSelectedIndex((i) => i <= 0 ? items.length - 1 : i - 1);
25
+ return;
26
+ }
27
+ if (key.downArrow) {
28
+ setSelectedIndex((i) => i >= items.length - 1 ? 0 : i + 1);
29
+ return;
30
+ }
31
+ if (key.return) {
32
+ if (selectedIndex === EXAMPLES.length) {
33
+ props.onCreateEmpty();
34
+ return;
35
+ }
36
+ const example = EXAMPLES[selectedIndex];
37
+ if (example) {
38
+ setCreating(example.label);
39
+ const options = {
40
+ interval: parseDuration(example.interval),
41
+ taskId: null,
42
+ command: example.command,
43
+ commandArgs: example.args,
44
+ cwd: "",
45
+ immediate: false,
46
+ maxRuns: null,
47
+ verbose: false,
48
+ description: example.label,
49
+ projectId: "default",
50
+ offset: null,
51
+ };
52
+ loopService.create(options, example.interval)
53
+ .then(() => props.onRefresh())
54
+ .then(() => props.onCreateLoop())
55
+ .catch(() => setCreating(null));
56
+ }
57
+ return;
58
+ }
59
+ if (_input === "n") {
60
+ props.onCreateEmpty();
61
+ return;
62
+ }
63
+ if (_input === "h") {
64
+ return;
65
+ }
66
+ });
67
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsxs(Box, { borderStyle: "round", borderColor: theme.accent.brand, backgroundColor: theme.bg.surface, paddingX: 4, paddingY: 1, flexDirection: "column", children: [_jsxs(Text, { color: theme.accent.brand, bold: true, children: ["\n", "Welcome to loop-task!", "\n"] }), _jsx(Text, { color: theme.text.secondary, children: "Loop engineering: define a purpose," }), _jsxs(Text, { color: theme.text.secondary, children: ["give it an interval, let it iterate.", "\n"] }), _jsxs(Text, { color: theme.text.muted, children: ["No loops yet. Here are some examples:", "\n"] }), items.map((label, i) => {
68
+ const isSelected = i === selectedIndex;
69
+ const bg = isSelected ? theme.bg.active : undefined;
70
+ const fg = isSelected ? theme.text.inverse : theme.text.primary;
71
+ const prefix = isSelected ? "\u203a " : " ";
72
+ return (_jsx(Box, { backgroundColor: bg, children: _jsxs(Text, { color: fg, children: [prefix, label] }) }, label));
73
+ }), _jsxs(Text, { color: theme.text.muted, children: ["\n", "or press 'n' to create a custom loop"] }), _jsx(Text, { color: theme.text.muted, children: "press 'h' for keyboard shortcuts (from main view)" }), creating ? (_jsxs(Text, { color: theme.accent.brand, children: ["\n", "Creating: ", creating, "..."] })) : null] }) }));
74
+ }
@@ -0,0 +1,55 @@
1
+ import { useMemo } from "react";
2
+ import { t } from "../../shared/i18n/index.js";
3
+ import { resolveInputOwner } from "../../shared/ui/state.js";
4
+ export function useOverlayStack(context) {
5
+ const { confirmState, setConfirmState, searchState, setSearchState, setSearchValue, logModalRun, setLogModalRun, logModalLoopId: _logModalLoopId, setLogModalLoopId, commandsBrowserOpen, setCommandsBrowserOpen, exportModal, setExportModal, diagramModal, setDiagramModal, contextHelpOpen, setContextHelpOpen, view, pop, onQuit, exit, commandBarHasText, commandBarDropdownOpen, } = context;
6
+ const popLayer = () => {
7
+ if (confirmState) {
8
+ setConfirmState(null);
9
+ return true;
10
+ }
11
+ if (searchState?.active) {
12
+ setSearchState(null);
13
+ setSearchValue("");
14
+ return true;
15
+ }
16
+ if (logModalRun) {
17
+ setLogModalRun(null);
18
+ setLogModalLoopId(null);
19
+ return true;
20
+ }
21
+ if (commandsBrowserOpen) {
22
+ setCommandsBrowserOpen(false);
23
+ return true;
24
+ }
25
+ if (exportModal) {
26
+ setExportModal(null);
27
+ return true;
28
+ }
29
+ if (diagramModal) {
30
+ setDiagramModal(null);
31
+ return true;
32
+ }
33
+ if (contextHelpOpen) {
34
+ setContextHelpOpen(false);
35
+ return true;
36
+ }
37
+ if (view !== "board") {
38
+ pop();
39
+ return true;
40
+ }
41
+ setConfirmState({
42
+ prompt: t("confirm.quit"),
43
+ onConfirm: () => { onQuit(); exit(); },
44
+ });
45
+ return true;
46
+ };
47
+ const anyModalOpen = !!(logModalRun || commandsBrowserOpen || exportModal || diagramModal);
48
+ const commandInputDisabled = anyModalOpen;
49
+ const inputOwner = useMemo(() => resolveInputOwner({
50
+ modalOpen: !!(logModalRun || commandsBrowserOpen || exportModal || diagramModal || contextHelpOpen || confirmState || searchState?.active),
51
+ commandBarHasText,
52
+ commandBarDropdownOpen,
53
+ }), [logModalRun, commandsBrowserOpen, exportModal, diagramModal, contextHelpOpen, confirmState, searchState?.active, commandBarHasText, commandBarDropdownOpen]);
54
+ return { popLayer, anyModalOpen, commandInputDisabled, inputOwner };
55
+ }
@@ -0,0 +1,151 @@
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 [diagramModal, setDiagramModal] = useState(null);
32
+ const [confirmState, setConfirmState] = useState(null);
33
+ const [searchState, setSearchState] = useState(null);
34
+ const [searchValue, setSearchValue] = useState("");
35
+ const [debugMode, setDebugMode] = useState(false);
36
+ const [debugEntries, setDebugEntries] = useState([]);
37
+ const [chordState, setChordState] = useState(null);
38
+ const [commandBarHasText, setCommandBarHasText] = useState(false);
39
+ const [commandBarDropdownOpen, setCommandBarDropdownOpen] = useState(false);
40
+ const visible = useMemo(() => applyLoopFilters(currentProjectId === "all" ? loops : loops.filter((l) => (l.projectId ?? "default") === currentProjectId), filters, sort, projects), [loops, filters, sort, currentProjectId, projects]);
41
+ const clampedIndex = Math.min(selectedIndex, Math.max(0, visible.length - 1));
42
+ const selected = visible[clampedIndex] ?? null;
43
+ const selectedId = selected?.id ?? null;
44
+ useEffect(() => { setSelectedRunIndex(0); }, [selected?.id]);
45
+ const filteredTasks = useMemo(() => {
46
+ const sorted = [...tasks].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
47
+ if (!taskQuery)
48
+ return sorted;
49
+ const q = taskQuery.toLowerCase();
50
+ return sorted.filter((t) => `${t.id} ${t.name} ${t.command}`.toLowerCase().includes(q));
51
+ }, [tasks, taskQuery]);
52
+ const taskClampedIndex = Math.min(taskSelectedIndex, Math.max(0, filteredTasks.length - 1));
53
+ const selectedTask = filteredTasks[taskClampedIndex] ?? null;
54
+ const filteredProjects = useMemo(() => applyProjectFilters(projects, loops, projectFilters), [projects, loops, projectFilters]);
55
+ const projectClampedIndex = Math.min(projectSelectedIndex, Math.max(0, filteredProjects.length - 1));
56
+ const selectedProjectEntity = filteredProjects[projectClampedIndex] ?? null;
57
+ const projectLoopCount = selectedProjectEntity
58
+ ? loops.filter((l) => (l.projectId ?? "default") === selectedProjectEntity.id).length
59
+ : 0;
60
+ async function refreshTasks() {
61
+ try {
62
+ setTasks(await taskService.list());
63
+ }
64
+ catch { /* ignore */ }
65
+ }
66
+ async function refreshProjects() {
67
+ try {
68
+ setProjects(await projectService.list());
69
+ }
70
+ catch { /* ignore */ }
71
+ }
72
+ useEffect(() => { void refreshTasks(); void refreshProjects(); }, []);
73
+ useEffect(() => {
74
+ const timer = setInterval(() => { void refreshTasks(); void refreshProjects(); }, POLL_MS);
75
+ return () => clearInterval(timer);
76
+ }, []);
77
+ function runAction(label, action) {
78
+ return async () => {
79
+ try {
80
+ await action();
81
+ void refresh();
82
+ pushToast("success", label);
83
+ }
84
+ catch (error) {
85
+ pushToast("error", error instanceof Error ? error.message : String(error));
86
+ }
87
+ };
88
+ }
89
+ function handleOpenRunLog(run) {
90
+ if (!selectedId)
91
+ return;
92
+ setLogModalRun(run);
93
+ setLogModalLoopId(selectedId);
94
+ if (run.status === "running") {
95
+ setLogModalLoading(false);
96
+ setLogModalLines([]);
97
+ return;
98
+ }
99
+ setLogModalLoading(true);
100
+ setLogModalLines([]);
101
+ logService.fetchRunLog(selectedId, run.runNumber)
102
+ .then((log) => { setLogModalLines(log ? log.split("\n").map((l) => l.replace(/\r$/, "")) : []); setLogModalLoading(false); })
103
+ .catch((err) => { setLogModalLines([]); setLogModalLoading(false); pushToast("error", err.message ?? "Failed to load log"); });
104
+ }
105
+ const leftPanelQuery = searchState?.active ? searchValue : activeTab === "tasks" ? taskQuery : filters.query;
106
+ const handleSearchChange = useCallback((value) => {
107
+ setSearchValue(value);
108
+ if (activeTab === "tasks")
109
+ setTaskQuery(value);
110
+ else
111
+ setFilters((prev) => ({ ...prev, query: value }));
112
+ }, [activeTab]);
113
+ const handleSearchSubmit = useCallback(() => { setSearchState(null); }, []);
114
+ const handleSearchCancel = useCallback(() => {
115
+ setSearchValue("");
116
+ if (activeTab === "tasks")
117
+ setTaskQuery("");
118
+ else
119
+ setFilters((prev) => ({ ...prev, query: "" }));
120
+ setSearchState(null);
121
+ }, [activeTab]);
122
+ const counts = {
123
+ total: loops.length, running: loops.filter((l) => l.status === "running").length,
124
+ waiting: loops.filter((l) => l.status === "waiting").length, paused: loops.filter((l) => l.status === "paused").length,
125
+ idle: loops.filter((l) => l.status === "idle").length,
126
+ };
127
+ const tabCounts = { loops: loops.length, tasks: tasks.length, projects: projects.length };
128
+ return {
129
+ activeTab, setActiveTab, focusedPanel, setFocusedPanel,
130
+ filters, setFilters, sort, setSort, selectedIndex, setSelectedIndex,
131
+ editTarget, setEditTarget, cloneMode, setCloneMode,
132
+ editTask, setEditTask, editProject, setEditProject,
133
+ pendingTaskSelection, setPendingTaskSelection, selectedRunIndex, setSelectedRunIndex,
134
+ logModalRun, setLogModalRun, logModalLoopId, setLogModalLoopId,
135
+ logModalLines, setLogModalLines, logModalLoading, setLogModalLoading,
136
+ tasks, setTasks, taskSelectedIndex, setTaskSelectedIndex, taskQuery, setTaskQuery,
137
+ projects, setProjects, currentProjectId, setCurrentProjectId,
138
+ projectSelectedIndex, setProjectSelectedIndex, projectFilters, setProjectFilters,
139
+ commandsBrowserOpen, setCommandsBrowserOpen, contextHelpOpen, setContextHelpOpen,
140
+ exportModal, setExportModal, diagramModal, setDiagramModal, confirmState, setConfirmState,
141
+ searchState, setSearchState, searchValue, setSearchValue,
142
+ debugMode, setDebugMode, debugEntries, setDebugEntries,
143
+ chordState, setChordState, commandBarHasText, setCommandBarHasText, commandBarDropdownOpen, setCommandBarDropdownOpen,
144
+ visible, clampedIndex, selected, selectedId,
145
+ filteredTasks, taskClampedIndex, selectedTask,
146
+ filteredProjects, projectClampedIndex, selectedProjectEntity, projectLoopCount,
147
+ refreshTasks, refreshProjects, runAction, handleOpenRunLog,
148
+ leftPanelQuery, handleSearchChange, handleSearchSubmit, handleSearchCancel,
149
+ counts, tabCounts,
150
+ };
151
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,25 @@
1
+ export class Logger {
2
+ constructor(verbose = false) {
3
+ this.verbose = verbose;
4
+ }
5
+ info(message) {
6
+ console.log(message);
7
+ }
8
+ error(message) {
9
+ console.error(message);
10
+ }
11
+ success(message) {
12
+ console.log(message);
13
+ }
14
+ debug(message) {
15
+ if (this.verbose) {
16
+ console.log(`[verbose] ${message}`);
17
+ }
18
+ }
19
+ warn(message) {
20
+ console.warn(message);
21
+ }
22
+ timestamp() {
23
+ return new Date().toISOString();
24
+ }
25
+ }