loop-task 2.0.14 → 2.0.16
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/cli/import-validator.js +1 -1
- package/dist/cli/import-writer.js +5 -1
- package/dist/cli.js +2 -1
- package/dist/i18n/en.json +1 -0
- package/dist/shared/clipboard.js +80 -10
- package/dist/tui/App.js +5 -2
- package/dist/tui/components/CodeEditorModal.js +29 -21
- package/dist/tui/components/ExportModal.js +6 -0
- package/dist/tui/components/LogModal.js +7 -0
- package/dist/tui/daemon.js +1 -1
- package/package.json +1 -1
|
@@ -38,7 +38,7 @@ const LOOP_FIELDS = [
|
|
|
38
38
|
{ name: "remainingDelayMs", type: "number", nullable: true },
|
|
39
39
|
{ name: "pid", type: "number", nullable: false },
|
|
40
40
|
{ name: "maxRunsReached", type: "boolean", nullable: false },
|
|
41
|
-
{ name: "runHistory", type: "array", nullable:
|
|
41
|
+
{ name: "runHistory", type: "array", nullable: true },
|
|
42
42
|
{ name: "skippedCount", type: "number", nullable: false },
|
|
43
43
|
{ name: "projectId", type: "string", nullable: false },
|
|
44
44
|
{ name: "offset", type: "number", nullable: true },
|
|
@@ -9,8 +9,12 @@ function backupFilePath(filePath) {
|
|
|
9
9
|
export function atomicImportWrite(loops, tasks, projects) {
|
|
10
10
|
const dataDir = getDataDir();
|
|
11
11
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
12
|
+
const normalizedLoops = loops.map((loop) => ({
|
|
13
|
+
...loop,
|
|
14
|
+
runHistory: Array.isArray(loop.runHistory) ? loop.runHistory : [],
|
|
15
|
+
}));
|
|
12
16
|
const contents = {
|
|
13
|
-
"loops.json": JSON.stringify(
|
|
17
|
+
"loops.json": JSON.stringify(normalizedLoops, null, 2),
|
|
14
18
|
"tasks.json": JSON.stringify(tasks, null, 2),
|
|
15
19
|
"projects.json": JSON.stringify(projects, null, 2),
|
|
16
20
|
};
|
package/dist/cli.js
CHANGED
|
@@ -206,10 +206,11 @@ program
|
|
|
206
206
|
sendRequest({ type: "task-list" }),
|
|
207
207
|
sendRequest({ type: "project-list" }),
|
|
208
208
|
]);
|
|
209
|
+
const loops = loopsRes.type === "ok" ? loopsRes.data : [];
|
|
209
210
|
const exportData = {
|
|
210
211
|
version: 2,
|
|
211
212
|
exportedAt: new Date().toISOString(),
|
|
212
|
-
loops:
|
|
213
|
+
loops: loops.map(({ runHistory: _ignored, ...rest }) => rest),
|
|
213
214
|
tasks: tasksRes.type === "ok" ? tasksRes.data : [],
|
|
214
215
|
projects: projectsRes.type === "ok" ? projectsRes.data : [],
|
|
215
216
|
};
|
package/dist/i18n/en.json
CHANGED
|
@@ -290,6 +290,7 @@
|
|
|
290
290
|
"codeEditor.fieldHint": "press enter to open editor",
|
|
291
291
|
"codeEditor.copied": "Copied!",
|
|
292
292
|
"codeEditor.cleared": "Cleared",
|
|
293
|
+
"codeEditor.clipboardEmpty": "Clipboard empty — right-click to paste",
|
|
293
294
|
"board.exampleInterval": "30m",
|
|
294
295
|
"board.exampleCommand": "opencode run \"search missing translations and translate them, 3 maximum\" --model \"opencode/big-pickle\"",
|
|
295
296
|
"board.exampleDescription": "translate missing strings",
|
package/dist/shared/clipboard.js
CHANGED
|
@@ -1,39 +1,109 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { platform } from "node:os";
|
|
3
|
+
/**
|
|
4
|
+
* Write text to the system clipboard. Never throws — returns false if no
|
|
5
|
+
* clipboard tool is available (e.g. a headless SSH box without xclip/xsel).
|
|
6
|
+
*
|
|
7
|
+
* Linux toolchain tried in order: xclip → xsel → wl-copy (Wayland) →
|
|
8
|
+
* tmux load-buffer (when $TMUX is set) → OSC 52 (works over SSH because
|
|
9
|
+
* the local terminal owns the clipboard).
|
|
10
|
+
*/
|
|
3
11
|
export function copyToClipboard(text) {
|
|
4
12
|
const os = platform();
|
|
5
13
|
if (os === "win32") {
|
|
6
|
-
|
|
14
|
+
try {
|
|
15
|
+
execFileSync("clip", { input: text });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
7
21
|
}
|
|
8
|
-
|
|
9
|
-
|
|
22
|
+
if (os === "darwin") {
|
|
23
|
+
try {
|
|
24
|
+
execFileSync("pbcopy", { input: text });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
10
30
|
}
|
|
11
|
-
|
|
31
|
+
// Linux / BSD: try each clipboard tool in order. All calls are guarded —
|
|
32
|
+
// a missing binary must never throw, since callers invoke this from
|
|
33
|
+
// synchronous keypress handlers (an uncaught throw here kills the app).
|
|
34
|
+
const tries = [
|
|
35
|
+
() => execFileSync("xclip", ["-selection", "clipboard"], { input: text }),
|
|
36
|
+
() => execFileSync("xsel", ["--clipboard", "--input"], { input: text }),
|
|
37
|
+
() => execFileSync("wl-copy", [], { input: text }),
|
|
38
|
+
];
|
|
39
|
+
for (const tryFn of tries) {
|
|
12
40
|
try {
|
|
13
|
-
|
|
41
|
+
tryFn();
|
|
42
|
+
return true;
|
|
14
43
|
}
|
|
15
44
|
catch {
|
|
16
|
-
|
|
45
|
+
// try next tool
|
|
17
46
|
}
|
|
18
47
|
}
|
|
48
|
+
if (process.env.TMUX) {
|
|
49
|
+
try {
|
|
50
|
+
execFileSync("tmux", ["load-buffer", "-"], { input: text });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// fall through to OSC 52
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// OSC 52: ask the connected terminal to copy. Works over SSH because the
|
|
58
|
+
// local terminal owns the clipboard. Most modern terminals support it
|
|
59
|
+
// (iTerm2, WezTerm, Windows Terminal, Kitty, Alacritty).
|
|
60
|
+
try {
|
|
61
|
+
process.stdout.write(`\x1b]52;c;${Buffer.from(text, "utf-8").toString("base64")}\x07`);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
19
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Read text from the system clipboard. Returns "" if unavailable.
|
|
70
|
+
*
|
|
71
|
+
* Linux toolchain: xclip → xsel → wl-paste → tmux save-buffer -. OSC 52
|
|
72
|
+
* reply is not solicited synchronously (timings are unreliable across
|
|
73
|
+
* terminals); callers fall back to bracketed-paste detection for input.
|
|
74
|
+
*/
|
|
20
75
|
export function readFromClipboard() {
|
|
21
76
|
const os = platform();
|
|
22
77
|
try {
|
|
23
78
|
if (os === "win32") {
|
|
24
79
|
return execFileSync("powershell", ["-NoProfile", "-Command", "Get-Clipboard"], { encoding: "utf-8" }).replace(/\r?\n$/, "");
|
|
25
80
|
}
|
|
26
|
-
|
|
81
|
+
if (os === "darwin") {
|
|
27
82
|
return execFileSync("pbpaste", { encoding: "utf-8" });
|
|
28
83
|
}
|
|
29
|
-
|
|
84
|
+
// Linux / BSD
|
|
85
|
+
const tries = [
|
|
86
|
+
() => execFileSync("xclip", ["-selection", "clipboard", "-o"], { encoding: "utf-8" }),
|
|
87
|
+
() => execFileSync("xsel", ["--clipboard", "--output"], { encoding: "utf-8" }),
|
|
88
|
+
() => execFileSync("wl-paste", [], { encoding: "utf-8" }),
|
|
89
|
+
];
|
|
90
|
+
for (const tryFn of tries) {
|
|
30
91
|
try {
|
|
31
|
-
return
|
|
92
|
+
return tryFn();
|
|
32
93
|
}
|
|
33
94
|
catch {
|
|
34
|
-
|
|
95
|
+
// try next tool
|
|
35
96
|
}
|
|
36
97
|
}
|
|
98
|
+
if (process.env.TMUX) {
|
|
99
|
+
try {
|
|
100
|
+
return execFileSync("tmux", ["save-buffer", "-"], { encoding: "utf-8" });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// ignore
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return "";
|
|
37
107
|
}
|
|
38
108
|
catch {
|
|
39
109
|
return "";
|
package/dist/tui/App.js
CHANGED
|
@@ -17,7 +17,7 @@ import { CommandsBrowserModal } from "./components/CommandsBrowserModal.js";
|
|
|
17
17
|
import { DebugPanel, MAX_ENTRIES } from "./components/DebugPanel.js";
|
|
18
18
|
import { ContextHelpModal } from "./components/ContextHelpModal.js";
|
|
19
19
|
import { ExportModal } from "./components/ExportModal.js";
|
|
20
|
-
import { fetchRunLog, deleteLoop, pauseLoop, resumeLoop, stopLoop, triggerLoop, listTasks, deleteTask, listProjects, exportConfig } from "./daemon.js";
|
|
20
|
+
import { fetchRunLog, deleteLoop, pauseLoop, resumeLoop, stopLoop, playLoop, triggerLoop, listTasks, deleteTask, listProjects, exportConfig } from "./daemon.js";
|
|
21
21
|
import { applyLoopFilters, applyProjectFilters, cycleSortMode, cycleStatusFilter, cycleProjectSortMode, cycleProjectHasLoopsFilter, cycleProjectIsSystemFilter, defaultFilters, defaultProjectFilters, resolveInputOwner } from "./state.js";
|
|
22
22
|
import { t } from "../i18n/index.js";
|
|
23
23
|
import { copyToClipboard } from "../shared/clipboard.js";
|
|
@@ -259,7 +259,10 @@ export function App(props) {
|
|
|
259
259
|
},
|
|
260
260
|
play: () => {
|
|
261
261
|
if (activeTab === "loops" && selected) {
|
|
262
|
-
|
|
262
|
+
const isPaused = selected.status === "paused";
|
|
263
|
+
const toastKey = isPaused ? "board.toastResumed" : "board.toastPlayed";
|
|
264
|
+
const fn = isPaused ? () => resumeLoop(selected.id) : () => playLoop(selected.id);
|
|
265
|
+
void runAction(t(toastKey, { desc: selected.description }), fn)();
|
|
263
266
|
}
|
|
264
267
|
},
|
|
265
268
|
stop: () => {
|
|
@@ -49,18 +49,32 @@ export function CodeEditorModal(props) {
|
|
|
49
49
|
}, [setValue]);
|
|
50
50
|
// ---- Keyboard handling ----
|
|
51
51
|
useInput((input, key) => {
|
|
52
|
+
// Bracketed paste: content wrapped in ESC[200~ ... ESC[201~
|
|
53
|
+
// Must be detected BEFORE the escape check — the leading ESC trips
|
|
54
|
+
// key.escape and would close the modal before paste is handled.
|
|
55
|
+
if (input.includes("\x1b[200~")) {
|
|
56
|
+
const pasted = sanitizePaste(input);
|
|
57
|
+
if (pasted) {
|
|
58
|
+
const next = [...lines];
|
|
59
|
+
const line = next[cursorRow] ?? "";
|
|
60
|
+
next[cursorRow] =
|
|
61
|
+
line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
|
|
62
|
+
applyMutation(next, cursorRow, cursorCol + pasted.length);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
52
66
|
// Esc → cancel
|
|
53
67
|
if (key.escape) {
|
|
54
68
|
onCancel();
|
|
55
69
|
return;
|
|
56
70
|
}
|
|
57
|
-
// Ctrl+S → save
|
|
58
|
-
if (key.ctrl && input === "s") {
|
|
71
|
+
// Ctrl+S → save (also accept raw \x13 fallback)
|
|
72
|
+
if ((key.ctrl && input === "s") || input === "\x13") {
|
|
59
73
|
onSave(value);
|
|
60
74
|
return;
|
|
61
75
|
}
|
|
62
|
-
// Ctrl+Z → undo
|
|
63
|
-
if (key.ctrl && !key.shift && input === "z") {
|
|
76
|
+
// Ctrl+Z → undo (also accept raw \x1a fallback)
|
|
77
|
+
if ((key.ctrl && !key.shift && input === "z") || input === "\x1a") {
|
|
64
78
|
undo();
|
|
65
79
|
return;
|
|
66
80
|
}
|
|
@@ -69,8 +83,8 @@ export function CodeEditorModal(props) {
|
|
|
69
83
|
redo();
|
|
70
84
|
return;
|
|
71
85
|
}
|
|
72
|
-
// Ctrl+X → cut (copy all to clipboard, then clear)
|
|
73
|
-
if (key.ctrl && input === "x") {
|
|
86
|
+
// Ctrl+X → cut (copy all to clipboard, then clear; also accept \x18)
|
|
87
|
+
if ((key.ctrl && input === "x") || input === "\x18") {
|
|
74
88
|
copyToClipboard(value);
|
|
75
89
|
setValue("");
|
|
76
90
|
setCursorRow(0);
|
|
@@ -78,8 +92,9 @@ export function CodeEditorModal(props) {
|
|
|
78
92
|
setFlashMsg(t("codeEditor.copied"));
|
|
79
93
|
return;
|
|
80
94
|
}
|
|
81
|
-
// Ctrl+V → paste from clipboard
|
|
82
|
-
|
|
95
|
+
// Ctrl+V → paste from clipboard (also accept raw \x16 fallback for
|
|
96
|
+
// terminals that don't set key.ctrl on the SYN control byte)
|
|
97
|
+
if ((key.ctrl && input === "v") || input === "\x16") {
|
|
83
98
|
const clip = readFromClipboard();
|
|
84
99
|
if (clip) {
|
|
85
100
|
const pasted = sanitizePaste(clip);
|
|
@@ -91,10 +106,15 @@ export function CodeEditorModal(props) {
|
|
|
91
106
|
applyMutation(next, cursorRow, cursorCol + pasted.length);
|
|
92
107
|
}
|
|
93
108
|
}
|
|
109
|
+
else {
|
|
110
|
+
// No clipboard tool available (common over SSH). Bracketed paste
|
|
111
|
+
// (right-click in the terminal) still works — point the user at it.
|
|
112
|
+
setFlashMsg(t("codeEditor.clipboardEmpty"));
|
|
113
|
+
}
|
|
94
114
|
return;
|
|
95
115
|
}
|
|
96
116
|
// Ctrl+L → clear all
|
|
97
|
-
if (key.ctrl && input === "l") {
|
|
117
|
+
if ((key.ctrl && input === "l") || input === "\x0c") {
|
|
98
118
|
setValue("");
|
|
99
119
|
setCursorRow(0);
|
|
100
120
|
setCursorCol(0);
|
|
@@ -165,18 +185,6 @@ export function CodeEditorModal(props) {
|
|
|
165
185
|
}
|
|
166
186
|
return;
|
|
167
187
|
}
|
|
168
|
-
// Bracketed paste: content wrapped in ESC[200~ ... ESC[201~
|
|
169
|
-
if (input.includes("\x1b[200~")) {
|
|
170
|
-
const pasted = sanitizePaste(input);
|
|
171
|
-
if (pasted) {
|
|
172
|
-
const next = [...lines];
|
|
173
|
-
const line = next[cursorRow] ?? "";
|
|
174
|
-
next[cursorRow] =
|
|
175
|
-
line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
|
|
176
|
-
applyMutation(next, cursorRow, cursorCol + pasted.length);
|
|
177
|
-
}
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
188
|
// Multi-char containing CR/LF with no bracketed markers — ignore
|
|
181
189
|
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
182
190
|
return;
|
|
@@ -14,6 +14,12 @@ export function ExportModal(props) {
|
|
|
14
14
|
const maxScroll = Math.max(0, displayLines.length - VISIBLE_LINES);
|
|
15
15
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
16
16
|
useInput((input, key) => {
|
|
17
|
+
// Bracketed paste: content wrapped in ESC[200~ ... ESC[201~. Must come
|
|
18
|
+
// before the escape check — the leading ESC trips key.escape and would
|
|
19
|
+
// close the modal on a right-click paste.
|
|
20
|
+
if (input.includes("\x1b[200~")) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
17
23
|
if (key.escape) {
|
|
18
24
|
props.onClose();
|
|
19
25
|
return;
|
|
@@ -54,6 +54,13 @@ export function LogModal(props) {
|
|
|
54
54
|
const endIdx = startIdx + MAX_VISIBLE_LINES;
|
|
55
55
|
const visible = filtered.slice(startIdx, endIdx);
|
|
56
56
|
useInput((input, key) => {
|
|
57
|
+
// Bracketed paste: content wrapped in ESC[200~ ... ESC[201~. Must come
|
|
58
|
+
// before the escape check — the leading ESC trips key.escape and would
|
|
59
|
+
// close the modal before the paste is acknowledged. LogModal doesn't
|
|
60
|
+
// insert pastes, so just swallow the sequence.
|
|
61
|
+
if (input.includes("\x1b[200~")) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
57
64
|
if (searchMode) {
|
|
58
65
|
if (key.escape || key.return) {
|
|
59
66
|
setSearchMode(false);
|
package/dist/tui/daemon.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
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": {
|