@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,106 @@
1
+ import { commandLine } from "../../shared/ui/format.js";
2
+ /**
3
+ * Render a task chain as ASCII art, starting from a root task.
4
+ * Handles cycles (shows "(cycle to TaskName)"), missing tasks
5
+ * (shows "(missing task)"), and silent chain markers ("[s]").
6
+ */
7
+ export function renderChainDiagram(rootTaskId, tasks) {
8
+ const taskMap = new Map();
9
+ for (const task of tasks) {
10
+ taskMap.set(task.id, task);
11
+ }
12
+ const rootTask = taskMap.get(rootTaskId);
13
+ if (!rootTask) {
14
+ return renderMissingTaskBox(rootTaskId);
15
+ }
16
+ const visited = new Set();
17
+ function renderTask(task, _branch) {
18
+ if (visited.has(task.id)) {
19
+ const name = task.name || task.id;
20
+ return `(cycle to ${name})`;
21
+ }
22
+ visited.add(task.id);
23
+ const lines = [];
24
+ const silent = task.silentChain ? " [s]" : "";
25
+ // Name line
26
+ lines.push(`${task.name}${silent}`);
27
+ // Steps or command
28
+ if (task.steps && task.steps.length > 0) {
29
+ for (let i = 0; i < task.steps.length; i++) {
30
+ const step = task.steps[i];
31
+ for (const cmd of step.commands) {
32
+ lines.push(` Step ${i + 1}: ${commandLine(cmd.command, cmd.commandArgs)}`);
33
+ }
34
+ }
35
+ }
36
+ else {
37
+ lines.push(` ${commandLine(task.command, task.commandArgs)}`);
38
+ }
39
+ // Branch annotations
40
+ const successTarget = task.onSuccessTaskId
41
+ ? resolveBranchLabel(task.onSuccessTaskId, taskMap, visited)
42
+ : "(none)";
43
+ const failureTarget = task.onFailureTaskId
44
+ ? resolveBranchLabel(task.onFailureTaskId, taskMap, visited)
45
+ : "(none)";
46
+ lines.push(` onSuccess -> ${successTarget}`);
47
+ lines.push(` onFailure -> ${failureTarget}`);
48
+ // Build box
49
+ const box = renderBox(lines);
50
+ // Recurse into children that haven't been visited yet
51
+ const children = [];
52
+ if (task.onSuccessTaskId) {
53
+ const child = taskMap.get(task.onSuccessTaskId);
54
+ if (child && !visited.has(child.id)) {
55
+ children.push(renderTask(child, "success"));
56
+ }
57
+ }
58
+ if (task.onFailureTaskId) {
59
+ const child = taskMap.get(task.onFailureTaskId);
60
+ if (child && !visited.has(child.id)) {
61
+ children.push(renderTask(child, "failure"));
62
+ }
63
+ }
64
+ if (children.length > 0) {
65
+ return box + "\n" + children.map((c) => connector() + "\n" + c).join("\n");
66
+ }
67
+ return box;
68
+ }
69
+ const result = renderTask(rootTask, null);
70
+ return result;
71
+ }
72
+ function resolveBranchLabel(targetId, taskMap, visited) {
73
+ const target = taskMap.get(targetId);
74
+ if (!target)
75
+ return `${targetId} (missing task)`;
76
+ if (visited.has(targetId))
77
+ return `${target.name || targetId} (cycle)`;
78
+ const silent = target.silentChain ? " (silent)" : "";
79
+ return `${target.name || targetId}${silent}`;
80
+ }
81
+ function renderBox(lines) {
82
+ const maxWidth = Math.max(3, ...lines.map((l) => stripAnsi(l).length)) + 4;
83
+ const border = "+" + "-".repeat(maxWidth) + "+";
84
+ const boxed = [border];
85
+ for (const line of lines) {
86
+ const displayLen = stripAnsi(line).length;
87
+ const padding = maxWidth - displayLen - 2;
88
+ boxed.push(`| ${line}${" ".repeat(Math.max(0, padding))} |`);
89
+ }
90
+ boxed.push(border);
91
+ return boxed.join("\n");
92
+ }
93
+ function renderMissingTaskBox(taskId) {
94
+ const lines = [taskId, " (missing task)"];
95
+ return renderBox(lines);
96
+ }
97
+ function connector() {
98
+ return " | \n v";
99
+ }
100
+ /**
101
+ * Strip ANSI escape codes for length calculation.
102
+ * Safe no-op for plain strings.
103
+ */
104
+ function stripAnsi(str) {
105
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
106
+ }
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useState, useCallback, useMemo } from "react";
3
+ import { Box, Text } from "ink";
4
+ import { darkTheme as theme } from "../../shared/ui/theme.js";
5
+ import { t } from "../../shared/i18n/index.js";
6
+ import { useUndoRedo } from "../../shared/hooks/useUndoRedo.js";
7
+ import { highlightSegments } from "../../shared/utils/syntax.js";
8
+ import { joinCommandLines } from "../../loop-config.js";
9
+ import { CODE_EDITOR_UNDO_LIMIT, CODE_EDITOR_SYNTAX_COLORS } from "../../shared/config/constants.js";
10
+ import { useModalDimensions } from "./useModalDimensions.js";
11
+ import { useEditorKeyboard } from "./useEditorKeyboard.js";
12
+ export function CodeEditorModal(props) {
13
+ const { initialValue, onSave, onCancel } = props;
14
+ const { modalWidth, modalHeight, editorVisibleLines: EDITOR_VISIBLE_LINES } = useModalDimensions();
15
+ const { value, setValue, undo, redo } = useUndoRedo(initialValue, CODE_EDITOR_UNDO_LIMIT);
16
+ const [cursorRow, setCursorRow] = useState(() => {
17
+ const lines = initialValue.split("\n");
18
+ return Math.max(0, lines.length - 1);
19
+ });
20
+ const [cursorCol, setCursorCol] = useState(() => {
21
+ const lines = initialValue.split("\n");
22
+ return (lines[lines.length - 1] ?? "").length;
23
+ });
24
+ const [flashMsg, setFlashMsg] = useState(null);
25
+ const lines = useMemo(() => (value ? value.split("\n") : [""]), [value]);
26
+ const lineCount = lines.length;
27
+ const lineNumWidth = String(lineCount).length;
28
+ React.useEffect(() => {
29
+ if (flashMsg) {
30
+ const timer = setTimeout(() => setFlashMsg(null), 1500);
31
+ return () => clearTimeout(timer);
32
+ }
33
+ }, [flashMsg]);
34
+ const scrollStart = useMemo(() => {
35
+ if (cursorRow < EDITOR_VISIBLE_LINES)
36
+ return 0;
37
+ return cursorRow - EDITOR_VISIBLE_LINES + 1;
38
+ }, [cursorRow]);
39
+ const visibleLines = useMemo(() => lines.slice(scrollStart, scrollStart + EDITOR_VISIBLE_LINES), [lines, scrollStart]);
40
+ const applyMutation = useCallback((nextLines, newCursorRow, newCursorCol) => {
41
+ setValue(nextLines.join("\n"));
42
+ setCursorRow(newCursorRow);
43
+ setCursorCol(newCursorCol);
44
+ }, [setValue]);
45
+ useEditorKeyboard({
46
+ lines,
47
+ cursorRow,
48
+ cursorCol,
49
+ value,
50
+ setValue,
51
+ setCursorRow,
52
+ setCursorCol,
53
+ undo,
54
+ redo,
55
+ onSave,
56
+ onCancel,
57
+ setFlashMsg,
58
+ applyMutation,
59
+ });
60
+ const joined = joinCommandLines(value);
61
+ const innerWidth = modalWidth - 2;
62
+ const truncatedPreview = joined.length > innerWidth
63
+ ? joined.slice(0, innerWidth - 1) + t("codeEditor.previewTruncated")
64
+ : joined;
65
+ const accent = theme.accent.brand;
66
+ return (_jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: _jsxs(Box, { width: modalWidth, height: modalHeight, flexDirection: "column", backgroundColor: theme.bg.elevated, borderStyle: "round", borderColor: accent, paddingX: 1, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: accent, bold: true, children: t("codeEditor.title") }), _jsxs(Text, { color: theme.text.muted, children: [lineCount, " ", t("cmdEditor.lines")] })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visibleLines.map((line, visIdx) => {
67
+ const rowIdx = scrollStart + visIdx;
68
+ const isCursor = rowIdx === cursorRow;
69
+ const lineNum = String(rowIdx + 1).padStart(lineNumWidth, " ");
70
+ if (isCursor) {
71
+ const before = line.slice(0, cursorCol);
72
+ const cur = cursorCol < line.length ? line[cursorCol] : " ";
73
+ const after = cursorCol < line.length ? line.slice(cursorCol + 1) : "";
74
+ // Cursor line: render with cursor (no syntax highlight on cursor line)
75
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), _jsx(Text, { color: theme.text.primary, children: before }), _jsx(Text, { inverse: true, children: cur }), _jsx(Text, { color: theme.text.primary, children: after })] }, rowIdx));
76
+ }
77
+ // Non-cursor line: syntax highlighted (whitespace preserved)
78
+ const segments = highlightSegments(line, CODE_EDITOR_SYNTAX_COLORS, theme.text.muted);
79
+ if (segments.length === 0) {
80
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), _jsx(Text, { children: " " })] }, rowIdx));
81
+ }
82
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), segments.map((seg, ti) => (_jsx(Text, { color: seg.color, children: seg.value }, ti)))] }, rowIdx));
83
+ }) }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [t("codeEditor.previewLabel"), " "] }), _jsx(Text, { color: theme.text.secondary, children: truncatedPreview || t("codeEditor.emptyPlaceholder") })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonCopy") }), _jsx(Text, { color: theme.text.muted, children: "ctrl+x" }), _jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonPaste") }), _jsx(Text, { color: theme.text.muted, children: "ctrl+v" }), _jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonClear") }), _jsx(Text, { color: theme.text.muted, children: "ctrl+l" })] }), flashMsg ? (_jsx(Text, { color: theme.semantic.success, children: flashMsg })) : null] }), _jsx(Box, { children: _jsx(Text, { color: theme.text.muted, children: t("codeEditor.hint") }) })] }) }));
84
+ }
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, 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 { t } from "../../shared/i18n/index.js";
5
+ import { CODE_EDITOR_MAX_VISIBLE } from "../../shared/config/constants.js";
6
+ export function CodeEditorPreview(props) {
7
+ const { value, hint, isActive, onActivate } = props;
8
+ const lines = value ? value.split("\n") : [];
9
+ const visibleLines = lines.slice(0, CODE_EDITOR_MAX_VISIBLE);
10
+ const truncated = lines.length > CODE_EDITOR_MAX_VISIBLE;
11
+ const hasValue = lines.length > 0;
12
+ void onActivate; // consumed by parent via useInput when active
13
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(Box, { borderStyle: "single", borderColor: isActive ? theme.accent.brand : theme.border.dim, backgroundColor: isActive ? theme.bg.input : undefined, paddingLeft: 1, overflow: "hidden", width: "100%", flexDirection: "column", children: hasValue ? (_jsxs(_Fragment, { children: [visibleLines.map((line, i) => (_jsx(Text, { color: theme.text.primary, children: line }, i))), truncated ? (_jsx(Text, { color: theme.text.muted, children: t("codeEditor.previewTruncated") })) : null] })) : (_jsx(Text, { color: theme.text.muted, children: hint })) }), isActive ? (_jsxs(Box, { marginTop: 0, children: [_jsx(Text, { color: theme.accent.brand, children: "› " }), _jsx(Text, { color: theme.text.muted, children: t("codeEditor.fieldHint") })] })) : null] }));
14
+ }
@@ -0,0 +1,150 @@
1
+ import { useInput } from "ink";
2
+ import { t } from "../../shared/i18n/index.js";
3
+ import { copyToClipboard, readFromClipboard } from "../../shared/clipboard.js";
4
+ import { sanitizeMultilinePaste, wrapCommand } from "../../shared/utils/paste.js";
5
+ export function useEditorKeyboard(params) {
6
+ const { lines, cursorRow, cursorCol, value, setValue, setCursorRow, setCursorCol, undo, redo, onSave, onCancel, setFlashMsg, applyMutation, } = params;
7
+ useInput((input, key) => {
8
+ if (input.includes("\x1b[200~")) {
9
+ const pasted = wrapCommand(sanitizeMultilinePaste(input));
10
+ if (pasted) {
11
+ const next = [...lines];
12
+ const line = next[cursorRow] ?? "";
13
+ next[cursorRow] =
14
+ line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
15
+ applyMutation(next, cursorRow, cursorCol + pasted.length);
16
+ }
17
+ return;
18
+ }
19
+ // Swallow SGR mouse sequences
20
+ if (input.includes("[<"))
21
+ return;
22
+ if (key.escape) {
23
+ onCancel();
24
+ return;
25
+ }
26
+ if ((key.ctrl && input === "s") || input === "\x13") {
27
+ onSave(value);
28
+ return;
29
+ }
30
+ if ((key.ctrl && !key.shift && input === "z") || input === "\x1a") {
31
+ undo();
32
+ return;
33
+ }
34
+ if (key.ctrl && key.shift && input === "z") {
35
+ redo();
36
+ return;
37
+ }
38
+ if ((key.ctrl && input === "x") || input === "\x18") {
39
+ copyToClipboard(value);
40
+ setValue("");
41
+ setCursorRow(0);
42
+ setCursorCol(0);
43
+ setFlashMsg(t("codeEditor.copied"));
44
+ return;
45
+ }
46
+ if ((key.ctrl && input === "v") || input === "\x16") {
47
+ const clip = readFromClipboard();
48
+ if (clip) {
49
+ const pasted = sanitizeMultilinePaste(clip);
50
+ if (pasted) {
51
+ const next = [...lines];
52
+ const line = next[cursorRow] ?? "";
53
+ next[cursorRow] =
54
+ line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
55
+ applyMutation(next, cursorRow, cursorCol + pasted.length);
56
+ }
57
+ }
58
+ else {
59
+ setFlashMsg(t("codeEditor.clipboardEmpty"));
60
+ }
61
+ return;
62
+ }
63
+ if ((key.ctrl && input === "l") || input === "\x0c") {
64
+ setValue("");
65
+ setCursorRow(0);
66
+ setCursorCol(0);
67
+ setFlashMsg(t("codeEditor.cleared"));
68
+ return;
69
+ }
70
+ if (key.return) {
71
+ const next = [...lines];
72
+ const line = next[cursorRow] ?? "";
73
+ next.splice(cursorRow, 1, line.slice(0, cursorCol), line.slice(cursorCol));
74
+ applyMutation(next, cursorRow + 1, 0);
75
+ return;
76
+ }
77
+ if (key.upArrow) {
78
+ if (cursorRow > 0) {
79
+ const newRow = cursorRow - 1;
80
+ setCursorRow(newRow);
81
+ setCursorCol((c) => Math.min(c, (lines[newRow] ?? "").length));
82
+ }
83
+ return;
84
+ }
85
+ if (key.downArrow) {
86
+ if (cursorRow < lines.length - 1) {
87
+ const newRow = cursorRow + 1;
88
+ setCursorRow(newRow);
89
+ setCursorCol((c) => Math.min(c, (lines[newRow] ?? "").length));
90
+ }
91
+ return;
92
+ }
93
+ if (key.leftArrow) {
94
+ if (cursorCol > 0) {
95
+ setCursorCol((c) => c - 1);
96
+ }
97
+ else if (cursorRow > 0) {
98
+ setCursorRow((r) => r - 1);
99
+ setCursorCol((lines[cursorRow - 1] ?? "").length);
100
+ }
101
+ return;
102
+ }
103
+ if (key.rightArrow) {
104
+ const curLen = (lines[cursorRow] ?? "").length;
105
+ if (cursorCol < curLen) {
106
+ setCursorCol((c) => c + 1);
107
+ }
108
+ else if (cursorRow < lines.length - 1) {
109
+ setCursorRow((r) => r + 1);
110
+ setCursorCol(0);
111
+ }
112
+ return;
113
+ }
114
+ if (key.backspace || key.delete) {
115
+ if (cursorCol > 0) {
116
+ const next = [...lines];
117
+ const line = next[cursorRow] ?? "";
118
+ next[cursorRow] =
119
+ line.slice(0, cursorCol - 1) + line.slice(cursorCol);
120
+ applyMutation(next, cursorRow, cursorCol - 1);
121
+ }
122
+ else if (cursorRow > 0) {
123
+ const next = [...lines];
124
+ const prev = next[cursorRow - 1] ?? "";
125
+ const cur = next[cursorRow] ?? "";
126
+ next.splice(cursorRow - 1, 2, prev + cur);
127
+ applyMutation(next, cursorRow - 1, prev.length);
128
+ }
129
+ return;
130
+ }
131
+ if (input.length > 1 && !key.meta) {
132
+ const pasted = wrapCommand(sanitizeMultilinePaste(input));
133
+ if (pasted) {
134
+ const next = [...lines];
135
+ const line = next[cursorRow] ?? "";
136
+ next[cursorRow] =
137
+ line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
138
+ applyMutation(next, cursorRow, cursorCol + pasted.length);
139
+ }
140
+ return;
141
+ }
142
+ if (input.length === 1 && input >= " " && input <= "~") {
143
+ const next = [...lines];
144
+ const line = next[cursorRow] ?? "";
145
+ next[cursorRow] =
146
+ line.slice(0, cursorCol) + input + line.slice(cursorCol);
147
+ applyMutation(next, cursorRow, cursorCol + 1);
148
+ }
149
+ }, { isActive: true });
150
+ }
@@ -0,0 +1,9 @@
1
+ import { CODE_EDITOR_MODAL_HEIGHT, CODE_EDITOR_MODAL_WIDTH, } from "../../shared/config/constants.js";
2
+ export function useModalDimensions() {
3
+ const termRows = process.stdout.rows || 24;
4
+ const termCols = process.stdout.columns || 80;
5
+ const modalHeight = Math.min(CODE_EDITOR_MODAL_HEIGHT, termRows - 2);
6
+ const modalWidth = Math.min(CODE_EDITOR_MODAL_WIDTH, termCols - 2);
7
+ const editorVisibleLines = Math.max(3, modalHeight - 6);
8
+ return { modalWidth, modalHeight, editorVisibleLines };
9
+ }
@@ -0,0 +1,132 @@
1
+ import { COMMAND_TIER_ACTION, COMMAND_TIER_CONFIRM, COMMAND_TIER_GLOBAL, COMMAND_CATEGORY_GLOBAL, COMMAND_CATEGORY_FILTERS, COMMAND_CATEGORY_LOOP, COMMAND_CATEGORY_TASK, COMMAND_CATEGORY_PROJECT, } from '../../shared/config/constants.js';
2
+ import { t } from '../../shared/i18n/index.js';
3
+ function globalCommands() {
4
+ return [
5
+ { label: t('cmd.help'), value: 'help', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+p' },
6
+ { label: t('cmd.debug'), value: 'debug', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+b' },
7
+ { label: t('cmd.toggleApi'), value: 'toggle-api', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
8
+ { label: t('cmd.api'), value: 'api', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
9
+ { label: t('cmd.toggleMcp'), value: 'toggle-mcp', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
10
+ { label: t('cmd.mcp'), value: 'mcp', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
11
+ { label: t('cmd.toggleTelemetry'), value: 'toggle-telemetry', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
12
+ { label: t('cmd.telemetryDiagnostics'), value: 'telemetry-diagnostics', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
13
+ { label: t('cmd.telemetry'), value: 'telemetry', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
14
+ { label: t('cmd.export'), value: 'export', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+x' },
15
+ { label: t('cmd.import'), value: 'import', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+i' },
16
+ ];
17
+ }
18
+ function loopFilterCommands() {
19
+ return [
20
+ { label: t('cmd.search'), value: 'search', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+s' },
21
+ { label: t('cmd.filterStatus'), value: 'filter-status', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+t' },
22
+ { label: t('cmd.sort'), value: 'sort', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+o' },
23
+ { label: t('cmd.project'), value: 'filter-project', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+p' },
24
+ ];
25
+ }
26
+ function taskFilterCommands() {
27
+ return [
28
+ { label: t('cmd.searchTasks'), value: 'search', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+s' },
29
+ ];
30
+ }
31
+ function projectFilterCommands() {
32
+ return [
33
+ { label: t('cmd.searchProjects'), value: 'search', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+s' },
34
+ ];
35
+ }
36
+ /**
37
+ * Re-rank autocomplete options: exact match → prefix match → fuzzy (existing order).
38
+ * Stable within each group. Exported for testing.
39
+ */
40
+ export function rankCommands(input, options) {
41
+ if (input.length === 0)
42
+ return options;
43
+ const q = input.toLowerCase();
44
+ const exact = [];
45
+ const prefix = [];
46
+ const fuzzy = [];
47
+ for (const opt of options) {
48
+ const l = opt.label.toLowerCase();
49
+ const v = opt.value.toLowerCase();
50
+ if (l === q || v === q) {
51
+ exact.push(opt);
52
+ }
53
+ else if (l.startsWith(q) || v.startsWith(q)) {
54
+ prefix.push(opt);
55
+ }
56
+ else {
57
+ fuzzy.push(opt);
58
+ }
59
+ }
60
+ return [...exact, ...prefix, ...fuzzy];
61
+ }
62
+ export function buildCommands(context) {
63
+ const commands = [...globalCommands()];
64
+ if (context.activeTab === 'loops') {
65
+ commands.push(...loopFilterCommands());
66
+ commands.push({
67
+ label: t('cmd.newLoop'), value: 'new-loop', hint: '',
68
+ tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+n',
69
+ });
70
+ if (context.selectedLoop !== null) {
71
+ const loop = context.selectedLoop;
72
+ const desc = loop.description || loop.id;
73
+ commands.push({ label: t('cmd.edit'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+e' }, { label: t('cmd.pause'), value: 'pause', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+p' }, { label: t('cmd.play'), value: 'play', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+r' }, { label: t('cmd.stop'), value: 'stop', hint: t('confirm.stopLoop', { name: desc }), tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+s' }, { label: t('cmd.trigger'), value: 'trigger', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+t' }, { label: t('cmd.clone'), value: 'clone', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+c' }, { label: t('cmd.delete'), value: 'delete', hint: t('confirm.deleteLoop', { name: desc }), tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+d' }, { label: t('cmd.logs'), value: 'logs', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+o' });
74
+ if (loop.taskId && loop.isRecipe) {
75
+ commands.push({
76
+ label: t('cmd.diagram'), value: 'diagram', hint: '',
77
+ tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP,
78
+ });
79
+ }
80
+ }
81
+ }
82
+ if (context.activeTab === 'tasks') {
83
+ commands.push(...taskFilterCommands());
84
+ commands.push({
85
+ label: t('cmd.newTask'), value: 'new-task', hint: '',
86
+ tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+n',
87
+ });
88
+ if (context.selectedTask !== null) {
89
+ const task = context.selectedTask;
90
+ commands.push({ label: t('cmd.editTask'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+e' }, { label: t('cmd.deleteTask'), value: 'delete', hint: t('confirm.deleteTask', { id: task.id }), tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+d' });
91
+ if (task.isRecipeTask) {
92
+ // Remove edit and delete for recipe tasks — they are immutable
93
+ commands.pop();
94
+ commands.pop();
95
+ }
96
+ }
97
+ }
98
+ if (context.activeTab === 'projects') {
99
+ commands.push(...projectFilterCommands());
100
+ commands.push({
101
+ label: t('cmd.newProject'), value: 'new-project', hint: '',
102
+ tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+n',
103
+ });
104
+ if (context.selectedProject !== null) {
105
+ const project = context.selectedProject;
106
+ commands.push({ label: t('cmd.editProject'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+e' }, { label: t('cmd.deleteProject'), value: 'delete', hint: t('confirm.deleteProject', { name: project.name }), tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+d' });
107
+ }
108
+ }
109
+ return commands;
110
+ }
111
+ export function buildTabCommands(context) {
112
+ const commands = [...globalCommands()];
113
+ if (context.activeTab === 'loops') {
114
+ commands.push(...loopFilterCommands());
115
+ commands.push({ label: t('cmd.newLoop'), value: 'new-loop', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+n' }, { label: t('cmd.edit'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+e' }, { label: t('cmd.pause'), value: 'pause', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+p' }, { label: t('cmd.play'), value: 'play', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+r' }, { label: t('cmd.stop'), value: 'stop', hint: '', tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+s' }, { label: t('cmd.trigger'), value: 'trigger', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+t' }, { label: t('cmd.clone'), value: 'clone', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+c' }, { label: t('cmd.delete'), value: 'delete', hint: '', tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+d' }, { label: t('cmd.logs'), value: 'logs', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP, shortcut: 'ctrl+a+o' });
116
+ if (context.selectedLoop?.taskId && context.selectedLoop?.isRecipe) {
117
+ commands.push({
118
+ label: t('cmd.diagram'), value: 'diagram', hint: '',
119
+ tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_LOOP,
120
+ });
121
+ }
122
+ }
123
+ if (context.activeTab === 'tasks') {
124
+ commands.push(...taskFilterCommands());
125
+ commands.push({ label: t('cmd.newTask'), value: 'new-task', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+n' }, { label: t('cmd.editTask'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+e' }, { label: t('cmd.deleteTask'), value: 'delete', hint: '', tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_TASK, shortcut: 'ctrl+a+d' });
126
+ }
127
+ if (context.activeTab === 'projects') {
128
+ commands.push(...projectFilterCommands());
129
+ commands.push({ label: t('cmd.newProject'), value: 'new-project', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+n' }, { label: t('cmd.editProject'), value: 'edit', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+e' }, { label: t('cmd.deleteProject'), value: 'delete', hint: '', tier: COMMAND_TIER_CONFIRM, category: COMMAND_CATEGORY_PROJECT, shortcut: 'ctrl+a+d' });
130
+ }
131
+ return commands;
132
+ }