loop-task 2.1.9 → 2.1.10

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,
@@ -2,7 +2,7 @@ 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
4
  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;
5
+ 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
6
  const commandHandlers = {
7
7
  edit: () => {
8
8
  if (activeTab === "loops" && selected) {
@@ -109,9 +109,10 @@ export function useCommandHandlers(context) {
109
109
  debug: () => { setDebugMode((prev) => !prev); },
110
110
  logs: () => {
111
111
  if (activeTab === "loops" && selected) {
112
- const runs = selected.runHistory;
113
- if (runs && runs.length > 0) {
114
- handleOpenRunLog(runs[runs.length - 1]);
112
+ const reversed = [...selected.runHistory].reverse();
113
+ if (reversed.length > 0) {
114
+ const run = reversed[Math.min(selectedRunIndex, reversed.length - 1)];
115
+ handleOpenRunLog(run ?? reversed[0]);
115
116
  }
116
117
  }
117
118
  },
@@ -3,7 +3,7 @@ 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
5
  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;
6
+ 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
7
  const handleContextualCopy = useCallback(() => {
8
8
  const copyHandlers = {
9
9
  loops: () => {
@@ -57,9 +57,11 @@ export function useContextualActions(context) {
57
57
  "loops:right": () => {
58
58
  if (!selected)
59
59
  return;
60
- const runs = selected.runHistory;
61
- if (runs && runs.length > 0)
62
- handleOpenRunLog(runs[runs.length - 1]);
60
+ const reversed = [...selected.runHistory].reverse();
61
+ if (reversed.length > 0) {
62
+ const run = reversed[Math.min(selectedRunIndex, reversed.length - 1)];
63
+ handleOpenRunLog(run ?? reversed[0]);
64
+ }
63
65
  else
64
66
  editSelectedLoop();
65
67
  },
@@ -68,7 +70,7 @@ export function useContextualActions(context) {
68
70
  const handlerKey = activeTab !== "loops" ? `${activeTab}:` : `loops:${focusedPanel}`;
69
71
  handlers[handlerKey]?.();
70
72
  }, [activeTab, focusedPanel, view, logModalRun, commandsBrowserOpen, confirmState, searchState,
71
- chordState, setChordState, selected, selectedTask, selectedProjectEntity, tasks, push,
73
+ chordState, setChordState, selected, selectedRunIndex, selectedTask, selectedProjectEntity, tasks, push,
72
74
  setCloneMode, setEditTarget, setPendingTaskSelection, handleCommand, handleOpenRunLog,
73
75
  isBoardView]);
74
76
  return { handleContextualCopy, triggerContextualAction };
@@ -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
2
  import { useEffect, useState } from "react";
3
- import { Box, Text, useInput } from "ink";
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;
@@ -32,6 +31,9 @@ export function LogModal(props) {
32
31
  const [follow, setFollow] = useState(true);
33
32
  const [scrollOffset, setScrollOffset] = useState(0);
34
33
  const logService = useInject(TYPES.LogService);
34
+ const { stdout } = useStdout();
35
+ const terminalHeight = stdout?.rows ?? 24;
36
+ const MAX_VISIBLE_LINES = Math.max(1, Math.floor(terminalHeight * 0.7) - 7);
35
37
  useEffect(() => {
36
38
  setLines(props.logLines);
37
39
  }, [props.logLines]);
@@ -92,6 +94,16 @@ export function LogModal(props) {
92
94
  props.onCopy?.();
93
95
  return;
94
96
  }
97
+ if (key.ctrl && input === "t") {
98
+ setFollow(false);
99
+ setScrollOffset(0);
100
+ return;
101
+ }
102
+ if (key.ctrl && input === "b") {
103
+ setFollow(true);
104
+ setScrollOffset(0);
105
+ return;
106
+ }
95
107
  if (key.downArrow) {
96
108
  setFollow(false);
97
109
  setScrollOffset((o) => Math.min(o + 1, Math.max(0, totalLines - MAX_VISIBLE_LINES)));
@@ -118,7 +130,7 @@ export function LogModal(props) {
118
130
  ? t("board.logModalRunning")
119
131
  : 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
132
  ? `/${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
133
+ : `[${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
134
  ? t("board.logModalLoading")
123
135
  : searchQuery
124
136
  ? 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.10",
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": {