loop-task 1.5.5 → 2.0.1

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 (105) hide show
  1. package/README.md +127 -21
  2. package/dist/board/App.js +1 -1
  3. package/dist/board/components/ActionButtons.js +1 -1
  4. package/dist/board/components/BoardButton.js +1 -1
  5. package/dist/board/components/ConfirmModal.js +1 -1
  6. package/dist/board/components/ContextHelpModal.js +1 -1
  7. package/dist/board/components/CreateForm.js +1 -1
  8. package/dist/board/components/CreateProjectModal.js +1 -1
  9. package/dist/board/components/DeleteProjectConfirm.js +1 -1
  10. package/dist/board/components/DetailView.js +1 -1
  11. package/dist/board/components/EditProjectModal.js +1 -1
  12. package/dist/board/components/FilterBar.js +1 -1
  13. package/dist/board/components/Footer.js +1 -1
  14. package/dist/board/components/Header.js +1 -1
  15. package/dist/board/components/HelpModal.js +1 -1
  16. package/dist/board/components/Inspector.js +1 -1
  17. package/dist/board/components/LogModal.js +1 -1
  18. package/dist/board/components/Navigator.js +1 -1
  19. package/dist/board/components/ProjectsModal.js +1 -1
  20. package/dist/board/components/ProjectsPage.js +1 -1
  21. package/dist/board/components/RunHistory.js +1 -1
  22. package/dist/board/components/SearchBox.js +1 -1
  23. package/dist/board/components/SearchSelect.js +1 -1
  24. package/dist/board/components/TaskBrowser.js +1 -1
  25. package/dist/board/components/TaskFilterBar.js +1 -1
  26. package/dist/board/components/TaskForm.js +1 -1
  27. package/dist/board/components/Timeline.js +1 -1
  28. package/dist/board/focus-context.js +1 -1
  29. package/dist/board/format.js +15 -6
  30. package/dist/board/index.js +1 -1
  31. package/dist/board/toast.js +1 -1
  32. package/dist/cli/import-validator.js +151 -0
  33. package/dist/cli/import-writer.js +77 -0
  34. package/dist/cli.js +111 -17
  35. package/dist/config/constants.js +52 -0
  36. package/dist/daemon/file-watcher.js +171 -0
  37. package/dist/daemon/http-server.js +605 -0
  38. package/dist/daemon/index.js +36 -6
  39. package/dist/daemon/manager.js +94 -13
  40. package/dist/daemon/projects.js +10 -0
  41. package/dist/daemon/server.js +21 -0
  42. package/dist/daemon/task-manager.js +7 -0
  43. package/dist/esm-loader.js +4 -1
  44. package/dist/hooks/useLoopFormValidation.js +140 -0
  45. package/dist/i18n/en.json +237 -17
  46. package/dist/shared/fs-utils.js +34 -1
  47. package/dist/tui/app.js +675 -151
  48. package/dist/tui/commands.js +114 -0
  49. package/dist/tui/components/ActionButtons.js +49 -0
  50. package/dist/tui/components/Button.js +18 -0
  51. package/dist/tui/components/ChainEditor.js +112 -0
  52. package/dist/tui/components/CommandBuilderField.js +143 -0
  53. package/dist/tui/components/CommandEditorModal.js +159 -0
  54. package/dist/tui/components/CommandInput.js +303 -0
  55. package/dist/tui/components/CommandsBrowserModal.js +77 -0
  56. package/dist/tui/components/ConfirmModal.js +9 -0
  57. package/dist/tui/components/ContextHelpModal.js +8 -0
  58. package/dist/tui/components/CreateForm.js +223 -0
  59. package/dist/tui/components/DebugPanel.js +8 -0
  60. package/dist/tui/components/ExportModal.js +37 -0
  61. package/dist/tui/components/FilterBar.js +13 -0
  62. package/dist/tui/components/FocusableButton.js +16 -0
  63. package/dist/tui/components/FocusableInput.js +69 -0
  64. package/dist/tui/components/FocusableList.js +46 -0
  65. package/dist/tui/components/FocusableSearchSelect.js +82 -0
  66. package/dist/tui/components/Footer.js +42 -0
  67. package/dist/tui/components/Header.js +40 -0
  68. package/dist/tui/components/HelpGuideModal.js +21 -0
  69. package/dist/tui/components/HelpModal.js +59 -0
  70. package/dist/tui/components/InlineCommandEditor.js +143 -0
  71. package/dist/tui/components/Inspector.js +31 -0
  72. package/dist/tui/components/LeftPanel.js +33 -0
  73. package/dist/tui/components/LogModal.js +121 -0
  74. package/dist/tui/components/Modal.js +11 -0
  75. package/dist/tui/components/Navigator.js +68 -0
  76. package/dist/tui/components/PatchEditForm.js +78 -0
  77. package/dist/tui/components/ProjectForm.js +64 -0
  78. package/dist/tui/components/ProjectsModal.js +40 -0
  79. package/dist/tui/components/ProjectsPage.js +99 -0
  80. package/dist/tui/components/RightPanel.js +34 -0
  81. package/dist/tui/components/RunHistory.js +108 -0
  82. package/dist/tui/components/SearchSelect.js +67 -0
  83. package/dist/tui/components/SelectModal.js +76 -0
  84. package/dist/tui/components/TabBar.js +20 -0
  85. package/dist/tui/components/TaskBrowser.js +96 -0
  86. package/dist/tui/components/TaskFilterBar.js +9 -0
  87. package/dist/tui/components/TaskForm.js +134 -0
  88. package/dist/tui/components/TaskPickerModal.js +60 -0
  89. package/dist/tui/components/Toast.js +37 -0
  90. package/dist/tui/components/WelcomeScreen.js +70 -0
  91. package/dist/tui/components/WizardForm.js +178 -0
  92. package/dist/tui/daemon.js +142 -0
  93. package/dist/tui/format.js +111 -0
  94. package/dist/tui/hooks/useBreakpoint.js +7 -0
  95. package/dist/tui/hooks/useHoverState.js +11 -0
  96. package/dist/tui/hooks/useLogStream.js +32 -0
  97. package/dist/tui/hooks/useLoopPolling.js +28 -0
  98. package/dist/tui/index.js +24 -0
  99. package/dist/tui/router.js +16 -0
  100. package/dist/tui/state.js +175 -0
  101. package/dist/tui/theme.js +96 -0
  102. package/dist/tui/types.js +1 -0
  103. package/dist/tui/utils/paste.js +10 -0
  104. package/dist/tui/utils/validation.js +56 -0
  105. package/package.json +13 -8
@@ -0,0 +1,303 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { 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, CONFIRM_YES, CONFIRM_CANCEL, } from "../../config/constants.js";
9
+ import { sanitizePaste } from "../utils/paste.js";
10
+ export { sanitizePaste } from "../utils/paste.js";
11
+ // ── Rendered input with cursor ───────────────────────────────────────
12
+ function renderInputLine(value, cursorOffset) {
13
+ if (value.length === 0) {
14
+ return "\x1b[7m \x1b[27m";
15
+ }
16
+ let result = "";
17
+ for (let i = 0; i < value.length; i++) {
18
+ if (i === cursorOffset) {
19
+ result += "\x1b[7m" + value[i] + "\x1b[27m";
20
+ }
21
+ else {
22
+ result += value[i];
23
+ }
24
+ }
25
+ if (cursorOffset >= value.length) {
26
+ result += "\x1b[7m \x1b[27m";
27
+ }
28
+ return result;
29
+ }
30
+ function renderHighlightedLabel(label, matchRanges, isFocused) {
31
+ if (matchRanges.length === 0) {
32
+ return isFocused
33
+ ? `\x1b[38;2;251;191;36m${label}\x1b[39m`
34
+ : label;
35
+ }
36
+ let result = "";
37
+ let pos = 0;
38
+ for (const range of matchRanges) {
39
+ if (pos < range.start) {
40
+ const segment = label.slice(pos, range.start);
41
+ result += isFocused
42
+ ? `\x1b[38;2;251;191;36m${segment}\x1b[39m`
43
+ : segment;
44
+ }
45
+ const matched = label.slice(range.start, range.end);
46
+ result += `\x1b[1m\x1b[38;2;251;191;36m${matched}\x1b[39m\x1b[22m`;
47
+ pos = range.end;
48
+ }
49
+ if (pos < label.length) {
50
+ const segment = label.slice(pos);
51
+ result += isFocused
52
+ ? `\x1b[38;2;251;191;36m${segment}\x1b[39m`
53
+ : segment;
54
+ }
55
+ return result;
56
+ }
57
+ // ── Dropdown ─────────────────────────────────────────────────────────
58
+ function CommandDropdown({ state, rankedFiltered, }) {
59
+ if (!state.isOpen || state.isLoading || state.error)
60
+ return null;
61
+ const filtered = rankedFiltered;
62
+ const visibleOptions = filtered.slice(state.visibleFromIndex, state.visibleToIndex);
63
+ if (visibleOptions.length === 0) {
64
+ if (state.inputValue.length > 0) {
65
+ 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") }) }));
66
+ }
67
+ return null;
68
+ }
69
+ const aboveCount = state.visibleFromIndex;
70
+ const belowCount = filtered.length - state.visibleToIndex;
71
+ 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) => {
72
+ const actualIndex = state.visibleFromIndex + i;
73
+ const isFocused = actualIndex === state.focusedIndex;
74
+ const label = renderHighlightedLabel(match.option.label, match.matchRanges, isFocused);
75
+ 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));
76
+ }), belowCount > 0 && (_jsx(Text, { color: theme.text.muted, children: ` \u2193 ${belowCount} more` }))] }));
77
+ }
78
+ // ── Confirm inline options ────────────────────────────────────────────
79
+ function ConfirmInlineOptions({ focusedIndex, yesLabel, cancelLabel, }) {
80
+ // Options order: [cancel(0), yes(1)] in the autocomplete state,
81
+ // displayed inline as "❯ yes cancel" (yes first, cancel second).
82
+ const yesFocused = focusedIndex === 1;
83
+ const cancelFocused = focusedIndex === 0;
84
+ return (_jsxs(Box, { paddingLeft: 3, children: [yesFocused ? (_jsx(Text, { color: theme.text.inverse, backgroundColor: theme.bg.active, children: `\u276f ${yesLabel}` })) : (_jsx(Text, { color: theme.text.muted, children: ` ${yesLabel}` })), _jsx(Text, { color: theme.text.muted, children: " " }), cancelFocused ? (_jsx(Text, { color: theme.text.inverse, backgroundColor: theme.bg.active, children: `\u276f ${cancelLabel}` })) : (_jsx(Text, { color: theme.text.muted, children: ` ${cancelLabel}` }))] }));
85
+ }
86
+ // ── Hint bar ──────────────────────────────────────────────────────────
87
+ function HintBar({ leftHint, rightHint, }) {
88
+ return (_jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsx(Box, { children: leftHint }), _jsx(Box, { children: rightHint })] }));
89
+ }
90
+ function KeyHint({ keyLabel, action }) {
91
+ return (_jsxs(Box, { marginRight: 2, children: [_jsx(Text, { bold: true, color: theme.text.primary, children: keyLabel }), _jsx(Text, { color: theme.text.muted, children: " " + action })] }));
92
+ }
93
+ // ── Command mode ─────────────────────────────────────────────────────
94
+ function CommandMode({ context, onCommand, onCopy, onPanelAction, disabled, navOwner, onInputStateChange, }) {
95
+ const commands = useMemo(() => buildCommands(context), [context]);
96
+ const options = useMemo(() => commands.map((cmd) => ({ label: cmd.label, value: cmd.value })), [commands]);
97
+ const { state, dispatch } = useAutocompleteState({
98
+ options,
99
+ visibleOptionCount: COMMAND_INPUT_DROPDOWN_MAX_VISIBLE,
100
+ onSelect: onCommand,
101
+ });
102
+ // Re-rank filteredOptions: exact match → prefix match → fuzzy (existing order)
103
+ const rankedFiltered = useMemo(() => {
104
+ const fo = state.filteredOptions;
105
+ if (fo.length === 0 || state.inputValue.length === 0)
106
+ return fo;
107
+ const byValue = new Map();
108
+ for (const m of fo)
109
+ byValue.set(m.option.value, m);
110
+ const ranked = rankCommands(state.inputValue, fo.map((m) => ({ label: m.option.label, value: m.option.value })));
111
+ return ranked
112
+ .map((opt) => byValue.get(opt.value))
113
+ .filter((m) => m !== undefined);
114
+ }, [state.filteredOptions, state.inputValue]);
115
+ // Report input state changes to parent for InputOwner resolution (1.2)
116
+ useEffect(() => {
117
+ onInputStateChange?.(state.inputValue.length > 0, state.isOpen);
118
+ }, [state.inputValue, state.isOpen, onInputStateChange]);
119
+ const insertText = useCallback((text) => {
120
+ for (const ch of text)
121
+ dispatch({ type: "INSERT_TEXT", text: ch });
122
+ }, [dispatch]);
123
+ const clearInput = useCallback(() => {
124
+ dispatch({ type: "MOVE_CURSOR_END" });
125
+ for (let i = 0; i < state.inputValue.length; i++)
126
+ dispatch({ type: "DELETE_BACKWARD" });
127
+ }, [dispatch, state.inputValue.length]);
128
+ useInput((input, key) => {
129
+ // Bracketed paste: content wrapped in ESC[200~ ... ESC[201~ arrives as one
130
+ // chunk. Detect first — the leading ESC can otherwise trip the ctrl/escape guards.
131
+ if (input.includes("\x1b[200~")) {
132
+ insertText(sanitizePaste(input));
133
+ return;
134
+ }
135
+ // Ctrl+U clears the line (the "select all + delete" gesture). Handle before
136
+ // the ctrl guard below. Some terminals send it as the raw NAK control char.
137
+ if ((key.ctrl && input === "u") || input === "\x15") {
138
+ clearInput();
139
+ return;
140
+ }
141
+ if (key.ctrl)
142
+ return;
143
+ // Multi-char containing CR/LF with no bracketed markers = the VS Code
144
+ // Ctrl+Enter escape sequence, handled by App's global useInput. Ignore here.
145
+ if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
146
+ return;
147
+ // When the command bar is empty and no dropdown is open, panels own
148
+ // navigation keys — return early so j/k/arrows reach the panel layer.
149
+ if (navOwner === "panel" && state.inputValue.length === 0 && !state.isOpen) {
150
+ if (input === "j" || input === "k" || key.upArrow || key.downArrow) {
151
+ return; // panels own these keys
152
+ }
153
+ }
154
+ // `c` with no modifiers + no open dropdown = contextual copy shortcut
155
+ if (onCopy && input === "c" && !state.isOpen) {
156
+ onCopy();
157
+ return;
158
+ }
159
+ if (key.escape) {
160
+ dispatch({ type: "CLOSE" });
161
+ if (state.inputValue.length > 0) {
162
+ clearInput();
163
+ }
164
+ return;
165
+ }
166
+ if (key.return) {
167
+ if (state.isOpen && rankedFiltered.length > 0 && state.focusedIndex < rankedFiltered.length) {
168
+ const focused = rankedFiltered[state.focusedIndex];
169
+ dispatch({ type: "CLOSE" });
170
+ for (let i = 0; i <= state.inputValue.length; i++) {
171
+ dispatch({ type: "DELETE_BACKWARD" });
172
+ }
173
+ onCommand(focused.option.value);
174
+ }
175
+ else if (state.inputValue.length === 0) {
176
+ // Empty command bar: Enter triggers the focused panel's contextual
177
+ // action (edit / logs) for terminals that collapse Ctrl+Enter to Enter.
178
+ onPanelAction?.();
179
+ }
180
+ return;
181
+ }
182
+ if (key.tab) {
183
+ dispatch({ type: "ACCEPT" });
184
+ return;
185
+ }
186
+ if (key.downArrow) {
187
+ dispatch({ type: "FOCUS_NEXT" });
188
+ return;
189
+ }
190
+ if (key.upArrow) {
191
+ dispatch({ type: "FOCUS_PREV" });
192
+ return;
193
+ }
194
+ if (key.backspace || key.delete) {
195
+ dispatch({ type: "DELETE_BACKWARD" });
196
+ return;
197
+ }
198
+ if (key.leftArrow) {
199
+ dispatch({ type: "MOVE_CURSOR_LEFT" });
200
+ return;
201
+ }
202
+ if (key.rightArrow) {
203
+ dispatch({ type: "MOVE_CURSOR_RIGHT" });
204
+ return;
205
+ }
206
+ // Multi-char printable input = an unbracketed single-line paste (e.g. right-click).
207
+ if (input.length > 1 && !key.meta) {
208
+ insertText(sanitizePaste(input));
209
+ return;
210
+ }
211
+ if (input.length === 1 && !key.ctrl && !key.meta && !key.return && !key.tab && !key.escape && input >= " " && input <= "~") {
212
+ dispatch({ type: "INSERT_TEXT", text: input });
213
+ }
214
+ }, { isActive: !disabled });
215
+ const isEmpty = state.inputValue.length === 0;
216
+ const cursor = "\x1b[7m \x1b[27m";
217
+ const inputContent = isEmpty ? cursor : renderInputLine(state.inputValue, state.cursorOffset);
218
+ 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" })] }) })] }));
219
+ }
220
+ // ── Confirm mode ─────────────────────────────────────────────────────
221
+ function ConfirmMode({ confirmState, onConfirmYes, onConfirmCancel, disabled, }) {
222
+ const yesLabel = t("cmdInput.confirmYes");
223
+ const cancelLabel = t("cmdInput.confirmCancel");
224
+ const options = useMemo(() => [
225
+ { label: cancelLabel, value: CONFIRM_CANCEL },
226
+ { label: yesLabel, value: CONFIRM_YES },
227
+ ], [cancelLabel, yesLabel]);
228
+ const handleSelect = useCallback((value) => {
229
+ if (value === CONFIRM_YES) {
230
+ onConfirmYes();
231
+ }
232
+ else {
233
+ onConfirmCancel();
234
+ }
235
+ }, [onConfirmYes, onConfirmCancel]);
236
+ const { state, dispatch } = useAutocompleteState({
237
+ options,
238
+ visibleOptionCount: 2,
239
+ onSelect: handleSelect,
240
+ });
241
+ useInput((_input, key) => {
242
+ if (key.ctrl)
243
+ return;
244
+ if (_input.length > 1 && (_input.includes("\r") || _input.includes("\n")))
245
+ return;
246
+ if (key.escape) {
247
+ onConfirmCancel();
248
+ return;
249
+ }
250
+ if (key.return) {
251
+ if (state.isOpen && state.filteredOptions.length > 0 && state.focusedIndex < state.filteredOptions.length) {
252
+ const focused = state.filteredOptions[state.focusedIndex];
253
+ dispatch({ type: "SELECT", value: focused.option.value, label: focused.option.label });
254
+ }
255
+ else {
256
+ onConfirmCancel();
257
+ }
258
+ return;
259
+ }
260
+ if (key.downArrow) {
261
+ dispatch({ type: "FOCUS_NEXT" });
262
+ return;
263
+ }
264
+ if (key.upArrow) {
265
+ dispatch({ type: "FOCUS_PREV" });
266
+ return;
267
+ }
268
+ }, { isActive: !disabled });
269
+ return (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.semantic.danger, children: "│ " }), _jsx(Text, { color: theme.text.primary, children: confirmState.prompt })] }), _jsx(ConfirmInlineOptions, { focusedIndex: state.focusedIndex, yesLabel: yesLabel, cancelLabel: cancelLabel }), _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" }) })] }));
270
+ }
271
+ // ── Search mode ──────────────────────────────────────────────────────
272
+ function SearchMode({ value, onSearchChange, onSearchSubmit, onSearchCancel, disabled, }) {
273
+ useInput((input, key) => {
274
+ if (key.ctrl)
275
+ return;
276
+ if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
277
+ return;
278
+ if (key.escape) {
279
+ onSearchCancel();
280
+ return;
281
+ }
282
+ if (key.return) {
283
+ onSearchSubmit();
284
+ return;
285
+ }
286
+ if (key.backspace || key.delete) {
287
+ onSearchChange(value.slice(0, -1));
288
+ return;
289
+ }
290
+ if (input && !key.ctrl && !key.meta && input.length === 1) {
291
+ onSearchChange(value + input);
292
+ return;
293
+ }
294
+ }, { isActive: !disabled });
295
+ const placeholder = t("cmdInput.searchPlaceholder");
296
+ const cursor = "\x1b[7m \x1b[27m";
297
+ 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" }) })] }));
298
+ }
299
+ // ── Main component ───────────────────────────────────────────────────
300
+ export function CommandInput(props) {
301
+ const { context, onCommand, confirmState, searchState, searchValue, onSearchChange, onSearchSubmit, onSearchCancel, onConfirmYes, onConfirmCancel, onCopy, onPanelAction, navOwner, onInputStateChange, } = props;
302
+ 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 })) }));
303
+ }
@@ -0,0 +1,77 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, 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 { buildTabCommands } from "../commands.js";
7
+ const CATEGORY_ORDER = ["global", "filters", "loop", "task", "project"];
8
+ function categoryLabel(cat) {
9
+ switch (cat) {
10
+ case "global": return t("cmdsBrowser.groupGlobal");
11
+ case "filters": return t("cmdsBrowser.groupFilters");
12
+ case "loop": return t("cmdsBrowser.groupLoop");
13
+ case "task": return t("cmdsBrowser.groupTask");
14
+ case "project": return t("cmdsBrowser.groupProject");
15
+ }
16
+ }
17
+ const MAX_VISIBLE = 16;
18
+ export function CommandsBrowserModal(props) {
19
+ const allCommands = useMemo(() => buildTabCommands(props.context), [props.context]);
20
+ const flatItems = useMemo(() => {
21
+ const items = [];
22
+ for (const cat of CATEGORY_ORDER) {
23
+ const cmds = allCommands.filter((c) => c.category === cat).sort((a, b) => a.label.localeCompare(b.label));
24
+ if (cmds.length === 0)
25
+ continue;
26
+ items.push({ command: { label: categoryLabel(cat), value: `__header_${cat}`, hint: "", tier: "global", category: cat }, isHeader: true, category: cat });
27
+ for (const cmd of cmds) {
28
+ items.push({ command: cmd, isHeader: false, category: cat });
29
+ }
30
+ }
31
+ return items;
32
+ }, [allCommands]);
33
+ const selectableIndices = useMemo(() => {
34
+ return flatItems
35
+ .map((item, i) => ({ item, i }))
36
+ .filter(({ item }) => !item.isHeader)
37
+ .map(({ i }) => i);
38
+ }, [flatItems]);
39
+ const [cursor, setCursor] = useState(0);
40
+ const currentFlatIdx = selectableIndices[cursor] ?? 0;
41
+ useInput((input, key) => {
42
+ if (key.escape) {
43
+ props.onClose();
44
+ return;
45
+ }
46
+ if (key.upArrow) {
47
+ setCursor((prev) => prev <= 0 ? Math.max(0, selectableIndices.length - 1) : prev - 1);
48
+ return;
49
+ }
50
+ if (key.downArrow) {
51
+ setCursor((prev) => prev >= selectableIndices.length - 1 ? 0 : prev + 1);
52
+ return;
53
+ }
54
+ if (key.return) {
55
+ const flatIdx = selectableIndices[cursor];
56
+ if (flatIdx !== undefined) {
57
+ const item = flatItems[flatIdx];
58
+ if (item && !item.isHeader) {
59
+ props.onExecute(item.command.value);
60
+ }
61
+ }
62
+ return;
63
+ }
64
+ });
65
+ const windowStart = Math.max(0, currentFlatIdx - Math.floor(MAX_VISIBLE / 2));
66
+ const windowEnd = Math.min(flatItems.length, windowStart + MAX_VISIBLE);
67
+ const visibleItems = flatItems.slice(windowStart, windowEnd);
68
+ return (_jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: _jsxs(Box, { width: 56, flexDirection: "column", backgroundColor: theme.bg.elevated, borderStyle: "round", borderColor: theme.border.dim, paddingX: 1, paddingY: 0, children: [_jsxs(Box, { justifyContent: "space-between", paddingY: 0, children: [_jsx(Text, { bold: true, color: theme.text.primary, children: t("cmdsBrowser.title") }), _jsx(Text, { color: theme.text.muted, children: "esc" })] }), _jsx(Box, { flexDirection: "column", children: visibleItems.map((item, visIdx) => {
69
+ const realIdx = windowStart + visIdx;
70
+ const isSelected = realIdx === currentFlatIdx;
71
+ if (item.isHeader) {
72
+ return (_jsx(Box, { marginTop: realIdx === 0 ? 0 : 1, children: _jsx(Text, { color: theme.accent.brand, bold: true, children: item.command.label }) }, item.command.value));
73
+ }
74
+ const shortcut = item.command.shortcut ?? "";
75
+ return (_jsxs(Box, { backgroundColor: isSelected ? theme.bg.active : undefined, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.primary, children: " " + item.command.label }), item.command.hint.length > 0 ? (_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.muted, children: " " + item.command.hint.slice(0, 20) })) : null] }), shortcut.length > 0 ? (_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.muted, children: shortcut })) : null] }, `${item.command.value}-${realIdx}`));
76
+ }) }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { color: theme.text.muted, children: t("cmdsBrowser.hint") }) })] }) }));
77
+ }
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box } from "ink";
3
+ import { darkTheme as theme } from "../theme.js";
4
+ import { Modal } from "./Modal.js";
5
+ import { FocusableButton } from "./FocusableButton.js";
6
+ import { t } from "../../i18n/index.js";
7
+ export function ConfirmModal(props) {
8
+ return (_jsx(Modal, { title: props.message, onClose: props.onNo, width: "50%", children: _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(FocusableButton, { label: t("board.yes"), color: theme.accent.focus, onPress: props.onYes }), _jsx(FocusableButton, { label: t("board.no"), color: theme.semantic.danger, onPress: props.onNo, variant: "danger" })] }) }));
9
+ }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { darkTheme as theme } from "../theme.js";
4
+ import { Modal } from "./Modal.js";
5
+ import { t } from "../../i18n/index.js";
6
+ export function ContextHelpModal(props) {
7
+ return (_jsxs(Modal, { title: t("context.helpTitle"), onClose: props.onClose, width: 64, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.text.primary, children: t("context.helpRules") }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.accent.brand, children: t("context.helpTemplates") }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("context.helpCaveat") }) }), _jsx(Box, { children: _jsx(Text, { color: theme.text.secondary, children: t("context.helpJqTip") }) })] }));
8
+ }
@@ -0,0 +1,223 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useCallback, useState, useRef } from "react";
3
+ import { createLoop, updateLoop } from "../daemon.js";
4
+ import { t } from "../../i18n/index.js";
5
+ import { WizardForm } from "./WizardForm.js";
6
+ import { TaskPickerModal } from "./TaskPickerModal.js";
7
+ import { SelectModal, SelectValueField } from "./SelectModal.js";
8
+ import { InlineCommandEditor } from "./InlineCommandEditor.js";
9
+ import { parseDuration } from "../../duration.js";
10
+ import { parseCommandLine } from "../../loop-config.js";
11
+ // ── Component ───────────────────────────────────────────────────────
12
+ export function CreateView(props) {
13
+ const { mode, editId, initial, selectedTaskId, selectedTaskName, tasks, currentProjectId, onCancel, onDone, onChooseTask, } = props;
14
+ const [taskPickerOpen, setTaskPickerOpen] = useState(false);
15
+ const [openSelect, setOpenSelect] = useState(null);
16
+ const [commandValue, setCommandValue] = useState(initial.command ?? "");
17
+ // renderCustom is invoked on every WizardForm render for every field, so this
18
+ // ref always holds the latest onChange/onAdvance closures per field key —
19
+ // letting the modal (rendered outside WizardForm) drive a field's value.
20
+ const fieldCallbacksRef = useRef({});
21
+ const taskModeInitial = initial.taskMode === "existing" ? "Existing task" : "Inline command";
22
+ // Resolve task display name on edit mode: if the user hasn't picked a task
23
+ // via the picker (selectedTaskName is null), look it up from the tasks list.
24
+ // Always format as "<name> (<short id>)" for consistency.
25
+ const resolvedTaskName = useMemo(() => {
26
+ const tid = selectedTaskId ?? initial.taskId;
27
+ if (!tid)
28
+ return null;
29
+ const displayName = selectedTaskName ?? tasks.find((t) => t.id === tid)?.name;
30
+ if (displayName)
31
+ return `${displayName} (${tid.slice(0, 8)})`;
32
+ return `${tid.slice(0, 8)}`;
33
+ }, [selectedTaskName, selectedTaskId, initial.taskId, tasks]);
34
+ const steps = useMemo(() => {
35
+ const list = [
36
+ {
37
+ key: "interval",
38
+ prompt: t("wizard.intervalPrompt"),
39
+ hint: t("wizard.intervalHint"),
40
+ required: true,
41
+ inputType: "text",
42
+ defaultValue: initial.interval ?? undefined,
43
+ },
44
+ {
45
+ key: "taskMode",
46
+ prompt: t("wizard.taskModePrompt"),
47
+ hint: t("board.hintTaskMode"),
48
+ required: true,
49
+ defaultValue: taskModeInitial,
50
+ onActivate: () => setOpenSelect("taskMode"),
51
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
52
+ fieldCallbacksRef.current.taskMode = { value, onChange, onAdvance };
53
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.taskModePrompt"), isActive: isActive }));
54
+ },
55
+ },
56
+ {
57
+ key: "taskId",
58
+ prompt: resolvedTaskName
59
+ ? t("board.selectedTask", { name: resolvedTaskName })
60
+ : t("board.chooseTask"),
61
+ hint: t("board.hintTask"),
62
+ required: true,
63
+ inputType: "text",
64
+ defaultValue: initial.taskId ?? undefined,
65
+ skip: (values) => !values.taskMode?.includes("Existing"),
66
+ onActivate: () => setTaskPickerOpen(true),
67
+ renderCustom: ({ isActive }) => (_jsx(SelectValueField, { label: resolvedTaskName, placeholder: t("board.chooseTask"), isActive: isActive })),
68
+ },
69
+ {
70
+ key: "command",
71
+ prompt: t("wizard.commandPrompt"),
72
+ hint: t("wizard.commandHint"),
73
+ required: true,
74
+ inputType: "text",
75
+ defaultValue: commandValue || undefined,
76
+ skip: (values) => !!values.taskMode?.includes("Existing"),
77
+ // No onActivate — inline editor, owned directly by renderCustom
78
+ renderCustom: ({ isActive, onChange }) => (_jsx(InlineCommandEditor, { value: commandValue, hint: t("wizard.commandHint"), isActive: isActive, onChange: (v) => {
79
+ setCommandValue(v);
80
+ onChange(v);
81
+ } })),
82
+ },
83
+ {
84
+ key: "runNow",
85
+ prompt: t("wizard.runNowPrompt"),
86
+ hint: t("board.hintRunNow"),
87
+ required: true,
88
+ defaultValue: initial.runNow === "true" || initial.runNow === "yes"
89
+ ? t("wizard.runNowNow")
90
+ : t("wizard.runNowWait"),
91
+ onActivate: () => setOpenSelect("runNow"),
92
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
93
+ fieldCallbacksRef.current.runNow = { value, onChange, onAdvance };
94
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.runNowPrompt"), isActive: isActive }));
95
+ },
96
+ },
97
+ {
98
+ key: "cwd",
99
+ prompt: t("wizard.cwdPrompt"),
100
+ hint: t("wizard.cwdHint"),
101
+ required: false,
102
+ inputType: "text",
103
+ defaultValue: initial.cwd ?? undefined,
104
+ },
105
+ {
106
+ key: "description",
107
+ prompt: t("wizard.descriptionPrompt"),
108
+ hint: t("wizard.descriptionHint"),
109
+ required: false,
110
+ inputType: "text",
111
+ defaultValue: initial.description ?? undefined,
112
+ },
113
+ {
114
+ key: "maxRuns",
115
+ prompt: t("wizard.maxRunsPrompt"),
116
+ hint: t("wizard.maxRunsHint"),
117
+ required: false,
118
+ inputType: "text",
119
+ defaultValue: initial.maxRuns ?? undefined,
120
+ },
121
+ {
122
+ key: "project",
123
+ prompt: t("wizard.projectPrompt"),
124
+ hint: t("wizard.projectHint"),
125
+ required: false,
126
+ defaultValue: props.projects.find((p) => p.id === (initial.project ?? "default"))?.name ?? props.projects[0]?.name,
127
+ onActivate: () => setOpenSelect("project"),
128
+ renderCustom: ({ value, isActive, onChange, onAdvance }) => {
129
+ fieldCallbacksRef.current.project = { value, onChange, onAdvance };
130
+ return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.projectPrompt"), isActive: isActive }));
131
+ },
132
+ },
133
+ ];
134
+ return list;
135
+ }, [taskModeInitial, selectedTaskId, resolvedTaskName, initial, commandValue, props.projects]);
136
+ const handleComplete = useCallback((values) => {
137
+ const intervalInput = values.interval ?? "";
138
+ if (!intervalInput.trim())
139
+ return;
140
+ let interval;
141
+ try {
142
+ interval = parseDuration(intervalInput.trim());
143
+ }
144
+ catch {
145
+ return;
146
+ }
147
+ const intervalHuman = intervalInput.trim();
148
+ const isExistingTask = !!values.taskMode?.includes("Existing");
149
+ if (isExistingTask && !selectedTaskId && !values.taskId?.trim())
150
+ return;
151
+ const cmdValue = values.command ?? commandValue;
152
+ if (!isExistingTask && !cmdValue.trim())
153
+ return;
154
+ const cmd = isExistingTask
155
+ ? ""
156
+ : cmdValue.split("\n").map((l) => l.trim()).filter(Boolean).join(" ");
157
+ let cmdOnly = "";
158
+ let args = [];
159
+ if (cmd.trim()) {
160
+ try {
161
+ const tokens = parseCommandLine(cmd.trim());
162
+ cmdOnly = tokens[0] ?? "";
163
+ args = tokens.slice(1);
164
+ }
165
+ catch {
166
+ return;
167
+ }
168
+ }
169
+ const runNowValue = values.runNow === t("wizard.runNowNow");
170
+ const projectName = values.project ?? "";
171
+ const project = props.projects.find((p) => p.name === projectName);
172
+ const projectId = project?.id ?? currentProjectId;
173
+ const options = {
174
+ interval,
175
+ taskId: isExistingTask
176
+ ? (selectedTaskId ?? values.taskId?.trim() ?? null)
177
+ : null,
178
+ command: cmdOnly,
179
+ commandArgs: args,
180
+ commandRaw: isExistingTask ? undefined : cmdValue,
181
+ cwd: (values.cwd ?? "").trim() || process.cwd(),
182
+ immediate: runNowValue,
183
+ maxRuns: (values.maxRuns ?? "").trim()
184
+ ? parseInt(values.maxRuns, 10)
185
+ : null,
186
+ verbose: false,
187
+ description: (values.description ?? "").trim(),
188
+ projectId,
189
+ offset: null,
190
+ };
191
+ const desc = (values.description ?? "").trim() || [cmdOnly, ...args].join(" ").trim();
192
+ if (mode === "edit" && editId) {
193
+ updateLoop(editId, options, intervalHuman)
194
+ .then((id) => onDone(true, id, desc))
195
+ .catch(() => { });
196
+ }
197
+ else {
198
+ createLoop(options, intervalHuman)
199
+ .then((id) => onDone(false, id, desc))
200
+ .catch(() => { });
201
+ }
202
+ }, [selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, props.projects]);
203
+ const selectOptionsFor = useCallback((field) => {
204
+ if (field === "taskMode") {
205
+ return [t("wizard.taskModeInline"), t("wizard.taskModeExisting")].map((v) => ({ value: v, label: v }));
206
+ }
207
+ if (field === "runNow") {
208
+ return [t("wizard.runNowWait"), t("wizard.runNowNow")].map((v) => ({ value: v, label: v }));
209
+ }
210
+ return props.projects.map((p) => ({ value: p.name, label: p.name }));
211
+ }, [props.projects]);
212
+ const selectTitleFor = (field) => field === "taskMode" ? t("wizard.taskModePrompt")
213
+ : field === "runNow" ? t("wizard.runNowPrompt")
214
+ : t("wizard.projectPrompt");
215
+ return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: mode === "edit" ? t("wizard.editLoop") : t("wizard.newLoop"), steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: taskPickerOpen || openSelect !== null }), taskPickerOpen ? (_jsx(TaskPickerModal, { tasks: tasks, onSelect: (task) => {
216
+ onChooseTask({ id: task.id, name: task.name });
217
+ setTaskPickerOpen(false);
218
+ }, onClose: () => setTaskPickerOpen(false) })) : null, openSelect ? (_jsx(SelectModal, { title: selectTitleFor(openSelect), options: selectOptionsFor(openSelect), initialValue: fieldCallbacksRef.current[openSelect]?.value, onSelect: (option) => {
219
+ fieldCallbacksRef.current[openSelect]?.onChange(option.value);
220
+ fieldCallbacksRef.current[openSelect]?.onAdvance();
221
+ setOpenSelect(null);
222
+ }, onClose: () => setOpenSelect(null) })) : null] }));
223
+ }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { darkTheme as theme } from "../theme.js";
4
+ const MAX_ENTRIES = 12;
5
+ export function DebugPanel(props) {
6
+ return (_jsxs(Box, { flexDirection: "column", width: "22%", flexShrink: 0, borderStyle: "single", borderColor: theme.semantic.warning, backgroundColor: theme.bg.surface, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.semantic.warning, bold: true, children: "DEBUG" }) }), _jsx(Box, { paddingLeft: 1, flexDirection: "column", children: props.entries.length === 0 ? (_jsx(Text, { color: theme.text.muted, children: "press keys..." })) : (props.entries.map((e) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.text.primary, children: ["len=", e.len, " codes=[", e.codes, "]"] }), _jsxs(Text, { color: theme.text.muted, children: ["c=", e.ctrl ? 1 : 0, " r=", e.return ? 1 : 0, " t=", e.tab ? 1 : 0, " e=", e.escape ? 1 : 0] })] }, e.id)))) })] }));
7
+ }
8
+ export { MAX_ENTRIES };