loop-task 2.0.4 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,12 @@ import { execa } from "execa";
3
3
  import { formatDuration } from "../duration.js";
4
4
  import { t } from "../i18n/index.js";
5
5
  import { MAX_CONTEXT_STDOUT_BYTES } from "../config/constants.js";
6
+ function quoteArg(arg) {
7
+ return /[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
8
+ }
9
+ function formatCommandLine(command, commandArgs) {
10
+ return [command, ...commandArgs.map((a) => quoteArg(a))].join(" ").trim();
11
+ }
6
12
  export function extractExitCode(error) {
7
13
  return error && typeof error === "object" && "exitCode" in error
8
14
  ? error.exitCode
@@ -12,6 +18,7 @@ export async function executeCommand(command, commandArgs, cwd, logStream, signa
12
18
  const startedAt = new Date();
13
19
  const header = t("loop.runHeader", { timestamp: startedAt.toLocaleString(), runNumber: runNumber ?? 0 });
14
20
  logStream.write(header);
21
+ logStream.write(t("loop.commandLine", { command: formatCommandLine(command, commandArgs) }));
15
22
  if (cwd && !fs.existsSync(cwd)) {
16
23
  const endedAt = new Date();
17
24
  logStream.write(t("loop.cwdMissingLog", { cwd }));
@@ -363,12 +363,15 @@ export class LoopController extends EventEmitter {
363
363
  if (mainRecord)
364
364
  mainRecord.chainGroupId = chainGroupId;
365
365
  let currentTargetId = chainTargetId;
366
+ let prevBranch = result.exitCode === 0 ? "onSuccess" : "onFailure";
367
+ let prevExit = result.exitCode;
366
368
  while (currentTargetId) {
367
369
  const chainTask = this.taskResolver(currentTargetId);
368
370
  if (!chainTask)
369
371
  break;
370
372
  if (this.logStream) {
371
- this.logStream.write(t("loop.chainHeader", { name: chainTask.name }));
373
+ this.logStream.write(t("loop.chainHeader", { name: chainTask.name, branch: prevBranch, prevExit }));
374
+ this.logStream.write(t("loop.commandLine", { command: [chainTask.command, ...chainTask.commandArgs.map((a) => /[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a)].join(" ").trim() }));
372
375
  }
373
376
  const chainStartedAt = new Date().toISOString();
374
377
  const chainOffset = fs.existsSync(this.logPath) ? fs.statSync(this.logPath).size : 0;
@@ -403,6 +406,8 @@ export class LoopController extends EventEmitter {
403
406
  this.lastExitCode = chainResult.exitCode;
404
407
  this.lastDuration = (this.lastDuration ?? 0) + chainResult.duration;
405
408
  currentTargetId = (chainResult.exitCode === 0 ? chainTask.onSuccessTaskId : chainTask.onFailureTaskId) ?? null;
409
+ prevBranch = chainResult.exitCode === 0 ? "onSuccess" : "onFailure";
410
+ prevExit = chainResult.exitCode;
406
411
  }
407
412
  }
408
413
  if (this.runHistory.length > 50) {
package/dist/i18n/en.json CHANGED
@@ -65,7 +65,8 @@
65
65
  "cli.projectAmbiguous": "Multiple projects named \"{name}\" - use the id instead",
66
66
  "cli.projectInvalidColor": "Invalid color \"{color}\". Valid names: {valid}, or a #rrggbb hex",
67
67
  "loop.runHeader": "\n[Run #{runNumber} - {timestamp}]\n",
68
- "loop.chainHeader": "\n--- Chain: {name} ---\n",
68
+ "loop.commandLine": "$ {command}\n",
69
+ "loop.chainHeader": "\n--- Chain: {name} ({branch}, prev exit {prevExit}) ---\n",
69
70
  "loop.exitMarker": "[exit {code} · {duration}]\n",
70
71
  "loop.cwdMissingLog": "[error] working directory does not exist: {cwd}\n",
71
72
  "loop.cwdMissing": "Working directory does not exist: {cwd}",
@@ -69,9 +69,12 @@ export function CodeEditorModal(props) {
69
69
  redo();
70
70
  return;
71
71
  }
72
- // Ctrl+Ycopy (NOT redo)
73
- if (key.ctrl && input === "y") {
72
+ // Ctrl+Xcut (copy all to clipboard, then clear)
73
+ if (key.ctrl && input === "x") {
74
74
  copyToClipboard(value);
75
+ setValue("");
76
+ setCursorRow(0);
77
+ setCursorCol(0);
75
78
  setFlashMsg(t("codeEditor.copied"));
76
79
  return;
77
80
  }
@@ -90,27 +93,8 @@ export function CodeEditorModal(props) {
90
93
  }
91
94
  return;
92
95
  }
93
- // Shift+Ccopy, Shift+V → paste, Shift+X → clear (button shortcuts)
94
- if (key.shift && !key.ctrl && !key.meta && input === "C") {
95
- copyToClipboard(value);
96
- setFlashMsg(t("codeEditor.copied"));
97
- return;
98
- }
99
- if (key.shift && !key.ctrl && !key.meta && input === "V") {
100
- const clip = readFromClipboard();
101
- if (clip) {
102
- const pasted = sanitizePaste(clip);
103
- if (pasted) {
104
- const next = [...lines];
105
- const line = next[cursorRow] ?? "";
106
- next[cursorRow] =
107
- line.slice(0, cursorCol) + pasted + line.slice(cursorCol);
108
- applyMutation(next, cursorRow, cursorCol + pasted.length);
109
- }
110
- }
111
- return;
112
- }
113
- if (key.shift && !key.ctrl && !key.meta && input === "X") {
96
+ // Ctrl+L → clear all
97
+ if (key.ctrl && input === "l") {
114
98
  setValue("");
115
99
  setCursorRow(0);
116
100
  setCursorCol(0);
@@ -241,5 +225,5 @@ export function CodeEditorModal(props) {
241
225
  return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), _jsx(Text, { children: " " })] }, rowIdx));
242
226
  }
243
227
  return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [lineNum, " "] }), segments.map((seg, ti) => (_jsx(Text, { color: seg.color, children: seg.value }, ti)))] }, rowIdx));
244
- }) }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [t("codeEditor.previewLabel"), " "] }), _jsx(Text, { color: theme.text.secondary, children: truncatedPreview || t("codeEditor.emptyPlaceholder") })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonCopy") }), _jsx(Text, { color: theme.text.muted, children: "shift+c" }), _jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonPaste") }), _jsx(Text, { color: theme.text.muted, children: "shift+v" }), _jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonClear") }), _jsx(Text, { color: theme.text.muted, children: "shift+x" })] }), flashMsg ? (_jsx(Text, { color: theme.semantic.success, children: flashMsg })) : null] }), _jsx(Box, { children: _jsx(Text, { color: theme.text.muted, children: t("codeEditor.hint") }) })] }) }));
228
+ }) }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.text.muted, children: [t("codeEditor.previewLabel"), " "] }), _jsx(Text, { color: theme.text.secondary, children: truncatedPreview || t("codeEditor.emptyPlaceholder") })] }), _jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonCopy") }), _jsx(Text, { color: theme.text.muted, children: "ctrl+x" }), _jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonPaste") }), _jsx(Text, { color: theme.text.muted, children: "ctrl+v" }), _jsx(Text, { color: theme.accent.brand, children: t("codeEditor.buttonClear") }), _jsx(Text, { color: theme.text.muted, children: "ctrl+l" })] }), flashMsg ? (_jsx(Text, { color: theme.semantic.success, children: flashMsg })) : null] }), _jsx(Box, { children: _jsx(Text, { color: theme.text.muted, children: t("codeEditor.hint") }) })] }) }));
245
229
  }
@@ -11,6 +11,8 @@ const MAX_VISIBLE_LINES = 20;
11
11
  function colorForLine(line, run) {
12
12
  if (line.includes("[Run #"))
13
13
  return theme.accent.loop;
14
+ if (line.startsWith("$ "))
15
+ return "#f0abfc";
14
16
  if (line.includes("--- Chain:"))
15
17
  return theme.accent.task;
16
18
  if (line.trimStart().startsWith("[exit")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loop-task",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
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": {