loop-task 2.1.8 → 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 +2 -2
- package/dist/core/loop/loop-controller.js +4 -2
- package/dist/daemon/index.js +4 -1
- package/dist/daemon/managers/project-manager.js +8 -1
- package/dist/daemon/state/index.js +8 -1
- package/dist/features/commands/useCommandHandlers.js +5 -4
- package/dist/features/commands/useContextualActions.js +7 -5
- package/dist/shared/i18n/en.json +1 -1
- package/dist/shared/ui/format.js +1 -1
- package/dist/widgets/log-modal/LogModal.js +15 -3
- package/dist/widgets/right-panel/Inspector.js +6 -12
- package/dist/widgets/right-panel/RightPanel.js +4 -2
- package/dist/widgets/right-panel/RunHistory.js +4 -2
- package/package.json +1 -1
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,
|
|
@@ -22,6 +22,7 @@ export class LoopController extends EventEmitter {
|
|
|
22
22
|
this.remainingDelayMs = null;
|
|
23
23
|
this.logStream = null;
|
|
24
24
|
this.loopPromise = null;
|
|
25
|
+
this._loopActive = false;
|
|
25
26
|
this.sessionStartedAt = null;
|
|
26
27
|
this.runHistory = [];
|
|
27
28
|
this.currentRunStartOffset = 0;
|
|
@@ -50,13 +51,14 @@ export class LoopController extends EventEmitter {
|
|
|
50
51
|
return this._status;
|
|
51
52
|
}
|
|
52
53
|
start() {
|
|
53
|
-
if (this.
|
|
54
|
+
if (this._loopActive)
|
|
54
55
|
return;
|
|
56
|
+
this._loopActive = true;
|
|
55
57
|
this.skippedCount = 0;
|
|
56
58
|
this.logStream?.end();
|
|
57
59
|
this.abortController = new AbortController();
|
|
58
60
|
this.logStream = fs.createWriteStream(this.logPath, { flags: "a" });
|
|
59
|
-
this.loopPromise = this.run();
|
|
61
|
+
this.loopPromise = this.run().finally(() => { this._loopActive = false; });
|
|
60
62
|
if (this.sessionStartedAt === null) {
|
|
61
63
|
this.sessionStartedAt = new Date().toISOString();
|
|
62
64
|
}
|
package/dist/daemon/index.js
CHANGED
|
@@ -3,7 +3,8 @@ import { TaskManager } from "./managers/task-manager.js";
|
|
|
3
3
|
import { IpcServer } from "./server/index.js";
|
|
4
4
|
import { HttpApiServer } from "./http/server.js";
|
|
5
5
|
import { FileWatcher } from "./watcher/index.js";
|
|
6
|
-
import { writeDaemonPid, removeDaemonPid, writeDaemonSignature, removeDaemonSignature, computeCodeSignature, migrateTasksToJson, migrateLoopsToJson, } from "./state/index.js";
|
|
6
|
+
import { writeDaemonPid, removeDaemonPid, writeDaemonSignature, removeDaemonSignature, computeCodeSignature, migrateTasksToJson, migrateLoopsToJson, setSelfWriteNotifier, } from "./state/index.js";
|
|
7
|
+
import { setProjectSelfWriteNotifier } from "./managers/project-manager.js";
|
|
7
8
|
import { t } from "../shared/i18n/index.js";
|
|
8
9
|
import { daemonLog } from "./daemon-log.js";
|
|
9
10
|
async function main() {
|
|
@@ -37,6 +38,8 @@ async function main() {
|
|
|
37
38
|
fileWatcher.setManagers(manager, taskManager, manager["projectManager"]);
|
|
38
39
|
fileWatcher.start();
|
|
39
40
|
daemonLog(`file watcher started for hot-reloading JSON configs`);
|
|
41
|
+
setSelfWriteNotifier((filePath, content) => fileWatcher.registerSelfWrite(filePath, content));
|
|
42
|
+
setProjectSelfWriteNotifier((filePath, content) => fileWatcher.registerSelfWrite(filePath, content));
|
|
40
43
|
let shuttingDown = false;
|
|
41
44
|
const cleanup = async () => {
|
|
42
45
|
if (shuttingDown)
|
|
@@ -4,6 +4,10 @@ import { daemonLog } from "../daemon-log.js";
|
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { getDataDir, projectsJson } from "../../shared/config/paths.js";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
let selfWriteNotifier = null;
|
|
8
|
+
export function setProjectSelfWriteNotifier(notifier) {
|
|
9
|
+
selfWriteNotifier = notifier;
|
|
10
|
+
}
|
|
7
11
|
export class ProjectManager {
|
|
8
12
|
constructor() {
|
|
9
13
|
this.projects = new Map();
|
|
@@ -68,7 +72,10 @@ export class ProjectManager {
|
|
|
68
72
|
}
|
|
69
73
|
saveAllProjects() {
|
|
70
74
|
const all = Array.from(this.projects.values());
|
|
71
|
-
|
|
75
|
+
const content = JSON.stringify(all, null, 2);
|
|
76
|
+
writeFileAtomic(projectsJson(), content);
|
|
77
|
+
if (selfWriteNotifier)
|
|
78
|
+
selfWriteNotifier(projectsJson(), content);
|
|
72
79
|
}
|
|
73
80
|
saveProject(project) {
|
|
74
81
|
this.projects.set(project.id, project);
|
|
@@ -5,6 +5,10 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { removeIfExists, writeFileAtomic } from "../../shared/fs-utils.js";
|
|
6
6
|
import { getLoopsDir, getTasksDir, getLogsDir, logFile, getPidFile, getSignatureFile, getSocketPath, loopsJson, tasksJson, getDataDir, } from "../../shared/config/paths.js";
|
|
7
7
|
export { getDataDir, getPidFile, getSocketPath } from "../../shared/config/paths.js";
|
|
8
|
+
let selfWriteNotifier = null;
|
|
9
|
+
export function setSelfWriteNotifier(notifier) {
|
|
10
|
+
selfWriteNotifier = notifier;
|
|
11
|
+
}
|
|
8
12
|
function ensureDirs() {
|
|
9
13
|
fs.mkdirSync(getDataDir(), { recursive: true });
|
|
10
14
|
fs.mkdirSync(getLogsDir(), { recursive: true });
|
|
@@ -23,7 +27,10 @@ function readJsonArray(filePath) {
|
|
|
23
27
|
}
|
|
24
28
|
function writeJsonArray(filePath, items) {
|
|
25
29
|
ensureDirs();
|
|
26
|
-
|
|
30
|
+
const content = JSON.stringify(items, null, 2);
|
|
31
|
+
writeFileAtomic(filePath, content);
|
|
32
|
+
if (selfWriteNotifier)
|
|
33
|
+
selfWriteNotifier(filePath, content);
|
|
27
34
|
}
|
|
28
35
|
export function migrateLoopsToJson() {
|
|
29
36
|
ensureDirs();
|
|
@@ -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
|
|
113
|
-
if (
|
|
114
|
-
|
|
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
|
|
61
|
-
if (
|
|
62
|
-
|
|
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 };
|
package/dist/shared/i18n/en.json
CHANGED
|
@@ -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 ",
|
package/dist/shared/ui/format.js
CHANGED
|
@@ -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
|
|
26
|
-
const
|
|
27
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|