loop-task 2.0.16 → 2.1.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.
- package/dist/config/constants.js +2 -12
- package/dist/core/loop-controller.js +35 -5
- package/dist/daemon/http-server.js +0 -6
- package/dist/hooks/useLoopFormValidation.js +0 -9
- package/dist/i18n/en.json +8 -1
- package/dist/tui/App.js +0 -15
- package/dist/tui/commands.js +0 -4
- package/dist/tui/components/CodeEditorModal.js +21 -14
- package/dist/tui/components/CommandInput.js +0 -8
- package/dist/tui/components/CreateForm.js +0 -1
- package/dist/tui/components/TaskForm.js +50 -12
- package/dist/tui/utils/paste.js +11 -5
- package/package.json +1 -1
package/dist/config/constants.js
CHANGED
|
@@ -13,7 +13,6 @@ export const LOG_TAIL_DEFAULT = 50;
|
|
|
13
13
|
export const BOARD_BREAKPOINT_WIDTH = 80;
|
|
14
14
|
export const HEADER_COMPACT_WIDTH = 60;
|
|
15
15
|
export const SEARCH_SELECT_HEIGHT = 6;
|
|
16
|
-
// ── Clipboard / bracketed paste ─────────────────────────────────────
|
|
17
16
|
// DECSET 2004: terminal wraps pasted text in ESC[200~ ... ESC[201~ so the
|
|
18
17
|
// app can insert it wholesale instead of processing each char as a keypress.
|
|
19
18
|
export const BRACKETED_PASTE_ENABLE = "\x1b[?2004h";
|
|
@@ -35,30 +34,24 @@ export const ENTITY_COLORS = {
|
|
|
35
34
|
task: "#a78bfa", // purple
|
|
36
35
|
project: "#34d399", // green
|
|
37
36
|
};
|
|
38
|
-
// ── Wizard step counts ──────────────────────────────────────────────
|
|
39
37
|
export const WIZARD_LOOP_REQUIRED_STEPS = 3;
|
|
40
38
|
export const WIZARD_LOOP_TOTAL_STEPS = 7;
|
|
41
39
|
export const WIZARD_TASK_REQUIRED_STEPS = 2;
|
|
42
40
|
export const WIZARD_TASK_TOTAL_STEPS = 4;
|
|
43
41
|
export const WIZARD_PROJECT_REQUIRED_STEPS = 1;
|
|
44
42
|
export const WIZARD_PROJECT_TOTAL_STEPS = 2;
|
|
45
|
-
// ── Command tiers ───────────────────────────────────────────────────
|
|
46
43
|
export const COMMAND_TIER_ACTION = "action";
|
|
47
44
|
export const COMMAND_TIER_CONFIRM = "confirm";
|
|
48
45
|
export const COMMAND_TIER_GLOBAL = "global";
|
|
49
|
-
// ── Command categories ─────────────────────────────────────────────
|
|
50
46
|
export const COMMAND_CATEGORY_GLOBAL = "global";
|
|
51
47
|
export const COMMAND_CATEGORY_FILTERS = "filters";
|
|
52
48
|
export const COMMAND_CATEGORY_LOOP = "loop";
|
|
53
49
|
export const COMMAND_CATEGORY_TASK = "task";
|
|
54
50
|
export const COMMAND_CATEGORY_PROJECT = "project";
|
|
55
|
-
// ── Confirm keywords ────────────────────────────────────────────────
|
|
56
51
|
export const CONFIRM_YES = "yes";
|
|
57
52
|
export const CONFIRM_CANCEL = "cancel";
|
|
58
|
-
// ── Panel focus types ───────────────────────────────────────────────
|
|
59
53
|
export const PANEL_LEFT = "left";
|
|
60
54
|
export const PANEL_RIGHT = "right";
|
|
61
|
-
// ── Command builder templates ──────────────────────────────────────
|
|
62
55
|
export const COMMAND_TEMPLATES = [
|
|
63
56
|
{ label: "npm run", command: "npm", args: "run" },
|
|
64
57
|
{ label: "npm test", command: "npm", args: "test" },
|
|
@@ -71,19 +64,16 @@ export const COMMAND_TEMPLATES = [
|
|
|
71
64
|
{ label: "make", command: "make", args: "" },
|
|
72
65
|
{ label: "shell script", command: "bash", args: "./script.sh" },
|
|
73
66
|
];
|
|
74
|
-
// ── Command input constants ─────────────────────────────────────────
|
|
75
67
|
export const COMMAND_INPUT_HEIGHT = 6;
|
|
76
68
|
export const COMMAND_INPUT_DROPDOWN_MAX_VISIBLE = 6;
|
|
77
|
-
// ── HTTP API server ─────────────────────────────────────────────────
|
|
78
69
|
export const HTTP_API_PORT = 8845;
|
|
79
70
|
export const HTTP_API_HOST = "127.0.0.1";
|
|
80
|
-
// ── Ctrl shortcut hints (for display) ───────────────────────────────
|
|
81
71
|
export const EXPORT_MAX_PREVIEW_LINES = 200;
|
|
82
72
|
export const CTRL_SHORTCUT_EDIT = "Ctrl+E";
|
|
83
73
|
export const CTRL_SHORTCUT_DELETE = "Ctrl+D";
|
|
84
|
-
// ── Code editor constants ──────────────────────────────────────────
|
|
85
74
|
export const CODE_EDITOR_MAX_VISIBLE = 2;
|
|
86
|
-
export const CODE_EDITOR_MODAL_HEIGHT =
|
|
75
|
+
export const CODE_EDITOR_MODAL_HEIGHT = 40;
|
|
76
|
+
export const CODE_EDITOR_MODAL_WIDTH = 120;
|
|
87
77
|
export const CODE_EDITOR_UNDO_LIMIT = 50;
|
|
88
78
|
export const CODE_EDITOR_SYNTAX_COLORS = {
|
|
89
79
|
flag: "#38bdf8",
|
|
@@ -330,19 +330,49 @@ export class LoopController extends EventEmitter {
|
|
|
330
330
|
});
|
|
331
331
|
this.runAbortController = new AbortController();
|
|
332
332
|
const task = this.options.taskId ? this.taskResolver(this.options.taskId) : null;
|
|
333
|
-
const command = task?.command ?? this.options.command;
|
|
334
|
-
const commandArgs = task?.commandArgs ?? this.options.commandArgs;
|
|
335
333
|
const cwd = resolveEffectiveCwd(this.options.cwd, this.projectDirectory);
|
|
336
334
|
const chainContext = {};
|
|
337
335
|
const hasChainTasks = !!(task?.onSuccessTaskId || task?.onFailureTaskId);
|
|
338
|
-
const
|
|
336
|
+
const singleCommandFallback = {
|
|
337
|
+
command: task?.command ?? this.options.command,
|
|
338
|
+
commandArgs: task?.commandArgs ?? this.options.commandArgs,
|
|
339
|
+
commandRaw: task?.commandRaw ?? this.options.commandRaw,
|
|
340
|
+
};
|
|
341
|
+
const taskSteps = task?.steps?.length
|
|
342
|
+
? task.steps
|
|
343
|
+
: [{ commands: [singleCommandFallback] }];
|
|
344
|
+
const shouldCaptureStdout = hasChainTasks || taskSteps.length > 1 || taskSteps[0].commands.length > 1;
|
|
345
|
+
let exitCode = 0;
|
|
346
|
+
let totalDuration = 0;
|
|
347
|
+
let combinedStdout = "";
|
|
348
|
+
for (const step of taskSteps) {
|
|
349
|
+
const stepResults = await Promise.allSettled(step.commands.map((cmd) => executeCommand(interpolate(cmd.command, chainContext), cmd.commandArgs.map(a => interpolate(a, chainContext)), cwd, this.logStream, AbortSignal.any([signal, this.runAbortController.signal]), this.runCount, shouldCaptureStdout)));
|
|
350
|
+
for (const r of stepResults) {
|
|
351
|
+
if (r.status === "fulfilled") {
|
|
352
|
+
totalDuration += r.value.duration;
|
|
353
|
+
if (r.value.stdout)
|
|
354
|
+
combinedStdout += (combinedStdout ? "\n" : "") + r.value.stdout;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const stepFailure = stepResults.some((r) => r.status === "rejected" || (r.status === "fulfilled" && r.value.exitCode !== 0));
|
|
358
|
+
if (stepFailure) {
|
|
359
|
+
const failed = stepResults.find((r) => r.status === "fulfilled" && r.value.exitCode !== 0);
|
|
360
|
+
exitCode = failed ? failed.value.exitCode : 1;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
339
364
|
this.runAbortController = null;
|
|
340
|
-
if (
|
|
341
|
-
const parsed = parseStdout(
|
|
365
|
+
if (shouldCaptureStdout && combinedStdout) {
|
|
366
|
+
const parsed = parseStdout(combinedStdout);
|
|
342
367
|
if (parsed !== null) {
|
|
343
368
|
Object.assign(chainContext, parsed);
|
|
344
369
|
}
|
|
345
370
|
}
|
|
371
|
+
const result = {
|
|
372
|
+
exitCode,
|
|
373
|
+
duration: totalDuration,
|
|
374
|
+
stdout: combinedStdout || undefined,
|
|
375
|
+
};
|
|
346
376
|
this.lastExitCode = result.exitCode;
|
|
347
377
|
this.lastDuration = result.duration;
|
|
348
378
|
const logSize = fs.existsSync(this.logPath) ? fs.statSync(this.logPath).size - this.currentRunStartOffset : 0;
|
|
@@ -139,7 +139,6 @@ export class HttpApiServer {
|
|
|
139
139
|
const segments = path.split("/").filter((s) => s.length > 0);
|
|
140
140
|
this.routes.push({ method, segments, handler });
|
|
141
141
|
};
|
|
142
|
-
// ── Loops ──────────────────────────────────────────────────────
|
|
143
142
|
r("GET", "/api/loops", (_req, res) => {
|
|
144
143
|
sendOk(res, this.manager.list());
|
|
145
144
|
});
|
|
@@ -249,7 +248,6 @@ export class HttpApiServer {
|
|
|
249
248
|
const count = this.manager.stopAllLoops();
|
|
250
249
|
sendOk(res, { count });
|
|
251
250
|
});
|
|
252
|
-
// ── Loop logs ─────────────────────────────────────────────────
|
|
253
251
|
r("GET", "/api/loops/:id/logs", (_req, res, params) => {
|
|
254
252
|
const logPath = this.manager.getLogPath(params.id);
|
|
255
253
|
if (!logPath) {
|
|
@@ -351,7 +349,6 @@ export class HttpApiServer {
|
|
|
351
349
|
});
|
|
352
350
|
sendOk(res, parts.join(""));
|
|
353
351
|
});
|
|
354
|
-
// ── Tasks ──────────────────────────────────────────────────────
|
|
355
352
|
r("GET", "/api/tasks", (_req, res) => {
|
|
356
353
|
sendOk(res, this.taskManager.list());
|
|
357
354
|
});
|
|
@@ -402,7 +399,6 @@ export class HttpApiServer {
|
|
|
402
399
|
}
|
|
403
400
|
sendOk(res);
|
|
404
401
|
});
|
|
405
|
-
// ── Projects ───────────────────────────────────────────────────
|
|
406
402
|
r("GET", "/api/projects", (_req, res) => {
|
|
407
403
|
sendOk(res, this.projectManager.getAll());
|
|
408
404
|
});
|
|
@@ -455,7 +451,6 @@ export class HttpApiServer {
|
|
|
455
451
|
}
|
|
456
452
|
}
|
|
457
453
|
});
|
|
458
|
-
// ── Swagger / OpenAPI ────────────────────────────────────────
|
|
459
454
|
r("GET", "/api/openapi.json", (_req, res) => {
|
|
460
455
|
const spec = this.buildOpenApiSpec();
|
|
461
456
|
const body = JSON.stringify(spec, null, 2);
|
|
@@ -481,7 +476,6 @@ export class HttpApiServer {
|
|
|
481
476
|
});
|
|
482
477
|
res.end(html);
|
|
483
478
|
});
|
|
484
|
-
// ── SSE Events ────────────────────────────────────────────────
|
|
485
479
|
r("GET", "/api/events", (_req, res) => {
|
|
486
480
|
res.writeHead(200, {
|
|
487
481
|
"Content-Type": "text/event-stream",
|
|
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
|
|
2
2
|
import { parseDuration } from "../duration.js";
|
|
3
3
|
import { parseMaxRuns } from "../loop-config.js";
|
|
4
4
|
import { t } from "../i18n/index.js";
|
|
5
|
-
// ── Types ───────────────────────────────────────────────────────────
|
|
6
5
|
export const createFields = [
|
|
7
6
|
"interval",
|
|
8
7
|
"taskMode",
|
|
@@ -14,7 +13,6 @@ export const createFields = [
|
|
|
14
13
|
"maxRuns",
|
|
15
14
|
"project",
|
|
16
15
|
];
|
|
17
|
-
// ── Validation ──────────────────────────────────────────────────────
|
|
18
16
|
/**
|
|
19
17
|
* Validates a single loop form field and returns an error string, or
|
|
20
18
|
* `null` if the value is valid.
|
|
@@ -30,7 +28,6 @@ export const createFields = [
|
|
|
30
28
|
function validateField(key, values) {
|
|
31
29
|
const value = values[key] ?? "";
|
|
32
30
|
switch (key) {
|
|
33
|
-
// ── interval ──────────────────────────────────────────────────
|
|
34
31
|
// Always required; delegates to parseDuration() which throws on
|
|
35
32
|
// empty, invalid syntax, or non-positive values.
|
|
36
33
|
case "interval": {
|
|
@@ -45,7 +42,6 @@ function validateField(key, values) {
|
|
|
45
42
|
return err instanceof Error ? err.message : String(err);
|
|
46
43
|
}
|
|
47
44
|
}
|
|
48
|
-
// ── command ───────────────────────────────────────────────────
|
|
49
45
|
// Required only when the user chose inline mode.
|
|
50
46
|
case "command": {
|
|
51
47
|
if (values.taskMode === "existing")
|
|
@@ -55,7 +51,6 @@ function validateField(key, values) {
|
|
|
55
51
|
}
|
|
56
52
|
return null;
|
|
57
53
|
}
|
|
58
|
-
// ── cwd ───────────────────────────────────────────────────────
|
|
59
54
|
// Optional; if provided the directory must exist on disk.
|
|
60
55
|
case "cwd": {
|
|
61
56
|
if (!value.trim())
|
|
@@ -65,7 +60,6 @@ function validateField(key, values) {
|
|
|
65
60
|
}
|
|
66
61
|
return null;
|
|
67
62
|
}
|
|
68
|
-
// ── description ───────────────────────────────────────────────
|
|
69
63
|
// Always required.
|
|
70
64
|
case "description": {
|
|
71
65
|
if (!value.trim()) {
|
|
@@ -73,7 +67,6 @@ function validateField(key, values) {
|
|
|
73
67
|
}
|
|
74
68
|
return null;
|
|
75
69
|
}
|
|
76
|
-
// ── maxRuns ───────────────────────────────────────────────────
|
|
77
70
|
// Optional; if provided it must be a positive integer.
|
|
78
71
|
case "maxRuns": {
|
|
79
72
|
if (!value.trim())
|
|
@@ -86,7 +79,6 @@ function validateField(key, values) {
|
|
|
86
79
|
return err instanceof Error ? err.message : String(err);
|
|
87
80
|
}
|
|
88
81
|
}
|
|
89
|
-
// ── no validation needed ──────────────────────────────────────
|
|
90
82
|
case "taskMode":
|
|
91
83
|
case "runNow":
|
|
92
84
|
case "project":
|
|
@@ -116,7 +108,6 @@ function validateAll(values) {
|
|
|
116
108
|
}
|
|
117
109
|
return errors;
|
|
118
110
|
}
|
|
119
|
-
// ── Hook ────────────────────────────────────────────────────────────
|
|
120
111
|
/**
|
|
121
112
|
* Shared form-validation hook for loop creation / edit forms.
|
|
122
113
|
*
|
package/dist/i18n/en.json
CHANGED
|
@@ -279,7 +279,7 @@
|
|
|
279
279
|
"cmdEditor.lines": "lines",
|
|
280
280
|
"cmdEditor.inlineHint": "enter: new line . ctrl+v: paste . ctrl+y: copy . tab: next field . ctrl+s: save",
|
|
281
281
|
"codeEditor.title": "Command Editor",
|
|
282
|
-
"codeEditor.hint": "ctrl+s save . esc cancel . ctrl+z/ctrl+shift+z undo/redo",
|
|
282
|
+
"codeEditor.hint": "ctrl+s save . esc cancel . ctrl+z/ctrl+shift+z undo/redo . && sequential . || parallel",
|
|
283
283
|
"codeEditor.buttonCopy": "Copy",
|
|
284
284
|
"codeEditor.buttonPaste": "Paste",
|
|
285
285
|
"codeEditor.buttonClear": "Clear",
|
|
@@ -291,6 +291,13 @@
|
|
|
291
291
|
"codeEditor.copied": "Copied!",
|
|
292
292
|
"codeEditor.cleared": "Cleared",
|
|
293
293
|
"codeEditor.clipboardEmpty": "Clipboard empty — right-click to paste",
|
|
294
|
+
"codeEditor.commandsTitle": "Parallel Commands",
|
|
295
|
+
"codeEditor.commandsHint": "tab: next . shift+tab: prev . ctrl+s: save all . esc: cancel",
|
|
296
|
+
"codeEditor.commandLabel": "Command",
|
|
297
|
+
"codeEditor.commandEmpty": "empty = deleted",
|
|
298
|
+
"codeEditor.commandMax": "max 4",
|
|
299
|
+
"wizard.taskCommandsPrompt": "Commands to run (parallel)",
|
|
300
|
+
"wizard.taskCommandsHint": "Multiple commands run at once, merge JSON output",
|
|
294
301
|
"board.exampleInterval": "30m",
|
|
295
302
|
"board.exampleCommand": "opencode run \"search missing translations and translate them, 3 maximum\" --model \"opencode/big-pickle\"",
|
|
296
303
|
"board.exampleDescription": "translate missing strings",
|
package/dist/tui/App.js
CHANGED
|
@@ -42,10 +42,8 @@ export function App(props) {
|
|
|
42
42
|
const { exit } = useApp();
|
|
43
43
|
const { loops, daemonStatus, refresh } = useLoopPolling();
|
|
44
44
|
const { view, push, pop } = useRouter("board");
|
|
45
|
-
// ── Tab and panel state (8.1, 8.2) ──
|
|
46
45
|
const [activeTab, setActiveTab] = useState("loops");
|
|
47
46
|
const [focusedPanel, setFocusedPanel] = useState("left");
|
|
48
|
-
// ── Filter state ──
|
|
49
47
|
const [filters, setFilters] = useState(defaultFilters);
|
|
50
48
|
const [sort, setSort] = useState("description");
|
|
51
49
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -66,19 +64,15 @@ export function App(props) {
|
|
|
66
64
|
const [currentProjectId, setCurrentProjectId] = useState("all");
|
|
67
65
|
const [projectSelectedIndex, setProjectSelectedIndex] = useState(0);
|
|
68
66
|
const [projectFilters, setProjectFilters] = useState(defaultProjectFilters);
|
|
69
|
-
// ── Overlay state ──
|
|
70
67
|
const [commandsBrowserOpen, setCommandsBrowserOpen] = useState(false);
|
|
71
68
|
const [contextHelpOpen, setContextHelpOpen] = useState(false);
|
|
72
69
|
const [exportModal, setExportModal] = useState(null);
|
|
73
|
-
// ── Confirm state (8.3) ──
|
|
74
70
|
const [confirmState, setConfirmState] = useState(null);
|
|
75
|
-
// ── Search state (command-driven search) ──
|
|
76
71
|
const [searchState, setSearchState] = useState(null);
|
|
77
72
|
const [searchValue, setSearchValue] = useState("");
|
|
78
73
|
const [debugMode, setDebugMode] = useState(false);
|
|
79
74
|
const [debugEntries, setDebugEntries] = useState([]);
|
|
80
75
|
const [chordState, setChordState] = useState(null);
|
|
81
|
-
// ── Command bar input state (for InputOwner resolution) ──
|
|
82
76
|
const [commandBarHasText, setCommandBarHasText] = useState(false);
|
|
83
77
|
const [commandBarDropdownOpen, setCommandBarDropdownOpen] = useState(false);
|
|
84
78
|
const { toasts, push: pushToast } = useToasts();
|
|
@@ -190,7 +184,6 @@ export function App(props) {
|
|
|
190
184
|
if (handler)
|
|
191
185
|
handler();
|
|
192
186
|
}, [activeTab, selected, selectedTask, selectedProjectEntity, pushToast]);
|
|
193
|
-
// ── Command handler — dictionary dispatch, no switch/case ──
|
|
194
187
|
const commandHandlers = {
|
|
195
188
|
edit: () => {
|
|
196
189
|
if (activeTab === "loops" && selected) {
|
|
@@ -332,7 +325,6 @@ export function App(props) {
|
|
|
332
325
|
pushToast("error", `Unknown command: ${value}`);
|
|
333
326
|
}
|
|
334
327
|
}
|
|
335
|
-
// ── Confirm handlers (8.3) ──
|
|
336
328
|
const handleConfirmYes = useCallback(() => {
|
|
337
329
|
if (confirmState) {
|
|
338
330
|
confirmState.onConfirm();
|
|
@@ -368,11 +360,9 @@ export function App(props) {
|
|
|
368
360
|
pop();
|
|
369
361
|
pushToast("success", updated ? t("project.toastUpdated", { name }) : t("project.toastCreated", { name }));
|
|
370
362
|
};
|
|
371
|
-
// ── Resolve query for LeftPanel based on activeTab + search state ──
|
|
372
363
|
const leftPanelQuery = searchState?.active
|
|
373
364
|
? searchValue
|
|
374
365
|
: activeTab === "tasks" ? taskQuery : filters.query;
|
|
375
|
-
// ── Search handlers ──
|
|
376
366
|
const handleSearchChange = useCallback((value) => {
|
|
377
367
|
setSearchValue(value);
|
|
378
368
|
if (activeTab === "tasks") {
|
|
@@ -395,7 +385,6 @@ export function App(props) {
|
|
|
395
385
|
}
|
|
396
386
|
setSearchState(null);
|
|
397
387
|
}, [activeTab]);
|
|
398
|
-
// ── Command context (8.3) ──
|
|
399
388
|
const commandContext = useMemo(() => ({ activeTab, selectedLoop: selected, selectedTask, selectedProject: selectedProjectEntity }), [activeTab, selected, selectedTask]);
|
|
400
389
|
// Contextual action for the focused panel: shared by Ctrl+Enter and, for
|
|
401
390
|
// terminals that collapse Ctrl+Enter to plain Enter, by Enter on an empty
|
|
@@ -433,7 +422,6 @@ export function App(props) {
|
|
|
433
422
|
const handlerKey = activeTab !== "loops" ? `${activeTab}:` : `loops:${focusedPanel}`;
|
|
434
423
|
handlers[handlerKey]?.();
|
|
435
424
|
};
|
|
436
|
-
// ── Pop topmost overlay layer (Escape handler helper) ──
|
|
437
425
|
const popLayer = () => {
|
|
438
426
|
if (confirmState) {
|
|
439
427
|
setConfirmState(null);
|
|
@@ -472,7 +460,6 @@ export function App(props) {
|
|
|
472
460
|
});
|
|
473
461
|
return true;
|
|
474
462
|
};
|
|
475
|
-
// ── Global useInput (8.2) ──
|
|
476
463
|
useInput((input, key) => {
|
|
477
464
|
// Ctrl+C always quits if no modal open
|
|
478
465
|
if (key.ctrl && input === "c") {
|
|
@@ -579,7 +566,6 @@ export function App(props) {
|
|
|
579
566
|
// Search mode is handled by CommandInput component, not here (non-Escape only)
|
|
580
567
|
if (searchState?.active && !key.escape)
|
|
581
568
|
return;
|
|
582
|
-
// ── Escape: pop the topmost overlay layer (2.2) ──
|
|
583
569
|
if (key.escape) {
|
|
584
570
|
popLayer();
|
|
585
571
|
return;
|
|
@@ -642,7 +628,6 @@ export function App(props) {
|
|
|
642
628
|
// Modals disable panel input behind them; CommandInput stays active for confirm/search
|
|
643
629
|
const anyModalOpen = !!(logModalRun || commandsBrowserOpen || exportModal);
|
|
644
630
|
const commandInputDisabled = anyModalOpen;
|
|
645
|
-
// ── Resolved input owner (1.2) ──
|
|
646
631
|
const inputOwner = useMemo(() => resolveInputOwner({
|
|
647
632
|
modalOpen: !!(logModalRun || commandsBrowserOpen || exportModal || contextHelpOpen || confirmState || searchState?.active),
|
|
648
633
|
commandBarHasText,
|
package/dist/tui/commands.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { COMMAND_TIER_ACTION, COMMAND_TIER_CONFIRM, COMMAND_TIER_GLOBAL, COMMAND_CATEGORY_GLOBAL, COMMAND_CATEGORY_FILTERS, COMMAND_CATEGORY_LOOP, COMMAND_CATEGORY_TASK, COMMAND_CATEGORY_PROJECT, } from '../config/constants.js';
|
|
2
2
|
import { t } from '../i18n/index.js';
|
|
3
|
-
// ── Shared helpers ───────────────────────────────────────────────────
|
|
4
3
|
function globalCommands() {
|
|
5
4
|
return [
|
|
6
5
|
{ label: t('cmd.help'), value: 'help', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+p' },
|
|
@@ -29,7 +28,6 @@ function projectFilterCommands() {
|
|
|
29
28
|
{ label: t('cmd.searchProjects'), value: 'search', hint: '', tier: COMMAND_TIER_ACTION, category: COMMAND_CATEGORY_FILTERS, shortcut: 'ctrl+f+s' },
|
|
30
29
|
];
|
|
31
30
|
}
|
|
32
|
-
// ── Command ranking (exact > prefix > fuzzy) ─────────────────────────
|
|
33
31
|
/**
|
|
34
32
|
* Re-rank autocomplete options: exact match → prefix match → fuzzy (existing order).
|
|
35
33
|
* Stable within each group. Exported for testing.
|
|
@@ -56,7 +54,6 @@ export function rankCommands(input, options) {
|
|
|
56
54
|
}
|
|
57
55
|
return [...exact, ...prefix, ...fuzzy];
|
|
58
56
|
}
|
|
59
|
-
// ── Autocomplete dropdown (context-sensitive, shown while typing) ────
|
|
60
57
|
export function buildCommands(context) {
|
|
61
58
|
const commands = [...globalCommands()];
|
|
62
59
|
if (context.activeTab === 'loops') {
|
|
@@ -95,7 +92,6 @@ export function buildCommands(context) {
|
|
|
95
92
|
}
|
|
96
93
|
return commands;
|
|
97
94
|
}
|
|
98
|
-
// ── Ctrl+P commands browser (shows all available commands for this tab) ─
|
|
99
95
|
export function buildTabCommands(context) {
|
|
100
96
|
const commands = [...globalCommands()];
|
|
101
97
|
if (context.activeTab === 'loops') {
|
|
@@ -7,13 +7,22 @@ import { useUndoRedo } from "../../shared/useUndoRedo.js";
|
|
|
7
7
|
import { highlightSegments } from "../utils/syntax.js";
|
|
8
8
|
import { joinCommandLines } from "../../loop-config.js";
|
|
9
9
|
import { copyToClipboard, readFromClipboard } from "../../shared/clipboard.js";
|
|
10
|
-
import {
|
|
11
|
-
import { CODE_EDITOR_MODAL_HEIGHT, CODE_EDITOR_UNDO_LIMIT, CODE_EDITOR_SYNTAX_COLORS, } from "../../config/constants.js";
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
10
|
+
import { sanitizeMultilinePaste } 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
|
+
}
|
|
15
23
|
export function CodeEditorModal(props) {
|
|
16
24
|
const { initialValue, onSave, onCancel } = props;
|
|
25
|
+
const { modalWidth, modalHeight, editorVisibleLines: EDITOR_VISIBLE_LINES } = useModalDimensions();
|
|
17
26
|
const { value, setValue, undo, redo } = useUndoRedo(initialValue, CODE_EDITOR_UNDO_LIMIT);
|
|
18
27
|
const [cursorRow, setCursorRow] = useState(() => {
|
|
19
28
|
const lines = initialValue.split("\n");
|
|
@@ -53,7 +62,7 @@ export function CodeEditorModal(props) {
|
|
|
53
62
|
// Must be detected BEFORE the escape check — the leading ESC trips
|
|
54
63
|
// key.escape and would close the modal before paste is handled.
|
|
55
64
|
if (input.includes("\x1b[200~")) {
|
|
56
|
-
const pasted =
|
|
65
|
+
const pasted = sanitizeMultilinePaste(input);
|
|
57
66
|
if (pasted) {
|
|
58
67
|
const next = [...lines];
|
|
59
68
|
const line = next[cursorRow] ?? "";
|
|
@@ -97,7 +106,7 @@ export function CodeEditorModal(props) {
|
|
|
97
106
|
if ((key.ctrl && input === "v") || input === "\x16") {
|
|
98
107
|
const clip = readFromClipboard();
|
|
99
108
|
if (clip) {
|
|
100
|
-
const pasted =
|
|
109
|
+
const pasted = sanitizeMultilinePaste(clip);
|
|
101
110
|
if (pasted) {
|
|
102
111
|
const next = [...lines];
|
|
103
112
|
const line = next[cursorRow] ?? "";
|
|
@@ -185,12 +194,10 @@ export function CodeEditorModal(props) {
|
|
|
185
194
|
}
|
|
186
195
|
return;
|
|
187
196
|
}
|
|
188
|
-
// Multi-char
|
|
189
|
-
|
|
190
|
-
return;
|
|
191
|
-
// Multi-char printable input = unbracketed single-line paste
|
|
197
|
+
// Multi-char printable input = unbracketed paste (e.g. right-click
|
|
198
|
+
// in terminals that don't support DECSET 2004). Preserve newlines.
|
|
192
199
|
if (input.length > 1 && !key.meta) {
|
|
193
|
-
const pasted =
|
|
200
|
+
const pasted = sanitizeMultilinePaste(input);
|
|
194
201
|
if (pasted) {
|
|
195
202
|
const next = [...lines];
|
|
196
203
|
const line = next[cursorRow] ?? "";
|
|
@@ -211,12 +218,12 @@ export function CodeEditorModal(props) {
|
|
|
211
218
|
}, { isActive: true });
|
|
212
219
|
// ---- Live preview footer ----
|
|
213
220
|
const joined = joinCommandLines(value);
|
|
214
|
-
const innerWidth =
|
|
221
|
+
const innerWidth = modalWidth - 2;
|
|
215
222
|
const truncatedPreview = joined.length > innerWidth
|
|
216
223
|
? joined.slice(0, innerWidth - 1) + t("codeEditor.previewTruncated")
|
|
217
224
|
: joined;
|
|
218
225
|
const accent = theme.accent.brand;
|
|
219
|
-
return (_jsx(Box, { position: "absolute", top: 0, left: 0, width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: _jsxs(Box, { width:
|
|
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) => {
|
|
220
227
|
const rowIdx = scrollStart + visIdx;
|
|
221
228
|
const isCursor = rowIdx === cursorRow;
|
|
222
229
|
const lineNum = String(rowIdx + 1).padStart(lineNumWidth, " ");
|
|
@@ -8,7 +8,6 @@ import { buildCommands, rankCommands } from "../commands.js";
|
|
|
8
8
|
import { COMMAND_INPUT_DROPDOWN_MAX_VISIBLE, COMMAND_INPUT_HEIGHT, } from "../../config/constants.js";
|
|
9
9
|
import { sanitizePaste } from "../utils/paste.js";
|
|
10
10
|
export { sanitizePaste } from "../utils/paste.js";
|
|
11
|
-
// ── Rendered input with cursor ───────────────────────────────────────
|
|
12
11
|
function renderInputLine(value, cursorOffset) {
|
|
13
12
|
if (value.length === 0) {
|
|
14
13
|
return "\x1b[7m \x1b[27m";
|
|
@@ -54,7 +53,6 @@ function renderHighlightedLabel(label, matchRanges, isFocused) {
|
|
|
54
53
|
}
|
|
55
54
|
return result;
|
|
56
55
|
}
|
|
57
|
-
// ── Dropdown ─────────────────────────────────────────────────────────
|
|
58
56
|
function CommandDropdown({ state, rankedFiltered, }) {
|
|
59
57
|
if (!state.isOpen || state.isLoading || state.error)
|
|
60
58
|
return null;
|
|
@@ -75,15 +73,12 @@ function CommandDropdown({ state, rankedFiltered, }) {
|
|
|
75
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));
|
|
76
74
|
}), belowCount > 0 && (_jsx(Text, { color: theme.text.muted, children: ` \u2193 ${belowCount} more` }))] }));
|
|
77
75
|
}
|
|
78
|
-
// ── Confirm inline options ────────────────────────────────────────────
|
|
79
|
-
// ── Hint bar ──────────────────────────────────────────────────────────
|
|
80
76
|
function HintBar({ leftHint, rightHint, }) {
|
|
81
77
|
return (_jsxs(Box, { justifyContent: "space-between", paddingX: 1, children: [_jsx(Box, { children: leftHint }), _jsx(Box, { children: rightHint })] }));
|
|
82
78
|
}
|
|
83
79
|
function KeyHint({ keyLabel, action }) {
|
|
84
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 })] }));
|
|
85
81
|
}
|
|
86
|
-
// ── Command mode ─────────────────────────────────────────────────────
|
|
87
82
|
function CommandMode({ context, onCommand, onCopy, onPanelAction, disabled, navOwner, onInputStateChange, }) {
|
|
88
83
|
const commands = useMemo(() => buildCommands(context), [context]);
|
|
89
84
|
const options = useMemo(() => commands.map((cmd) => ({ label: cmd.label, value: cmd.value })), [commands]);
|
|
@@ -210,7 +205,6 @@ function CommandMode({ context, onCommand, onCopy, onPanelAction, disabled, navO
|
|
|
210
205
|
const inputContent = isEmpty ? cursor : renderInputLine(state.inputValue, state.cursorOffset);
|
|
211
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" })] }) })] }));
|
|
212
207
|
}
|
|
213
|
-
// ── Confirm mode ─────────────────────────────────────────────────────
|
|
214
208
|
function ConfirmMode({ confirmState, onConfirmYes, onConfirmCancel, disabled, }) {
|
|
215
209
|
const [value, setValue] = useState("");
|
|
216
210
|
useInput((input, key) => {
|
|
@@ -246,7 +240,6 @@ function ConfirmMode({ confirmState, onConfirmYes, onConfirmCancel, disabled, })
|
|
|
246
240
|
const cursor = "\x1b[7m \x1b[27m";
|
|
247
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" }) })] }));
|
|
248
242
|
}
|
|
249
|
-
// ── Search mode ──────────────────────────────────────────────────────
|
|
250
243
|
function SearchMode({ value, onSearchChange, onSearchSubmit, onSearchCancel, disabled, }) {
|
|
251
244
|
useInput((input, key) => {
|
|
252
245
|
if (key.ctrl)
|
|
@@ -274,7 +267,6 @@ function SearchMode({ value, onSearchChange, onSearchSubmit, onSearchCancel, dis
|
|
|
274
267
|
const cursor = "\x1b[7m \x1b[27m";
|
|
275
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" }) })] }));
|
|
276
269
|
}
|
|
277
|
-
// ── Main component ───────────────────────────────────────────────────
|
|
278
270
|
export function CommandInput(props) {
|
|
279
271
|
const { context, onCommand, confirmState, searchState, searchValue, onSearchChange, onSearchSubmit, onSearchCancel, onConfirmYes, onConfirmCancel, onCopy, onPanelAction, navOwner, onInputStateChange, } = props;
|
|
280
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 })) }));
|
|
@@ -9,7 +9,6 @@ import { CodeEditorPreview } from "./CodeEditorPreview.js";
|
|
|
9
9
|
import { CodeEditorModal } from "./CodeEditorModal.js";
|
|
10
10
|
import { parseDuration } from "../../duration.js";
|
|
11
11
|
import { parseCommandLine, joinCommandLines } from "../../loop-config.js";
|
|
12
|
-
// ── Component ───────────────────────────────────────────────────────
|
|
13
12
|
export function CreateView(props) {
|
|
14
13
|
const { mode, editId, initial, selectedTaskId, selectedTaskName, tasks, currentProjectId, onCancel, onDone, onChooseTask, } = props;
|
|
15
14
|
const [taskPickerOpen, setTaskPickerOpen] = useState(false);
|
|
@@ -7,8 +7,7 @@ import { CodeEditorModal } from "./CodeEditorModal.js";
|
|
|
7
7
|
import { createTask, updateTask, listTasks } from "../daemon.js";
|
|
8
8
|
import crypto from "node:crypto";
|
|
9
9
|
import { t } from "../../i18n/index.js";
|
|
10
|
-
import { joinCommandLines } from "../../loop-config.js";
|
|
11
|
-
// ── Utility ─────────────────────────────────────────────────────────
|
|
10
|
+
import { joinCommandLines, parseCommandLine } from "../../loop-config.js";
|
|
12
11
|
function parseArgs(cmd) {
|
|
13
12
|
const tokens = [];
|
|
14
13
|
const regex = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
@@ -21,11 +20,14 @@ function parseArgs(cmd) {
|
|
|
21
20
|
function joinCommand(task) {
|
|
22
21
|
return [task.command, ...task.commandArgs].join(" ");
|
|
23
22
|
}
|
|
24
|
-
// ── Component ───────────────────────────────────────────────────────
|
|
25
23
|
export function TaskForm(props) {
|
|
26
24
|
const { mode, editTask, onCancel, onDone } = props;
|
|
27
25
|
const [tasks, setTasks] = useState([]);
|
|
28
|
-
const [commandValue, setCommandValue] = useState(editTask
|
|
26
|
+
const [commandValue, setCommandValue] = useState(editTask
|
|
27
|
+
? (editTask.steps?.length
|
|
28
|
+
? editTask.steps.map(step => step.commands.map(c => c.commandRaw ?? [c.command, ...c.commandArgs].join(" ")).join("\n||\n")).join("\n&&\n")
|
|
29
|
+
: (editTask.commandRaw ?? joinCommand(editTask)))
|
|
30
|
+
: "");
|
|
29
31
|
useEffect(() => {
|
|
30
32
|
listTasks().then(setTasks).catch(() => setTasks([]));
|
|
31
33
|
}, []);
|
|
@@ -100,14 +102,50 @@ export function TaskForm(props) {
|
|
|
100
102
|
return;
|
|
101
103
|
const onSuccessTaskId = resolveChainId(values.onSuccess ?? "");
|
|
102
104
|
const onFailureTaskId = resolveChainId(values.onFailure ?? "");
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
const commandSegments = commandValue
|
|
106
|
+
.split(/\n&&\n|\n&&|&&\n|&&/)
|
|
107
|
+
.map(s => s.trim())
|
|
108
|
+
.filter(s => s.length > 0);
|
|
109
|
+
let payload;
|
|
110
|
+
if (commandSegments.length > 1 || commandValue.includes("||")) {
|
|
111
|
+
const steps = commandSegments.slice(0, 8).map(seg => {
|
|
112
|
+
const parallelCmds = seg
|
|
113
|
+
.split(/\n\|\|\n|\n\|\||\|\|\n|\|\|/)
|
|
114
|
+
.map(s => s.trim())
|
|
115
|
+
.filter(s => s.length > 0)
|
|
116
|
+
.slice(0, 4)
|
|
117
|
+
.map(cmdSeg => {
|
|
118
|
+
const joined = joinCommandLines(cmdSeg);
|
|
119
|
+
const tokens = parseCommandLine(joined);
|
|
120
|
+
return {
|
|
121
|
+
command: tokens[0] ?? "",
|
|
122
|
+
commandArgs: tokens.slice(1),
|
|
123
|
+
commandRaw: cmdSeg,
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
return { commands: parallelCmds };
|
|
127
|
+
});
|
|
128
|
+
const firstCmd = steps[0].commands[0];
|
|
129
|
+
payload = {
|
|
130
|
+
name,
|
|
131
|
+
command: firstCmd.command,
|
|
132
|
+
commandArgs: firstCmd.commandArgs,
|
|
133
|
+
commandRaw: firstCmd.commandRaw,
|
|
134
|
+
steps,
|
|
135
|
+
onSuccessTaskId,
|
|
136
|
+
onFailureTaskId,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
payload = {
|
|
141
|
+
name,
|
|
142
|
+
command: rawCommand.split(" ")[0] ?? "",
|
|
143
|
+
commandArgs: parseArgs(rawCommand),
|
|
144
|
+
commandRaw: commandValue,
|
|
145
|
+
onSuccessTaskId,
|
|
146
|
+
onFailureTaskId,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
111
149
|
if (mode === "edit" && editTask) {
|
|
112
150
|
updateTask(editTask.id, payload)
|
|
113
151
|
.then(() => onDone(true, editTask.id))
|
package/dist/tui/utils/paste.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { PASTE_MAX_CHARS } from "../../config/constants.js";
|
|
2
|
-
// Turn a raw paste (possibly bracketed, multi-line, with stray control chars)
|
|
3
|
-
// into a single-line insertable string. Exported for tests.
|
|
4
2
|
export function sanitizePaste(raw) {
|
|
5
3
|
return raw
|
|
6
|
-
.replace(/\x1b\[20[01]~/g, "")
|
|
7
|
-
.replace(/[\r\n]+/g, " ")
|
|
8
|
-
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
|
4
|
+
.replace(/\x1b\[20[01]~/g, "")
|
|
5
|
+
.replace(/[\r\n]+/g, " ")
|
|
6
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
|
7
|
+
.slice(0, PASTE_MAX_CHARS);
|
|
8
|
+
}
|
|
9
|
+
export function sanitizeMultilinePaste(raw) {
|
|
10
|
+
return raw
|
|
11
|
+
.replace(/\x1b\[20[01]~/g, "")
|
|
12
|
+
.replace(/\r\n/g, "\n")
|
|
13
|
+
.replace(/\r/g, "\n")
|
|
14
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
|
|
9
15
|
.slice(0, PASTE_MAX_CHARS);
|
|
10
16
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|