loop-task 1.5.5 → 2.0.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.
- package/README.md +141 -21
- package/dist/board/App.js +1 -1
- package/dist/board/components/ActionButtons.js +1 -1
- package/dist/board/components/BoardButton.js +1 -1
- package/dist/board/components/ConfirmModal.js +1 -1
- package/dist/board/components/ContextHelpModal.js +1 -1
- package/dist/board/components/CreateForm.js +1 -1
- package/dist/board/components/CreateProjectModal.js +1 -1
- package/dist/board/components/DeleteProjectConfirm.js +1 -1
- package/dist/board/components/DetailView.js +1 -1
- package/dist/board/components/EditProjectModal.js +1 -1
- package/dist/board/components/FilterBar.js +1 -1
- package/dist/board/components/Footer.js +1 -1
- package/dist/board/components/Header.js +1 -1
- package/dist/board/components/HelpModal.js +1 -1
- package/dist/board/components/Inspector.js +1 -1
- package/dist/board/components/LogModal.js +1 -1
- package/dist/board/components/Navigator.js +1 -1
- package/dist/board/components/ProjectsModal.js +1 -1
- package/dist/board/components/ProjectsPage.js +1 -1
- package/dist/board/components/RunHistory.js +1 -1
- package/dist/board/components/SearchBox.js +1 -1
- package/dist/board/components/SearchSelect.js +1 -1
- package/dist/board/components/TaskBrowser.js +1 -1
- package/dist/board/components/TaskFilterBar.js +1 -1
- package/dist/board/components/TaskForm.js +1 -1
- package/dist/board/components/Timeline.js +1 -1
- package/dist/board/focus-context.js +1 -1
- package/dist/board/format.js +15 -6
- package/dist/board/index.js +1 -1
- package/dist/board/toast.js +1 -1
- package/dist/cli/import-validator.js +151 -0
- package/dist/cli/import-writer.js +77 -0
- package/dist/cli.js +111 -17
- package/dist/config/constants.js +52 -0
- package/dist/daemon/file-watcher.js +171 -0
- package/dist/daemon/http-server.js +605 -0
- package/dist/daemon/index.js +36 -6
- package/dist/daemon/manager.js +94 -13
- package/dist/daemon/projects.js +10 -0
- package/dist/daemon/server.js +21 -0
- package/dist/daemon/task-manager.js +7 -0
- package/dist/esm-loader.js +4 -1
- package/dist/hooks/useLoopFormValidation.js +140 -0
- package/dist/i18n/en.json +237 -17
- package/dist/shared/fs-utils.js +34 -1
- package/dist/tui/app.js +675 -151
- package/dist/tui/commands.js +114 -0
- package/dist/tui/components/ActionButtons.js +49 -0
- package/dist/tui/components/Button.js +18 -0
- package/dist/tui/components/ChainEditor.js +112 -0
- package/dist/tui/components/CommandBuilderField.js +143 -0
- package/dist/tui/components/CommandEditorModal.js +159 -0
- package/dist/tui/components/CommandInput.js +303 -0
- package/dist/tui/components/CommandsBrowserModal.js +77 -0
- package/dist/tui/components/ConfirmModal.js +9 -0
- package/dist/tui/components/ContextHelpModal.js +8 -0
- package/dist/tui/components/CreateForm.js +223 -0
- package/dist/tui/components/DebugPanel.js +8 -0
- package/dist/tui/components/ExportModal.js +37 -0
- package/dist/tui/components/FilterBar.js +13 -0
- package/dist/tui/components/FocusableButton.js +16 -0
- package/dist/tui/components/FocusableInput.js +69 -0
- package/dist/tui/components/FocusableList.js +46 -0
- package/dist/tui/components/FocusableSearchSelect.js +82 -0
- package/dist/tui/components/Footer.js +42 -0
- package/dist/tui/components/Header.js +40 -0
- package/dist/tui/components/HelpGuideModal.js +21 -0
- package/dist/tui/components/HelpModal.js +59 -0
- package/dist/tui/components/InlineCommandEditor.js +143 -0
- package/dist/tui/components/Inspector.js +31 -0
- package/dist/tui/components/LeftPanel.js +33 -0
- package/dist/tui/components/LogModal.js +121 -0
- package/dist/tui/components/Modal.js +11 -0
- package/dist/tui/components/Navigator.js +68 -0
- package/dist/tui/components/PatchEditForm.js +78 -0
- package/dist/tui/components/ProjectForm.js +64 -0
- package/dist/tui/components/ProjectsModal.js +40 -0
- package/dist/tui/components/ProjectsPage.js +99 -0
- package/dist/tui/components/RightPanel.js +34 -0
- package/dist/tui/components/RunHistory.js +108 -0
- package/dist/tui/components/SearchSelect.js +67 -0
- package/dist/tui/components/SelectModal.js +76 -0
- package/dist/tui/components/TabBar.js +20 -0
- package/dist/tui/components/TaskBrowser.js +96 -0
- package/dist/tui/components/TaskFilterBar.js +9 -0
- package/dist/tui/components/TaskForm.js +134 -0
- package/dist/tui/components/TaskPickerModal.js +60 -0
- package/dist/tui/components/Toast.js +37 -0
- package/dist/tui/components/WelcomeScreen.js +70 -0
- package/dist/tui/components/WizardForm.js +178 -0
- package/dist/tui/daemon.js +142 -0
- package/dist/tui/format.js +111 -0
- package/dist/tui/hooks/useBreakpoint.js +7 -0
- package/dist/tui/hooks/useHoverState.js +11 -0
- package/dist/tui/hooks/useLogStream.js +32 -0
- package/dist/tui/hooks/useLoopPolling.js +28 -0
- package/dist/tui/index.js +24 -0
- package/dist/tui/router.js +16 -0
- package/dist/tui/state.js +175 -0
- package/dist/tui/theme.js +96 -0
- package/dist/tui/types.js +1 -0
- package/dist/tui/utils/paste.js +10 -0
- package/dist/tui/utils/validation.js +56 -0
- 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 };
|