loop-task 2.1.9 → 2.1.11

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/app/App.js CHANGED
@@ -48,7 +48,7 @@ export function App(props) {
48
48
  const s = useAppState(loops, pushToast, refresh, loopService, taskService, projectService, logService, view, push, pop);
49
49
  useLogStream(s.selectedId, view, (error) => pushToast("error", error.message));
50
50
  const { handleCommand } = useCommandHandlers({
51
- activeTab: s.activeTab, selected: s.selected, selectedTask: s.selectedTask, selectedProjectEntity: s.selectedProjectEntity,
51
+ activeTab: s.activeTab, selected: s.selected, selectedRunIndex: s.selectedRunIndex, selectedTask: s.selectedTask, selectedProjectEntity: s.selectedProjectEntity,
52
52
  tasks: s.tasks, projects: s.projects, currentProjectId: s.currentProjectId, filters: s.filters, projectFilters: s.projectFilters,
53
53
  setCloneMode: s.setCloneMode, setEditTarget: s.setEditTarget, setPendingTaskSelection: s.setPendingTaskSelection,
54
54
  setEditTask: s.setEditTask, setEditProject: s.setEditProject, setActiveTab: s.setActiveTab,
@@ -61,7 +61,7 @@ export function App(props) {
61
61
  runAction: s.runAction, handleOpenRunLog: s.handleOpenRunLog,
62
62
  });
63
63
  const { handleContextualCopy, triggerContextualAction } = useContextualActions({
64
- activeTab: s.activeTab, focusedPanel: s.focusedPanel, selected: s.selected, selectedTask: s.selectedTask, selectedProjectEntity: s.selectedProjectEntity,
64
+ activeTab: s.activeTab, focusedPanel: s.focusedPanel, selected: s.selected, selectedRunIndex: s.selectedRunIndex, selectedTask: s.selectedTask, selectedProjectEntity: s.selectedProjectEntity,
65
65
  tasks: s.tasks, push, setCloneMode: s.setCloneMode, setEditTarget: s.setEditTarget, setPendingTaskSelection: s.setPendingTaskSelection,
66
66
  handleCommand, handleOpenRunLog: s.handleOpenRunLog, pushToast, isBoardView,
67
67
  view, logModalRun: s.logModalRun, commandsBrowserOpen: s.commandsBrowserOpen, confirmState: s.confirmState, searchState: s.searchState,
@@ -1,8 +1,9 @@
1
1
  import { t } from "../../shared/i18n/index.js";
2
2
  import { cycleSortMode, cycleStatusFilter } from "../../entities/loops/filters.js";
3
3
  import { cycleProjectSortMode, cycleProjectHasLoopsFilter, cycleProjectIsSystemFilter } from "../../entities/projects/filters.js";
4
+ import { groupRunsByCycle } from "../../widgets/right-panel/RunHistory.js";
4
5
  export function useCommandHandlers(context) {
5
- const { activeTab, selected, selectedTask, selectedProjectEntity, tasks, projects, currentProjectId, setCloneMode, setEditTarget, setPendingTaskSelection, setEditTask, setEditProject, setActiveTab, setConfirmState, setCommandsBrowserOpen, setSearchValue, setSearchState, setFilters, setSort, setCurrentProjectId, setProjectFilters, setProjectSelectedIndex, setDebugMode, setExportModal, push, pop, refresh, refreshTasks, refreshProjects, pushToast, loopService, taskService, projectService, exportService, runAction, handleOpenRunLog, } = context;
6
+ const { activeTab, selected, selectedRunIndex, selectedTask, selectedProjectEntity, tasks, projects, currentProjectId, setCloneMode, setEditTarget, setPendingTaskSelection, setEditTask, setEditProject, setActiveTab, setConfirmState, setCommandsBrowserOpen, setSearchValue, setSearchState, setFilters, setSort, setCurrentProjectId, setProjectFilters, setProjectSelectedIndex, setDebugMode, setExportModal, push, pop, refresh, refreshTasks, refreshProjects, pushToast, loopService, taskService, projectService, exportService, runAction, handleOpenRunLog, } = context;
6
7
  const commandHandlers = {
7
8
  edit: () => {
8
9
  if (activeTab === "loops" && selected) {
@@ -109,9 +110,10 @@ export function useCommandHandlers(context) {
109
110
  debug: () => { setDebugMode((prev) => !prev); },
110
111
  logs: () => {
111
112
  if (activeTab === "loops" && selected) {
112
- const runs = selected.runHistory;
113
- if (runs && runs.length > 0) {
114
- handleOpenRunLog(runs[runs.length - 1]);
113
+ const reversed = [...groupRunsByCycle(selected.runHistory)].reverse();
114
+ if (reversed.length > 0) {
115
+ const run = reversed[Math.min(selectedRunIndex, reversed.length - 1)];
116
+ handleOpenRunLog(run ?? reversed[0]);
115
117
  }
116
118
  }
117
119
  },
@@ -2,8 +2,9 @@ import { useCallback } from "react";
2
2
  import { copyToClipboard } from "../../shared/clipboard.js";
3
3
  import { commandLine } from "../../shared/ui/format.js";
4
4
  import { t } from "../../shared/i18n/index.js";
5
+ import { groupRunsByCycle } from "../../widgets/right-panel/RunHistory.js";
5
6
  export function useContextualActions(context) {
6
- const { activeTab, focusedPanel, selected, selectedTask, selectedProjectEntity, tasks, push, setCloneMode, setEditTarget, setPendingTaskSelection, handleCommand, handleOpenRunLog, pushToast, isBoardView, view, logModalRun, commandsBrowserOpen, confirmState, searchState, setChordState, chordState, } = context;
7
+ const { activeTab, focusedPanel, selected, selectedRunIndex, selectedTask, selectedProjectEntity, tasks, push, setCloneMode, setEditTarget, setPendingTaskSelection, handleCommand, handleOpenRunLog, pushToast, isBoardView, view, logModalRun, commandsBrowserOpen, confirmState, searchState, setChordState, chordState, } = context;
7
8
  const handleContextualCopy = useCallback(() => {
8
9
  const copyHandlers = {
9
10
  loops: () => {
@@ -57,9 +58,11 @@ export function useContextualActions(context) {
57
58
  "loops:right": () => {
58
59
  if (!selected)
59
60
  return;
60
- const runs = selected.runHistory;
61
- if (runs && runs.length > 0)
62
- handleOpenRunLog(runs[runs.length - 1]);
61
+ const reversed = [...groupRunsByCycle(selected.runHistory)].reverse();
62
+ if (reversed.length > 0) {
63
+ const run = reversed[Math.min(selectedRunIndex, reversed.length - 1)];
64
+ handleOpenRunLog(run ?? reversed[0]);
65
+ }
63
66
  else
64
67
  editSelectedLoop();
65
68
  },
@@ -68,7 +71,7 @@ export function useContextualActions(context) {
68
71
  const handlerKey = activeTab !== "loops" ? `${activeTab}:` : `loops:${focusedPanel}`;
69
72
  handlers[handlerKey]?.();
70
73
  }, [activeTab, focusedPanel, view, logModalRun, commandsBrowserOpen, confirmState, searchState,
71
- chordState, setChordState, selected, selectedTask, selectedProjectEntity, tasks, push,
74
+ chordState, setChordState, selected, selectedRunIndex, selectedTask, selectedProjectEntity, tasks, push,
72
75
  setCloneMode, setEditTarget, setPendingTaskSelection, handleCommand, handleOpenRunLog,
73
76
  isBoardView]);
74
77
  return { handleContextualCopy, triggerContextualAction };
@@ -4,7 +4,9 @@ import { TYPES } from "../services/types.js";
4
4
  import { t } from "../i18n/index.js";
5
5
  import { LOG_LINES_MAX } from "../config/constants.js";
6
6
  export function useLogStream(selectedId, view, onError) {
7
- const logService = useInject(TYPES.LogService);
7
+ const injectedLogService = useInject(TYPES.LogService);
8
+ const logServiceRef = useRef(injectedLogService);
9
+ const logService = logServiceRef.current;
8
10
  const [logLines, setLogLines] = useState([]);
9
11
  const logSocket = useRef(null);
10
12
  useEffect(() => {
@@ -193,7 +193,7 @@
193
193
  "board.logModalExit": "exit {code}",
194
194
  "board.logModalDuration": "duration",
195
195
  "board.logModalSearchHint": "type to filter, enter to apply, esc to exit search",
196
- "board.logModalFooterHints": "/:search ctrl+x:copy up/down:scroll esc:close",
196
+ "board.logModalFooterHints": "/:search ctrl+x:copy up/down:scroll ctrl+t:top ctrl+b:bottom esc:close",
197
197
  "board.logModalEscClose": "esc to close",
198
198
  "board.logModalRunning": "running…",
199
199
  "board.detailTitle": " Loop Detail ",
@@ -100,7 +100,7 @@ export function formatRunTime(iso) {
100
100
  export function formatDate(iso) {
101
101
  const d = new Date(iso);
102
102
  const date = d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
103
- const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
103
+ const time = d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
104
104
  return `${date} ${time}`;
105
105
  }
106
106
  export function sinceLabel(loop) {
@@ -1,13 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
- import { Box, Text, useInput } from "ink";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Box, Text, useInput, useStdout } from "ink";
4
4
  import { darkTheme as theme } from "../../shared/ui/theme.js";
5
5
  import { t } from "../../shared/i18n/index.js";
6
6
  import { useInject } from "../../shared/hooks/useInject.js";
7
7
  import { TYPES } from "../../shared/services/types.js";
8
8
  import { formatDate } from "../../shared/ui/format.js";
9
9
  import { copyToClipboard } from "../../shared/clipboard.js";
10
- const MAX_VISIBLE_LINES = 20;
11
10
  function colorForLine(line, run) {
12
11
  if (line.includes("[Run #"))
13
12
  return theme.accent.loop;
@@ -31,7 +30,13 @@ export function LogModal(props) {
31
30
  const [searchQuery, setSearchQuery] = useState("");
32
31
  const [follow, setFollow] = useState(true);
33
32
  const [scrollOffset, setScrollOffset] = useState(0);
34
- const logService = useInject(TYPES.LogService);
33
+ const bottomDistanceRef = useRef(0);
34
+ const injectedLogService = useInject(TYPES.LogService);
35
+ const logServiceRef = useRef(injectedLogService);
36
+ const logService = logServiceRef.current;
37
+ const { stdout } = useStdout();
38
+ const terminalHeight = stdout?.rows ?? 24;
39
+ const MAX_VISIBLE_LINES = Math.max(1, Math.floor(terminalHeight * 0.7) - 7);
35
40
  useEffect(() => {
36
41
  setLines(props.logLines);
37
42
  }, [props.logLines]);
@@ -92,14 +97,26 @@ export function LogModal(props) {
92
97
  props.onCopy?.();
93
98
  return;
94
99
  }
100
+ if (key.ctrl && input === "t") {
101
+ setFollow(false);
102
+ setScrollOffset(0);
103
+ return;
104
+ }
105
+ if (key.ctrl && input === "b") {
106
+ setFollow(true);
107
+ setScrollOffset(0);
108
+ return;
109
+ }
95
110
  if (key.downArrow) {
111
+ const base = follow ? Math.max(0, totalLines - MAX_VISIBLE_LINES) : scrollOffset;
96
112
  setFollow(false);
97
- setScrollOffset((o) => Math.min(o + 1, Math.max(0, totalLines - MAX_VISIBLE_LINES)));
113
+ setScrollOffset(Math.min(base + 1, Math.max(0, totalLines - MAX_VISIBLE_LINES)));
98
114
  return;
99
115
  }
100
116
  if (key.upArrow) {
117
+ const base = follow ? Math.max(0, totalLines - MAX_VISIBLE_LINES) : scrollOffset;
101
118
  setFollow(false);
102
- setScrollOffset((o) => Math.max(0, o - 1));
119
+ setScrollOffset(Math.max(0, base - 1));
103
120
  return;
104
121
  }
105
122
  });
@@ -118,7 +135,7 @@ export function LogModal(props) {
118
135
  ? t("board.logModalRunning")
119
136
  : t("board.logModalExit", { code: props.run.exitCode })] }), props.run.duration > 0 ? (_jsxs(Text, { color: theme.text.muted, children: [" ", t("board.logModalDuration"), " ", props.run.duration, "ms"] })) : null, _jsx(Text, { color: theme.text.muted, children: searchMode
120
137
  ? `/${searchQuery}`
121
- : `[${startIdx}-${endIdx}/${totalLines}]` })] }), searchMode ? (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.text.muted, children: "Search: " }), _jsxs(Text, { color: theme.text.primary, children: [searchQuery, "_"] })] })) : null, _jsx(Box, { flexDirection: "column", flexGrow: 1, children: visible.length === 0 ? (_jsx(Text, { color: theme.text.muted, children: isLoading
138
+ : `[${startIdx}-${endIdx}/${totalLines}]` })] }), searchMode ? (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.text.muted, children: "Search: " }), _jsxs(Text, { color: theme.text.primary, children: [searchQuery, "_"] })] })) : null, _jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: visible.length === 0 ? (_jsx(Text, { color: theme.text.muted, children: isLoading
122
139
  ? t("board.logModalLoading")
123
140
  : searchQuery
124
141
  ? t("board.logModalNoMatches")
@@ -1,16 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { darkTheme as theme, statusColor } from "../../shared/ui/theme.js";
4
- import { describeLoop, commandLine, timeAgo, timeUntil } from "../../shared/ui/format.js";
4
+ import { describeLoop, commandLine, timeAgo, timeUntil, truncate } from "../../shared/ui/format.js";
5
5
  import { t } from "../../shared/i18n/index.js";
6
- import { resolveEffectiveCwd } from "../../core/command/resolve-cwd.js";
7
6
  const LABEL_WIDTH = 11;
8
7
  function Field(props) {
9
- return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), _jsx(Text, { color: theme.text.primary, children: props.children })] }));
8
+ return (_jsxs(Box, { overflow: "hidden", children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), _jsx(Text, { color: theme.text.primary, wrap: "truncate", children: props.children })] }));
10
9
  }
11
10
  const DIVIDER = "\u2500".repeat(40);
12
11
  function MutedField(props) {
13
- return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), _jsx(Text, { color: theme.text.muted, children: props.children })] }));
12
+ return (_jsxs(Box, { overflow: "hidden", children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), _jsx(Text, { color: theme.text.muted, wrap: "truncate", children: props.children })] }));
14
13
  }
15
14
  export function Inspector(props) {
16
15
  const { loop, projects } = props;
@@ -22,12 +21,7 @@ export function Inspector(props) {
22
21
  const lastRun = loop.lastRunAt ? timeAgo(loop.lastRunAt) : t("format.dash");
23
22
  const lastExit = loop.lastExitCode !== null ? String(loop.lastExitCode) : t("format.dash");
24
23
  const nextRun = loop.nextRunAt ? t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) }) : t("format.dash");
25
- const pid = loop.pid ? String(loop.pid) : t("format.dash");
26
- const projectDirectory = projects?.find((p) => p.id === loop.projectId)?.directory;
27
- const effectiveCwd = resolveEffectiveCwd(loop.cwd, projectDirectory);
28
- const showEffective = loop.cwd !== effectiveCwd;
29
- const fullCmd = commandLine(loop.command, loop.commandArgs);
30
- const desc = describeLoop(loop);
31
- const showCommand = desc !== fullCmd;
32
- return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: t("board.fieldStatus").padEnd(LABEL_WIDTH) }), _jsx(Text, { color: sColor, children: loop.status })] }), _jsx(Field, { label: t("board.fieldLastExit"), children: _jsx(Text, { color: theme.text.primary, children: lastExit }) }), _jsx(Field, { label: t("board.fieldLastRun"), children: _jsx(Text, { color: theme.text.primary, children: lastRun }) }), _jsx(Field, { label: t("board.fieldNextRun"), children: _jsx(Text, { color: theme.text.primary, children: nextRun }) }), _jsx(Field, { label: t("board.fieldRuns"), children: _jsxs(Text, { color: theme.text.primary, children: [loop.runCount, " / ", maxRunsLabel] }) }), _jsx(Field, { label: t("board.fieldInterval"), children: _jsx(Text, { color: theme.text.primary, children: loop.intervalHuman }) }), _jsx(Field, { label: t("board.fieldDir"), children: _jsxs(Text, { color: theme.text.primary, children: [loop.cwd || t("board.inherit"), showEffective ? ` → ${effectiveCwd}` : ""] }) }), _jsx(MutedField, { label: t("board.fieldId"), children: loop.id }), _jsx(MutedField, { label: t("board.fieldDesc"), children: desc }), showCommand && _jsx(MutedField, { label: t("board.fieldCommand"), children: fullCmd }), _jsx(MutedField, { label: t("board.fieldTask"), children: loop.taskId ?? t("format.dash") }), _jsx(MutedField, { label: t("board.fieldPid"), children: pid })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
24
+ const fullCmd = truncate(commandLine(loop.command, loop.commandArgs), 38);
25
+ const desc = truncate(describeLoop(loop), 38);
26
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: t("board.fieldStatus").padEnd(LABEL_WIDTH) }), _jsx(Text, { color: sColor, children: loop.status })] }), _jsx(Field, { label: t("board.fieldRuns"), children: _jsxs(Text, { color: theme.text.primary, children: [loop.runCount, " / ", maxRunsLabel] }) }), _jsx(Field, { label: t("board.fieldInterval"), children: _jsx(Text, { color: theme.text.primary, children: loop.intervalHuman }) }), _jsx(Field, { label: t("board.fieldLastExit"), children: _jsx(Text, { color: theme.text.primary, children: lastExit }) }), _jsx(Field, { label: t("board.fieldLastRun"), children: _jsx(Text, { color: theme.text.primary, children: lastRun }) }), _jsx(Field, { label: t("board.fieldNextRun"), children: _jsx(Text, { color: theme.text.primary, children: nextRun }) }), _jsx(MutedField, { label: t("board.fieldDesc"), children: desc }), _jsx(MutedField, { label: t("board.fieldCommand"), children: fullCmd })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
33
27
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
2
+ import { Box, Text, useStdout } from "ink";
3
3
  import { darkTheme as theme, tabAccentColor } from "../../shared/ui/theme.js";
4
4
  import { t } from "../../shared/i18n/index.js";
5
5
  import { Inspector } from "./Inspector.js";
@@ -10,7 +10,9 @@ const DIVIDER = "\u2500".repeat(40);
10
10
  export function RightPanel(props) {
11
11
  const { isFocused, navActive = true, activeTab, loop, selectedRunIndex, onSelectRun, onOpenRun, selectedTask, allTasks, selectedProject, projectLoopCount, onProjectEdit, onProjectDelete, projects, } = props;
12
12
  const borderColor = isFocused ? tabAccentColor(activeTab) : theme.border.default;
13
- return (_jsx(Box, { flexDirection: "column", width: "40%", borderStyle: "single", borderColor: borderColor, children: activeTab === "projects" ? (_jsx(ProjectInspector, { project: selectedProject ?? null, loopCount: projectLoopCount ?? 0, onEdit: onProjectEdit, onDelete: onProjectDelete })) : activeTab === "tasks" ? (_jsx(TaskInspector, { task: selectedTask ?? null, allTasks: allTasks ?? [] })) : (_jsxs(_Fragment, { children: [_jsx(Inspector, { loop: loop, projects: projects }), _jsx(RunHistory, { loop: loop, selectedRunIndex: selectedRunIndex, onSelectRun: onSelectRun, onOpenRun: onOpenRun, isFocused: isFocused, navActive: navActive })] })) }));
13
+ const { stdout } = useStdout();
14
+ const panelHeight = (stdout?.rows ?? 24) - 8;
15
+ return (_jsx(Box, { flexDirection: "column", width: "40%", height: panelHeight, borderStyle: "single", borderColor: borderColor, children: activeTab === "projects" ? (_jsx(ProjectInspector, { project: selectedProject ?? null, loopCount: projectLoopCount ?? 0, onEdit: onProjectEdit, onDelete: onProjectDelete })) : activeTab === "tasks" ? (_jsx(TaskInspector, { task: selectedTask ?? null, allTasks: allTasks ?? [] })) : (_jsxs(_Fragment, { children: [_jsx(Inspector, { loop: loop, projects: projects }), _jsx(RunHistory, { loop: loop, selectedRunIndex: selectedRunIndex, onSelectRun: onSelectRun, onOpenRun: onOpenRun, isFocused: isFocused, navActive: navActive })] })) }));
14
16
  }
15
17
  function TaskInspector(props) {
16
18
  const { task, allTasks } = props;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Box, Text, useInput } from "ink";
2
+ import { Box, Text, useInput, useStdout } from "ink";
3
3
  import { ScrollList } from "ink-scroll-list";
4
4
  import { darkTheme as theme } from "../../shared/ui/theme.js";
5
5
  import { formatRunTime, formatRunDuration, formatFileSize } from "../../shared/ui/format.js";
@@ -110,9 +110,11 @@ function computeTrends(runs) {
110
110
  lastFailureAgo,
111
111
  };
112
112
  }
113
- const LIMIT = 15;
114
113
  export function RunHistory(props) {
115
114
  const { loop, selectedRunIndex, onSelectRun, onOpenRun, isFocused, navActive = true } = props;
115
+ const { stdout } = useStdout();
116
+ const terminalHeight = stdout?.rows ?? 24;
117
+ const LIMIT = Math.max(3, terminalHeight - 22);
116
118
  const runs = groupRunsByCycle(loop?.runHistory ?? []);
117
119
  const reversed = [...runs].reverse();
118
120
  const n = reversed.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loop-task",
3
- "version": "2.1.9",
3
+ "version": "2.1.11",
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": {