loop-task 2.1.4 → 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} +3 -3
  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
@@ -1,244 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useState, useCallback, useMemo } from "react";
3
- import { Box, Text, useInput } from "ink";
4
- import { darkTheme as theme } from "../theme.js";
5
- import { t } from "../../i18n/index.js";
6
- import { useUndoRedo } from "../../shared/useUndoRedo.js";
7
- import { highlightSegments } from "../utils/syntax.js";
8
- import { joinCommandLines } from "../../loop-config.js";
9
- import { copyToClipboard, readFromClipboard } from "../../shared/clipboard.js";
10
- import { sanitizeMultilinePaste, wrapCommand } from "../utils/paste.js";
11
- import { CODE_EDITOR_MODAL_HEIGHT, CODE_EDITOR_MODAL_WIDTH, CODE_EDITOR_UNDO_LIMIT, CODE_EDITOR_SYNTAX_COLORS, } from "../../config/constants.js";
12
- // Cap modal to terminal size: never exceed (terminal - 2) so the border
13
- // and a 1-row margin are always visible. Ideal max is the constant.
14
- function useModalDimensions() {
15
- const termRows = process.stdout.rows || 24;
16
- const termCols = process.stdout.columns || 80;
17
- const modalHeight = Math.min(CODE_EDITOR_MODAL_HEIGHT, termRows - 2);
18
- const modalWidth = Math.min(CODE_EDITOR_MODAL_WIDTH, termCols - 2);
19
- // total height - title - preview - buttons - hint - 2 padding = -6
20
- const editorVisibleLines = Math.max(3, modalHeight - 6);
21
- return { modalWidth, modalHeight, editorVisibleLines };
22
- }
23
- export function CodeEditorModal(props) {
24
- const { initialValue, onSave, onCancel } = props;
25
- const { modalWidth, modalHeight, editorVisibleLines: EDITOR_VISIBLE_LINES } = useModalDimensions();
26
- const { value, setValue, undo, redo } = useUndoRedo(initialValue, CODE_EDITOR_UNDO_LIMIT);
27
- const [cursorRow, setCursorRow] = useState(() => {
28
- const lines = initialValue.split("\n");
29
- return Math.max(0, lines.length - 1);
30
- });
31
- const [cursorCol, setCursorCol] = useState(() => {
32
- const lines = initialValue.split("\n");
33
- return (lines[lines.length - 1] ?? "").length;
34
- });
35
- const [flashMsg, setFlashMsg] = useState(null);
36
- const lines = useMemo(() => (value ? value.split("\n") : [""]), [value]);
37
- const lineCount = lines.length;
38
- const lineNumWidth = String(lineCount).length;
39
- // Flash message auto-clear
40
- React.useEffect(() => {
41
- if (flashMsg) {
42
- const timer = setTimeout(() => setFlashMsg(null), 1500);
43
- return () => clearTimeout(timer);
44
- }
45
- }, [flashMsg]);
46
- // Scroll: keep cursor visible in the editor area
47
- const scrollStart = useMemo(() => {
48
- if (cursorRow < EDITOR_VISIBLE_LINES)
49
- return 0;
50
- return cursorRow - EDITOR_VISIBLE_LINES + 1;
51
- }, [cursorRow]);
52
- const visibleLines = useMemo(() => lines.slice(scrollStart, scrollStart + EDITOR_VISIBLE_LINES), [lines, scrollStart]);
53
- // ---- Helper: apply a text mutation via setValue and update cursor ----
54
- const applyMutation = useCallback((nextLines, newCursorRow, newCursorCol) => {
55
- setValue(nextLines.join("\n"));
56
- setCursorRow(newCursorRow);
57
- setCursorCol(newCursorCol);
58
- }, [setValue]);
59
- // ---- Keyboard handling ----
60
- useInput((input, key) => {
61
- // Bracketed paste: content wrapped in ESC[200~ ... ESC[201~
62
- // Must be detected BEFORE the escape check — the leading ESC trips
63
- // key.escape and would close the modal before paste is handled.
64
- if (input.includes("\x1b[200~")) {
65
- const pasted = wrapCommand(sanitizeMultilinePaste(input));
66
- if (pasted) {
67
- const next = [...lines];
68
- const line = next[cursorRow] ?? "";
69
- next[cursorRow] =
70
- line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
71
- applyMutation(next, cursorRow, cursorCol + pasted.length);
72
- }
73
- return;
74
- }
75
- // Esc → cancel
76
- if (key.escape) {
77
- onCancel();
78
- return;
79
- }
80
- // Ctrl+S → save (also accept raw \x13 fallback)
81
- if ((key.ctrl && input === "s") || input === "\x13") {
82
- onSave(value);
83
- return;
84
- }
85
- // Ctrl+Z → undo (also accept raw \x1a fallback)
86
- if ((key.ctrl && !key.shift && input === "z") || input === "\x1a") {
87
- undo();
88
- return;
89
- }
90
- // Ctrl+Shift+Z → redo
91
- if (key.ctrl && key.shift && input === "z") {
92
- redo();
93
- return;
94
- }
95
- // Ctrl+X → cut (copy all to clipboard, then clear; also accept \x18)
96
- if ((key.ctrl && input === "x") || input === "\x18") {
97
- copyToClipboard(value);
98
- setValue("");
99
- setCursorRow(0);
100
- setCursorCol(0);
101
- setFlashMsg(t("codeEditor.copied"));
102
- return;
103
- }
104
- // Ctrl+V → paste from clipboard (also accept raw \x16 fallback for
105
- // terminals that don't set key.ctrl on the SYN control byte)
106
- if ((key.ctrl && input === "v") || input === "\x16") {
107
- const clip = readFromClipboard();
108
- if (clip) {
109
- const pasted = sanitizeMultilinePaste(clip);
110
- if (pasted) {
111
- const next = [...lines];
112
- const line = next[cursorRow] ?? "";
113
- next[cursorRow] =
114
- line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
115
- applyMutation(next, cursorRow, cursorCol + pasted.length);
116
- }
117
- }
118
- else {
119
- // No clipboard tool available (common over SSH). Bracketed paste
120
- // (right-click in the terminal) still works — point the user at it.
121
- setFlashMsg(t("codeEditor.clipboardEmpty"));
122
- }
123
- return;
124
- }
125
- // Ctrl+L → clear all
126
- if ((key.ctrl && input === "l") || input === "\x0c") {
127
- setValue("");
128
- setCursorRow(0);
129
- setCursorCol(0);
130
- setFlashMsg(t("codeEditor.cleared"));
131
- return;
132
- }
133
- // Enter → insert newline at cursor
134
- if (key.return) {
135
- const next = [...lines];
136
- const line = next[cursorRow] ?? "";
137
- next.splice(cursorRow, 1, line.slice(0, cursorCol), line.slice(cursorCol));
138
- applyMutation(next, cursorRow + 1, 0);
139
- return;
140
- }
141
- // Arrow keys
142
- if (key.upArrow) {
143
- if (cursorRow > 0) {
144
- const newRow = cursorRow - 1;
145
- setCursorRow(newRow);
146
- setCursorCol((c) => Math.min(c, (lines[newRow] ?? "").length));
147
- }
148
- return;
149
- }
150
- if (key.downArrow) {
151
- if (cursorRow < lines.length - 1) {
152
- const newRow = cursorRow + 1;
153
- setCursorRow(newRow);
154
- setCursorCol((c) => Math.min(c, (lines[newRow] ?? "").length));
155
- }
156
- return;
157
- }
158
- if (key.leftArrow) {
159
- if (cursorCol > 0) {
160
- setCursorCol((c) => c - 1);
161
- }
162
- else if (cursorRow > 0) {
163
- setCursorRow((r) => r - 1);
164
- setCursorCol((lines[cursorRow - 1] ?? "").length);
165
- }
166
- return;
167
- }
168
- if (key.rightArrow) {
169
- const curLen = (lines[cursorRow] ?? "").length;
170
- if (cursorCol < curLen) {
171
- setCursorCol((c) => c + 1);
172
- }
173
- else if (cursorRow < lines.length - 1) {
174
- setCursorRow((r) => r + 1);
175
- setCursorCol(0);
176
- }
177
- return;
178
- }
179
- // Backspace / Delete
180
- if (key.backspace || key.delete) {
181
- if (cursorCol > 0) {
182
- const next = [...lines];
183
- const line = next[cursorRow] ?? "";
184
- next[cursorRow] =
185
- line.slice(0, cursorCol - 1) + line.slice(cursorCol);
186
- applyMutation(next, cursorRow, cursorCol - 1);
187
- }
188
- else if (cursorRow > 0) {
189
- const next = [...lines];
190
- const prev = next[cursorRow - 1] ?? "";
191
- const cur = next[cursorRow] ?? "";
192
- next.splice(cursorRow - 1, 2, prev + cur);
193
- applyMutation(next, cursorRow - 1, prev.length);
194
- }
195
- return;
196
- }
197
- // Multi-char printable input = unbracketed paste (e.g. right-click
198
- // in terminals that don't support DECSET 2004). Preserve newlines.
199
- if (input.length > 1 && !key.meta) {
200
- const pasted = wrapCommand(sanitizeMultilinePaste(input));
201
- if (pasted) {
202
- const next = [...lines];
203
- const line = next[cursorRow] ?? "";
204
- next[cursorRow] =
205
- line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
206
- applyMutation(next, cursorRow, cursorCol + pasted.length);
207
- }
208
- return;
209
- }
210
- // Single printable char → insert at cursor
211
- if (input.length === 1 && input >= " " && input <= "~") {
212
- const next = [...lines];
213
- const line = next[cursorRow] ?? "";
214
- next[cursorRow] =
215
- line.slice(0, cursorCol) + input + line.slice(cursorCol);
216
- applyMutation(next, cursorRow, cursorCol + 1);
217
- }
218
- }, { isActive: true });
219
- // ---- Live preview footer ----
220
- const joined = joinCommandLines(value);
221
- const innerWidth = modalWidth - 2;
222
- const truncatedPreview = joined.length > innerWidth
223
- ? joined.slice(0, innerWidth - 1) + t("codeEditor.previewTruncated")
224
- : joined;
225
- const accent = theme.accent.brand;
226
- 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) => {
227
- const rowIdx = scrollStart + visIdx;
228
- const isCursor = rowIdx === cursorRow;
229
- const lineNum = String(rowIdx + 1).padStart(lineNumWidth, " ");
230
- if (isCursor) {
231
- const before = line.slice(0, cursorCol);
232
- const cur = cursorCol < line.length ? line[cursorCol] : " ";
233
- const after = cursorCol < line.length ? line.slice(cursorCol + 1) : "";
234
- // Cursor line: render with cursor (no syntax highlight on cursor line)
235
- 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));
236
- }
237
- // Non-cursor line: syntax highlighted (whitespace preserved)
238
- const segments = highlightSegments(line, CODE_EDITOR_SYNTAX_COLORS, theme.text.muted);
239
- if (segments.length === 0) {
240
- return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), _jsx(Text, { children: " " })] }, rowIdx));
241
- }
242
- 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));
243
- }) }), _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") }) })] }) }));
244
- }
@@ -1,273 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState, useEffect, useMemo, useCallback } from "react";
3
- import { Box, Text, useInput } from "ink";
4
- import { useAutocompleteState, } from "ink-combobox";
5
- import { darkTheme as theme } from "../theme.js";
6
- import { t } from "../../i18n/index.js";
7
- import { buildCommands, rankCommands } from "../commands.js";
8
- import { COMMAND_INPUT_DROPDOWN_MAX_VISIBLE, COMMAND_INPUT_HEIGHT, } from "../../config/constants.js";
9
- import { sanitizePaste } from "../utils/paste.js";
10
- export { sanitizePaste } from "../utils/paste.js";
11
- function renderInputLine(value, cursorOffset) {
12
- if (value.length === 0) {
13
- return "\x1b[7m \x1b[27m";
14
- }
15
- let result = "";
16
- for (let i = 0; i < value.length; i++) {
17
- if (i === cursorOffset) {
18
- result += "\x1b[7m" + value[i] + "\x1b[27m";
19
- }
20
- else {
21
- result += value[i];
22
- }
23
- }
24
- if (cursorOffset >= value.length) {
25
- result += "\x1b[7m \x1b[27m";
26
- }
27
- return result;
28
- }
29
- function renderHighlightedLabel(label, matchRanges, isFocused) {
30
- if (matchRanges.length === 0) {
31
- return isFocused
32
- ? `\x1b[38;2;251;191;36m${label}\x1b[39m`
33
- : label;
34
- }
35
- let result = "";
36
- let pos = 0;
37
- for (const range of matchRanges) {
38
- if (pos < range.start) {
39
- const segment = label.slice(pos, range.start);
40
- result += isFocused
41
- ? `\x1b[38;2;251;191;36m${segment}\x1b[39m`
42
- : segment;
43
- }
44
- const matched = label.slice(range.start, range.end);
45
- result += `\x1b[1m\x1b[38;2;251;191;36m${matched}\x1b[39m\x1b[22m`;
46
- pos = range.end;
47
- }
48
- if (pos < label.length) {
49
- const segment = label.slice(pos);
50
- result += isFocused
51
- ? `\x1b[38;2;251;191;36m${segment}\x1b[39m`
52
- : segment;
53
- }
54
- return result;
55
- }
56
- function CommandDropdown({ state, rankedFiltered, }) {
57
- if (!state.isOpen || state.isLoading || state.error)
58
- return null;
59
- const filtered = rankedFiltered;
60
- const visibleOptions = filtered.slice(state.visibleFromIndex, state.visibleToIndex);
61
- if (visibleOptions.length === 0) {
62
- if (state.inputValue.length > 0) {
63
- return (_jsx(Box, { paddingLeft: 3, position: "absolute", bottom: 3, borderStyle: "single", borderColor: theme.border.dim, children: _jsx(Text, { color: theme.text.muted, children: t("cmdInput.noMatches") }) }));
64
- }
65
- return null;
66
- }
67
- const aboveCount = state.visibleFromIndex;
68
- const belowCount = filtered.length - state.visibleToIndex;
69
- return (_jsxs(Box, { flexDirection: "column", paddingLeft: 3, position: "absolute", bottom: 3, children: [aboveCount > 0 && (_jsx(Text, { color: theme.text.muted, children: ` \u2191 ${aboveCount} more` })), visibleOptions.map((match, i) => {
70
- const actualIndex = state.visibleFromIndex + i;
71
- const isFocused = actualIndex === state.focusedIndex;
72
- const label = renderHighlightedLabel(match.option.label, match.matchRanges, isFocused);
73
- return (_jsxs(Box, { backgroundColor: isFocused ? theme.bg.active : undefined, children: [_jsx(Text, { color: isFocused ? theme.text.inverse : theme.text.muted, children: isFocused ? "\u276f " : " " }), _jsx(Text, { color: isFocused ? theme.text.inverse : undefined, children: label })] }, match.option.value));
74
- }), belowCount > 0 && (_jsx(Text, { color: theme.text.muted, children: ` \u2193 ${belowCount} more` }))] }));
75
- }
76
- function HintBar({ leftHint, rightHint, }) {
77
- return (_jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsx(Box, { children: leftHint }), _jsx(Box, { children: rightHint })] }));
78
- }
79
- function KeyHint({ keyLabel, action }) {
80
- return (_jsxs(Box, { marginRight: 2, children: [_jsx(Text, { bold: true, color: theme.text.primary, children: keyLabel }), _jsx(Text, { color: theme.text.muted, children: " " + action })] }));
81
- }
82
- function CommandMode({ context, onCommand, onCopy, onPanelAction, disabled, navOwner, onInputStateChange, }) {
83
- const commands = useMemo(() => buildCommands(context), [context]);
84
- const options = useMemo(() => commands.map((cmd) => ({ label: cmd.label, value: cmd.value })), [commands]);
85
- const { state, dispatch } = useAutocompleteState({
86
- options,
87
- visibleOptionCount: COMMAND_INPUT_DROPDOWN_MAX_VISIBLE,
88
- onSelect: onCommand,
89
- });
90
- // Re-rank filteredOptions: exact match → prefix match → fuzzy (existing order)
91
- const rankedFiltered = useMemo(() => {
92
- const fo = state.filteredOptions;
93
- if (fo.length === 0 || state.inputValue.length === 0)
94
- return fo;
95
- const byValue = new Map();
96
- for (const m of fo)
97
- byValue.set(m.option.value, m);
98
- const ranked = rankCommands(state.inputValue, fo.map((m) => ({ label: m.option.label, value: m.option.value })));
99
- return ranked
100
- .map((opt) => byValue.get(opt.value))
101
- .filter((m) => m !== undefined);
102
- }, [state.filteredOptions, state.inputValue]);
103
- // Report input state changes to parent for InputOwner resolution (1.2)
104
- useEffect(() => {
105
- onInputStateChange?.(state.inputValue.length > 0, state.isOpen);
106
- }, [state.inputValue, state.isOpen, onInputStateChange]);
107
- const insertText = useCallback((text) => {
108
- for (const ch of text)
109
- dispatch({ type: "INSERT_TEXT", text: ch });
110
- }, [dispatch]);
111
- const clearInput = useCallback(() => {
112
- dispatch({ type: "MOVE_CURSOR_END" });
113
- for (let i = 0; i < state.inputValue.length; i++)
114
- dispatch({ type: "DELETE_BACKWARD" });
115
- }, [dispatch, state.inputValue.length]);
116
- useInput((input, key) => {
117
- // Bracketed paste: content wrapped in ESC[200~ ... ESC[201~ arrives as one
118
- // chunk. Detect first — the leading ESC can otherwise trip the ctrl/escape guards.
119
- if (input.includes("\x1b[200~")) {
120
- insertText(sanitizePaste(input));
121
- return;
122
- }
123
- // Ctrl+U clears the line (the "select all + delete" gesture). Handle before
124
- // the ctrl guard below. Some terminals send it as the raw NAK control char.
125
- if ((key.ctrl && input === "u") || input === "\x15") {
126
- clearInput();
127
- return;
128
- }
129
- if (key.ctrl)
130
- return;
131
- // Multi-char containing CR/LF with no bracketed markers = the VS Code
132
- // Ctrl+Enter escape sequence, handled by App's global useInput. Ignore here.
133
- if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
134
- return;
135
- // When the command bar is empty and no dropdown is open, panels own
136
- // navigation keys — return early so j/k/arrows reach the panel layer.
137
- if (navOwner === "panel" && state.inputValue.length === 0 && !state.isOpen) {
138
- if (input === "j" || input === "k" || key.upArrow || key.downArrow) {
139
- return; // panels own these keys
140
- }
141
- }
142
- // `c` with no modifiers + no open dropdown = contextual copy shortcut
143
- if (onCopy && input === "c" && !state.isOpen) {
144
- onCopy();
145
- return;
146
- }
147
- if (key.escape) {
148
- dispatch({ type: "CLOSE" });
149
- if (state.inputValue.length > 0) {
150
- clearInput();
151
- }
152
- return;
153
- }
154
- if (key.return) {
155
- if (state.isOpen && rankedFiltered.length > 0 && state.focusedIndex < rankedFiltered.length) {
156
- const focused = rankedFiltered[state.focusedIndex];
157
- dispatch({ type: "CLOSE" });
158
- for (let i = 0; i <= state.inputValue.length; i++) {
159
- dispatch({ type: "DELETE_BACKWARD" });
160
- }
161
- onCommand(focused.option.value);
162
- }
163
- else if (state.inputValue.length === 0) {
164
- // Empty command bar: Enter triggers the focused panel's contextual
165
- // action (edit / logs) for terminals that collapse Ctrl+Enter to Enter.
166
- onPanelAction?.();
167
- }
168
- return;
169
- }
170
- if (key.tab) {
171
- dispatch({ type: "ACCEPT" });
172
- return;
173
- }
174
- if (key.downArrow) {
175
- dispatch({ type: "FOCUS_NEXT" });
176
- return;
177
- }
178
- if (key.upArrow) {
179
- dispatch({ type: "FOCUS_PREV" });
180
- return;
181
- }
182
- if (key.backspace || key.delete) {
183
- dispatch({ type: "DELETE_BACKWARD" });
184
- return;
185
- }
186
- if (key.leftArrow) {
187
- dispatch({ type: "MOVE_CURSOR_LEFT" });
188
- return;
189
- }
190
- if (key.rightArrow) {
191
- dispatch({ type: "MOVE_CURSOR_RIGHT" });
192
- return;
193
- }
194
- // Multi-char printable input = an unbracketed single-line paste (e.g. right-click).
195
- if (input.length > 1 && !key.meta) {
196
- insertText(sanitizePaste(input));
197
- return;
198
- }
199
- if (input.length === 1 && !key.ctrl && !key.meta && !key.return && !key.tab && !key.escape && input >= " " && input <= "~") {
200
- dispatch({ type: "INSERT_TEXT", text: input });
201
- }
202
- }, { isActive: !disabled });
203
- const isEmpty = state.inputValue.length === 0;
204
- const cursor = "\x1b[7m \x1b[27m";
205
- const inputContent = isEmpty ? cursor : renderInputLine(state.inputValue, state.cursorOffset);
206
- return (_jsxs(_Fragment, { children: [_jsx(CommandDropdown, { state: state, rankedFiltered: rankedFiltered }), _jsxs(Box, { children: [_jsx(Text, { color: theme.accent.brand, children: "│ " }), isEmpty ? (_jsx(Text, { color: theme.text.muted, children: t("cmdInput.placeholder") })) : (_jsx(Text, { children: inputContent }))] }), _jsx(HintBar, { leftHint: _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 " }), _jsx(KeyHint, { keyLabel: "esc", action: "cancel" })] }), rightHint: _jsxs(Box, { children: [_jsx(KeyHint, { keyLabel: "enter", action: "edit/logs" }), _jsx(KeyHint, { keyLabel: "ctrl+u", action: "clear" }), _jsx(KeyHint, { keyLabel: "c", action: "copy" }), _jsx(KeyHint, { keyLabel: "tab", action: "panels" }), _jsx(KeyHint, { keyLabel: "ctrl+\u2190\u2192", action: "tabs" }), _jsx(KeyHint, { keyLabel: "ctrl+p", action: "commands" })] }) })] }));
207
- }
208
- function ConfirmMode({ confirmState, onConfirmYes, onConfirmCancel, disabled, }) {
209
- const [value, setValue] = useState("");
210
- useInput((input, key) => {
211
- if (key.ctrl)
212
- return;
213
- if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
214
- return;
215
- if (key.escape) {
216
- setValue("");
217
- onConfirmCancel();
218
- return;
219
- }
220
- if (key.return) {
221
- if (value.toLowerCase() === "yes") {
222
- setValue("");
223
- onConfirmYes();
224
- }
225
- else {
226
- setValue("");
227
- onConfirmCancel();
228
- }
229
- return;
230
- }
231
- if (key.backspace || key.delete) {
232
- setValue((v) => v.slice(0, -1));
233
- return;
234
- }
235
- if (input && !key.ctrl && !key.meta && input.length === 1) {
236
- setValue((v) => v + input);
237
- return;
238
- }
239
- }, { isActive: !disabled });
240
- const cursor = "\x1b[7m \x1b[27m";
241
- return (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.semantic.danger, children: "│ " }), _jsx(Text, { color: theme.text.muted, children: confirmState.prompt + " " }), value.length > 0 ? (_jsx(Text, { children: value + cursor })) : (_jsx(Text, { children: cursor }))] }), _jsx(HintBar, { leftHint: _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 " }), _jsx(KeyHint, { keyLabel: "esc", action: "cancel" })] }), rightHint: _jsx(KeyHint, { keyLabel: "enter", action: "confirm" }) })] }));
242
- }
243
- function SearchMode({ value, onSearchChange, onSearchSubmit, onSearchCancel, disabled, }) {
244
- useInput((input, key) => {
245
- if (key.ctrl)
246
- return;
247
- if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
248
- return;
249
- if (key.escape) {
250
- onSearchCancel();
251
- return;
252
- }
253
- if (key.return) {
254
- onSearchSubmit();
255
- return;
256
- }
257
- if (key.backspace || key.delete) {
258
- onSearchChange(value.slice(0, -1));
259
- return;
260
- }
261
- if (input && !key.ctrl && !key.meta && input.length === 1) {
262
- onSearchChange(value + input);
263
- return;
264
- }
265
- }, { isActive: !disabled });
266
- const placeholder = t("cmdInput.searchPlaceholder");
267
- const cursor = "\x1b[7m \x1b[27m";
268
- return (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.accent.project, children: "│ " }), value.length > 0 ? (_jsx(Text, { children: value + cursor })) : (_jsx(Text, { color: theme.text.muted, children: placeholder }))] }), _jsx(HintBar, { leftHint: _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7\u00b7 " }), _jsx(KeyHint, { keyLabel: "esc", action: "cancel" })] }), rightHint: _jsx(KeyHint, { keyLabel: "enter", action: "apply" }) })] }));
269
- }
270
- export function CommandInput(props) {
271
- const { context, onCommand, confirmState, searchState, searchValue, onSearchChange, onSearchSubmit, onSearchCancel, onConfirmYes, onConfirmCancel, onCopy, onPanelAction, navOwner, onInputStateChange, } = props;
272
- return (_jsx(Box, { flexDirection: "column", height: COMMAND_INPUT_HEIGHT, borderStyle: "single", borderColor: theme.border.dim, paddingY: 1, children: searchState?.active ? (_jsx(SearchMode, { value: searchValue, onSearchChange: onSearchChange, onSearchSubmit: onSearchSubmit, onSearchCancel: onSearchCancel, disabled: props.disabled })) : confirmState === null ? (_jsx(CommandMode, { context: context, onCommand: onCommand, onCopy: onCopy, onPanelAction: onPanelAction, disabled: props.disabled, navOwner: navOwner, onInputStateChange: onInputStateChange })) : (_jsx(ConfirmMode, { confirmState: confirmState, onConfirmYes: onConfirmYes, onConfirmCancel: onConfirmCancel, disabled: props.disabled })) }));
273
- }