loop-task 2.0.13 → 2.0.14
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/core/loop-controller.js +11 -3
- package/dist/core/resolve-cwd.js +8 -0
- package/dist/daemon/manager.js +15 -8
- package/dist/daemon/projects.js +6 -2
- package/dist/daemon/server.js +4 -4
- package/dist/i18n/en.json +5 -2
- package/dist/tui/App.js +1 -1
- package/dist/tui/components/Inspector.js +6 -4
- package/dist/tui/components/ProjectForm.js +11 -3
- package/dist/tui/components/ProjectsPage.js +1 -1
- package/dist/tui/components/RightPanel.js +3 -3
- package/dist/tui/daemon.js +4 -4
- package/package.json +2 -1
|
@@ -9,6 +9,7 @@ import { computePhase, alignToPhase } from "./scheduling.js";
|
|
|
9
9
|
import { t } from "../i18n/index.js";
|
|
10
10
|
import { parseStdout } from "./context-parser.js";
|
|
11
11
|
import { interpolate } from "./template.js";
|
|
12
|
+
import { resolveEffectiveCwd } from "./resolve-cwd.js";
|
|
12
13
|
export class LoopController extends EventEmitter {
|
|
13
14
|
abortController;
|
|
14
15
|
runAbortController = null;
|
|
@@ -30,6 +31,7 @@ export class LoopController extends EventEmitter {
|
|
|
30
31
|
options;
|
|
31
32
|
logPath;
|
|
32
33
|
taskResolver;
|
|
34
|
+
projectDirectory;
|
|
33
35
|
remainingDelayMs = null;
|
|
34
36
|
logStream = null;
|
|
35
37
|
loopPromise = null;
|
|
@@ -37,12 +39,13 @@ export class LoopController extends EventEmitter {
|
|
|
37
39
|
runHistory = [];
|
|
38
40
|
currentRunStartOffset = 0;
|
|
39
41
|
skippedCount = 0;
|
|
40
|
-
constructor(id, options, logPath, taskResolver, state) {
|
|
42
|
+
constructor(id, options, logPath, taskResolver, state, projectDirectory) {
|
|
41
43
|
super();
|
|
42
44
|
this.id = id;
|
|
43
45
|
this.options = options;
|
|
44
46
|
this.logPath = logPath;
|
|
45
47
|
this.taskResolver = taskResolver;
|
|
48
|
+
this.projectDirectory = projectDirectory;
|
|
46
49
|
this.abortController = new AbortController();
|
|
47
50
|
this.createdAt = state?.createdAt ?? new Date().toISOString();
|
|
48
51
|
this.runCount = state?.runCount ?? 0;
|
|
@@ -91,6 +94,11 @@ export class LoopController extends EventEmitter {
|
|
|
91
94
|
if (interruptCurrentRun) {
|
|
92
95
|
this.runAbortController?.abort();
|
|
93
96
|
}
|
|
97
|
+
this.abortController.abort();
|
|
98
|
+
if (this.resumeResolve) {
|
|
99
|
+
this.resumeResolve();
|
|
100
|
+
this.resumeResolve = null;
|
|
101
|
+
}
|
|
94
102
|
this.emit("stopped");
|
|
95
103
|
}
|
|
96
104
|
}
|
|
@@ -324,7 +332,7 @@ export class LoopController extends EventEmitter {
|
|
|
324
332
|
const task = this.options.taskId ? this.taskResolver(this.options.taskId) : null;
|
|
325
333
|
const command = task?.command ?? this.options.command;
|
|
326
334
|
const commandArgs = task?.commandArgs ?? this.options.commandArgs;
|
|
327
|
-
const cwd = this.options.cwd;
|
|
335
|
+
const cwd = resolveEffectiveCwd(this.options.cwd, this.projectDirectory);
|
|
328
336
|
const chainContext = {};
|
|
329
337
|
const hasChainTasks = !!(task?.onSuccessTaskId || task?.onFailureTaskId);
|
|
330
338
|
const result = await executeCommand(command, commandArgs, cwd, this.logStream, AbortSignal.any([signal, this.runAbortController.signal]), this.runCount, hasChainTasks);
|
|
@@ -387,7 +395,7 @@ export class LoopController extends EventEmitter {
|
|
|
387
395
|
});
|
|
388
396
|
const interpolatedCommand = interpolate(chainTask.command, chainContext);
|
|
389
397
|
const interpolatedArgs = chainTask.commandArgs.map(a => interpolate(a, chainContext));
|
|
390
|
-
const chainResult = await executeCommand(interpolatedCommand, interpolatedArgs,
|
|
398
|
+
const chainResult = await executeCommand(interpolatedCommand, interpolatedArgs, cwd, this.logStream, signal, this.runCount, true, true);
|
|
391
399
|
if (chainResult.stdout) {
|
|
392
400
|
const parsed = parseStdout(chainResult.stdout);
|
|
393
401
|
if (parsed !== null) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function resolveEffectiveCwd(loopCwd, projectDirectory) {
|
|
3
|
+
if (!loopCwd)
|
|
4
|
+
return projectDirectory || process.cwd();
|
|
5
|
+
if (path.isAbsolute(loopCwd))
|
|
6
|
+
return loopCwd;
|
|
7
|
+
return path.join(projectDirectory || process.cwd(), loopCwd);
|
|
8
|
+
}
|
package/dist/daemon/manager.js
CHANGED
|
@@ -13,6 +13,9 @@ export class LoopManager {
|
|
|
13
13
|
this.projectManager = projectManager || new ProjectManager();
|
|
14
14
|
}
|
|
15
15
|
taskResolver = (taskId) => this.taskManager.get(taskId);
|
|
16
|
+
getProjectDirectory(projectId) {
|
|
17
|
+
return this.projectManager.get(projectId)?.directory;
|
|
18
|
+
}
|
|
16
19
|
init() {
|
|
17
20
|
// Initialize projects first
|
|
18
21
|
this.projectManager.init();
|
|
@@ -39,6 +42,7 @@ export class LoopManager {
|
|
|
39
42
|
offset: meta.offset ?? null,
|
|
40
43
|
};
|
|
41
44
|
const logPath = getLogPath(meta.id);
|
|
45
|
+
const projectDir = this.getProjectDirectory(options.projectId ?? "default");
|
|
42
46
|
const controller = new LoopController(meta.id, options, logPath, this.taskResolver, {
|
|
43
47
|
status: meta.status,
|
|
44
48
|
createdAt: meta.createdAt,
|
|
@@ -50,7 +54,7 @@ export class LoopManager {
|
|
|
50
54
|
nextRunAt: meta.nextRunAt,
|
|
51
55
|
remainingDelayMs: meta.remainingDelayMs,
|
|
52
56
|
runHistory: meta.runHistory,
|
|
53
|
-
});
|
|
57
|
+
}, projectDir);
|
|
54
58
|
this.loops.set(meta.id, {
|
|
55
59
|
controller,
|
|
56
60
|
options,
|
|
@@ -69,7 +73,8 @@ export class LoopManager {
|
|
|
69
73
|
start(options, intervalHuman) {
|
|
70
74
|
const id = crypto.randomUUID().slice(0, 8);
|
|
71
75
|
const logPath = getLogPath(id);
|
|
72
|
-
const
|
|
76
|
+
const projectDir = this.getProjectDirectory(options.projectId ?? "default");
|
|
77
|
+
const controller = new LoopController(id, options, logPath, this.taskResolver, undefined, projectDir);
|
|
73
78
|
this.loops.set(id, { controller, options, intervalHuman });
|
|
74
79
|
controller.start();
|
|
75
80
|
this.wireEvents(id, controller, options, intervalHuman);
|
|
@@ -112,11 +117,11 @@ export class LoopManager {
|
|
|
112
117
|
listProjects() {
|
|
113
118
|
return this.projectManager.getAll();
|
|
114
119
|
}
|
|
115
|
-
createProject(name, color) {
|
|
116
|
-
return this.projectManager.create(name, color);
|
|
120
|
+
createProject(name, color, directory) {
|
|
121
|
+
return this.projectManager.create(name, color, directory);
|
|
117
122
|
}
|
|
118
|
-
updateProject(id, name, color) {
|
|
119
|
-
this.projectManager.update(id, name, color);
|
|
123
|
+
updateProject(id, name, color, directory) {
|
|
124
|
+
this.projectManager.update(id, name, color, directory);
|
|
120
125
|
}
|
|
121
126
|
deleteProject(id) {
|
|
122
127
|
for (const [loopId, entry] of this.loops) {
|
|
@@ -251,6 +256,7 @@ export class LoopManager {
|
|
|
251
256
|
offset: meta.offset ?? null,
|
|
252
257
|
};
|
|
253
258
|
const logPath = getLogPath(meta.id);
|
|
259
|
+
const projectDir = this.getProjectDirectory(options.projectId ?? "default");
|
|
254
260
|
const controller = new LoopController(meta.id, options, logPath, this.taskResolver, {
|
|
255
261
|
status: meta.status,
|
|
256
262
|
createdAt: meta.createdAt,
|
|
@@ -262,7 +268,7 @@ export class LoopManager {
|
|
|
262
268
|
nextRunAt: meta.nextRunAt,
|
|
263
269
|
remainingDelayMs: meta.remainingDelayMs,
|
|
264
270
|
runHistory: meta.runHistory,
|
|
265
|
-
});
|
|
271
|
+
}, projectDir);
|
|
266
272
|
this.loops.set(meta.id, { controller, options, intervalHuman: meta.intervalHuman });
|
|
267
273
|
this.wireEvents(meta.id, controller, options, meta.intervalHuman);
|
|
268
274
|
if (shouldAutoStart(meta.status)) {
|
|
@@ -286,6 +292,7 @@ export class LoopManager {
|
|
|
286
292
|
offset: meta.offset ?? null,
|
|
287
293
|
};
|
|
288
294
|
const logPath = getLogPath(meta.id);
|
|
295
|
+
const projectDir = this.getProjectDirectory(options.projectId ?? "default");
|
|
289
296
|
const controller = new LoopController(meta.id, options, logPath, this.taskResolver, {
|
|
290
297
|
status: meta.status,
|
|
291
298
|
createdAt: meta.createdAt,
|
|
@@ -297,7 +304,7 @@ export class LoopManager {
|
|
|
297
304
|
nextRunAt: meta.nextRunAt,
|
|
298
305
|
remainingDelayMs: meta.remainingDelayMs,
|
|
299
306
|
runHistory: meta.runHistory,
|
|
300
|
-
});
|
|
307
|
+
}, projectDir);
|
|
301
308
|
this.loops.set(meta.id, { controller, options, intervalHuman: meta.intervalHuman });
|
|
302
309
|
this.wireEvents(meta.id, controller, options, meta.intervalHuman);
|
|
303
310
|
if (shouldAutoStart(meta.status)) {
|
package/dist/daemon/projects.js
CHANGED
|
@@ -77,6 +77,7 @@ export class ProjectManager {
|
|
|
77
77
|
id: "default",
|
|
78
78
|
name: "Default",
|
|
79
79
|
color: "#ffffff",
|
|
80
|
+
directory: "",
|
|
80
81
|
createdAt: new Date().toISOString(),
|
|
81
82
|
isSystem: true,
|
|
82
83
|
isDefault: true,
|
|
@@ -89,12 +90,13 @@ export class ProjectManager {
|
|
|
89
90
|
get(id) {
|
|
90
91
|
return this.projects.get(id);
|
|
91
92
|
}
|
|
92
|
-
create(name, color) {
|
|
93
|
+
create(name, color, directory) {
|
|
93
94
|
const id = crypto.randomBytes(4).toString("hex");
|
|
94
95
|
const project = {
|
|
95
96
|
id,
|
|
96
97
|
name,
|
|
97
98
|
color,
|
|
99
|
+
directory: directory ?? "",
|
|
98
100
|
createdAt: new Date().toISOString(),
|
|
99
101
|
isSystem: false,
|
|
100
102
|
isDefault: false,
|
|
@@ -102,7 +104,7 @@ export class ProjectManager {
|
|
|
102
104
|
this.saveProject(project);
|
|
103
105
|
return project;
|
|
104
106
|
}
|
|
105
|
-
update(id, name, color) {
|
|
107
|
+
update(id, name, color, directory) {
|
|
106
108
|
const project = this.projects.get(id);
|
|
107
109
|
if (!project)
|
|
108
110
|
throw new Error(`Project ${id} not found`);
|
|
@@ -111,6 +113,8 @@ export class ProjectManager {
|
|
|
111
113
|
project.name = name;
|
|
112
114
|
if (color)
|
|
113
115
|
project.color = color;
|
|
116
|
+
if (directory !== undefined)
|
|
117
|
+
project.directory = directory;
|
|
114
118
|
this.saveProject(project);
|
|
115
119
|
}
|
|
116
120
|
delete(id) {
|
package/dist/daemon/server.js
CHANGED
|
@@ -200,23 +200,23 @@ export class IpcServer {
|
|
|
200
200
|
break;
|
|
201
201
|
}
|
|
202
202
|
case "project-create": {
|
|
203
|
-
const { name, color } = request.payload;
|
|
203
|
+
const { name, color, directory } = request.payload;
|
|
204
204
|
if (!name || !name.trim()) {
|
|
205
205
|
send(socket, { type: "error", message: t("project.error.nameRequired") });
|
|
206
206
|
break;
|
|
207
207
|
}
|
|
208
|
-
const project = this.manager.createProject(name.trim(), color);
|
|
208
|
+
const project = this.manager.createProject(name.trim(), color, directory);
|
|
209
209
|
send(socket, { type: "ok", data: project });
|
|
210
210
|
break;
|
|
211
211
|
}
|
|
212
212
|
case "project-update": {
|
|
213
|
-
const { id, name, color } = request.payload;
|
|
213
|
+
const { id, name, color, directory } = request.payload;
|
|
214
214
|
if (!name || !name.trim()) {
|
|
215
215
|
send(socket, { type: "error", message: t("project.error.nameEmpty") });
|
|
216
216
|
break;
|
|
217
217
|
}
|
|
218
218
|
try {
|
|
219
|
-
this.manager.updateProject(id, name.trim(), color);
|
|
219
|
+
this.manager.updateProject(id, name.trim(), color, directory);
|
|
220
220
|
send(socket, { type: "ok" });
|
|
221
221
|
}
|
|
222
222
|
catch (err) {
|
package/dist/i18n/en.json
CHANGED
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
"board.hintInterval": "How often to run, e.g. 30s, 5m, 30m, 1h, 1d",
|
|
261
261
|
"board.hintCommand": "Full command line. Quote args with spaces",
|
|
262
262
|
"board.hintDescription": "Short label shown in the list. Blank uses the command",
|
|
263
|
-
"board.hintCwd": "
|
|
263
|
+
"board.hintCwd": "Leave blank to inherit from project directory, or set a specific path",
|
|
264
264
|
"board.hintRunNow": "run once now then every interval, or wait the first interval",
|
|
265
265
|
"board.hintMaxRuns": "Stop after N runs. Leave blank to run forever",
|
|
266
266
|
"board.hintTask": "Pick a previously defined task",
|
|
@@ -440,6 +440,8 @@
|
|
|
440
440
|
"project.wizard.nameHint": "A short name for this project",
|
|
441
441
|
"project.wizard.colorPrompt": "Pick a color for this project",
|
|
442
442
|
"project.wizard.colorHint": "Choose a color to identify this project",
|
|
443
|
+
"project.wizard.directoryPrompt": "Working directory? (optional)",
|
|
444
|
+
"project.wizard.directoryHint": "Default directory for loops in this project. Leave blank to inherit",
|
|
443
445
|
"project.error.updateFailed": "Failed to update project",
|
|
444
446
|
"project.error.deleteFailed": "Failed to delete project",
|
|
445
447
|
"project.toastCreated": "Project \"{name}\" created",
|
|
@@ -464,6 +466,7 @@
|
|
|
464
466
|
"project.headerCreated": "CREATED",
|
|
465
467
|
"project.inspectorEmpty": " Select a project to view details",
|
|
466
468
|
"project.fieldId": "ID: ",
|
|
469
|
+
"project.fieldDirectory": "Dir: ",
|
|
467
470
|
"project.fieldCreated": "Created: ",
|
|
468
471
|
"project.fieldLoops": "Loops: ",
|
|
469
472
|
"project.hintSearch": "search",
|
|
@@ -573,7 +576,7 @@
|
|
|
573
576
|
"wizard.runNowWait": "Wait for the first interval",
|
|
574
577
|
"wizard.runNowNow": "Run now, then every interval",
|
|
575
578
|
"wizard.cwdPrompt": "Working directory? (optional)",
|
|
576
|
-
"wizard.cwdHint": "
|
|
579
|
+
"wizard.cwdHint": "Leave blank to inherit from project, or set a specific directory",
|
|
577
580
|
"wizard.descriptionPrompt": "Description?",
|
|
578
581
|
"wizard.descriptionHint": "Short label shown in the list",
|
|
579
582
|
"wizard.maxRunsPrompt": "Max runs? (optional)",
|
package/dist/tui/App.js
CHANGED
|
@@ -655,7 +655,7 @@ export function App(props) {
|
|
|
655
655
|
const tabCounts = { loops: loops.length, tasks: tasks.length, projects: projects.length };
|
|
656
656
|
return (_jsxs(Box, { flexDirection: "column", width: "100%", height: process.stdout.rows || 24, backgroundColor: theme.bg.base, children: [_jsx(Header, { daemonStatus: daemonStatus, counts: counts, activeTab: activeTab, onTabChange: setActiveTab, tabCounts: tabCounts }), _jsx(Box, { flexGrow: 1, children: view === "create" ? (_jsx(CreateView, { mode: editTarget && !cloneMode ? "edit" : "create", editId: editTarget && !cloneMode ? editTarget.id : null, initial: createInitialValues(editTarget, currentProjectId), selectedTaskId: pendingTaskSelection?.id ?? null, selectedTaskName: pendingTaskSelection?.name ?? null, tasks: tasks, projects: projects, currentProjectId: currentProjectId, onCancel: cancelCreate, onDone: onCreateDone, onChooseTask: handleChooseTask })) : TASK_FORM_VIEWS.has(view) ? (_jsx(TaskForm, { mode: view === "task-edit" ? "edit" : "create", editTask: editTask, onCancel: cancelTask, onDone: onTaskDone })) : view === "project-create" || view === "project-edit" ? (_jsx(ProjectFormView, { mode: view === "project-edit" ? "edit" : "create", editProject: view === "project-edit" ? editProject : null, onCancel: cancelProject, onDone: onProjectDone })) : (
|
|
657
657
|
// Board view: left panel + right panel (+ optional debug panel)
|
|
658
|
-
_jsxs(Box, { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, children: [_jsx(LeftPanel, { isFocused: focusedPanel === "left" && !anyModalOpen, navActive: inputOwner === "panel", activeTab: activeTab, query: leftPanelQuery, loops: visible, selectedIndex: clampedIndex, filters: filters, sort: sort, breakpoint: breakpoint, projects: projects, onSelect: (index) => setSelectedIndex(index), onActivate: (index) => { setSelectedIndex(index); }, tasks: filteredTasks, taskSelectedIndex: taskClampedIndex, onTaskSelect: (index) => setTaskSelectedIndex(index), onTaskActivate: (index) => { setTaskSelectedIndex(index); setEditTask(filteredTasks[index] ?? null); push("task-edit"); }, onStatusCycle: () => setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })), onSortCycle: () => setSort(cycleSortMode(sort)), onSelectProject: () => setActiveTab("projects"), currentProjectName: currentProjectId === "all" ? t("project.showAll") : (projects.find(p => p.id === currentProjectId)?.name ?? "Default"), projectFilters: projectFilters, projectSelectedIndex: projectClampedIndex, onProjectSelect: (index) => setProjectSelectedIndex(index), onProjectActivate: (index) => { setProjectSelectedIndex(index); }, projectLoops: loops }), _jsx(RightPanel, { isFocused: focusedPanel === "right" && !anyModalOpen, navActive: inputOwner === "panel", activeTab: activeTab, loop: selected, selectedRunIndex: selectedRunIndex, onSelectRun: (index) => setSelectedRunIndex(index), onOpenRun: handleOpenRunLog, selectedTask: selectedTask, allTasks: tasks, selectedProject: selectedProjectEntity, projectLoopCount: projectLoopCount, onProjectEdit: () => { if (selectedProjectEntity && !selectedProjectEntity.isSystem) {
|
|
658
|
+
_jsxs(Box, { flexDirection: breakpoint === "narrow" ? "column" : "row", flexGrow: 1, children: [_jsx(LeftPanel, { isFocused: focusedPanel === "left" && !anyModalOpen, navActive: inputOwner === "panel", activeTab: activeTab, query: leftPanelQuery, loops: visible, selectedIndex: clampedIndex, filters: filters, sort: sort, breakpoint: breakpoint, projects: projects, onSelect: (index) => setSelectedIndex(index), onActivate: (index) => { setSelectedIndex(index); }, tasks: filteredTasks, taskSelectedIndex: taskClampedIndex, onTaskSelect: (index) => setTaskSelectedIndex(index), onTaskActivate: (index) => { setTaskSelectedIndex(index); setEditTask(filteredTasks[index] ?? null); push("task-edit"); }, onStatusCycle: () => setFilters((prev) => ({ ...prev, status: cycleStatusFilter(prev.status) })), onSortCycle: () => setSort(cycleSortMode(sort)), onSelectProject: () => setActiveTab("projects"), currentProjectName: currentProjectId === "all" ? t("project.showAll") : (projects.find(p => p.id === currentProjectId)?.name ?? "Default"), projectFilters: projectFilters, projectSelectedIndex: projectClampedIndex, onProjectSelect: (index) => setProjectSelectedIndex(index), onProjectActivate: (index) => { setProjectSelectedIndex(index); }, projectLoops: loops }), _jsx(RightPanel, { isFocused: focusedPanel === "right" && !anyModalOpen, navActive: inputOwner === "panel", activeTab: activeTab, loop: selected, selectedRunIndex: selectedRunIndex, onSelectRun: (index) => setSelectedRunIndex(index), onOpenRun: handleOpenRunLog, selectedTask: selectedTask, allTasks: tasks, selectedProject: selectedProjectEntity, projectLoopCount: projectLoopCount, projects: projects, onProjectEdit: () => { if (selectedProjectEntity && !selectedProjectEntity.isSystem) {
|
|
659
659
|
commandHandlers["edit"]?.();
|
|
660
660
|
} }, onProjectDelete: () => { if (selectedProjectEntity && !selectedProjectEntity.isSystem) {
|
|
661
661
|
commandHandlers["delete"]?.();
|
|
@@ -3,17 +3,17 @@ import { Box, Text } from "ink";
|
|
|
3
3
|
import { darkTheme as theme, statusColor } from "../theme.js";
|
|
4
4
|
import { describeLoop, commandLine, timeAgo, timeUntil } from "../format.js";
|
|
5
5
|
import { t } from "../../i18n/index.js";
|
|
6
|
+
import { resolveEffectiveCwd } from "../../core/resolve-cwd.js";
|
|
6
7
|
const LABEL_WIDTH = 11;
|
|
7
8
|
function Field(props) {
|
|
8
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 })] }));
|
|
9
10
|
}
|
|
10
11
|
const DIVIDER = "\u2500".repeat(40);
|
|
11
|
-
/** Muted-label field for identity block (de-emphasized). */
|
|
12
12
|
function MutedField(props) {
|
|
13
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 })] }));
|
|
14
14
|
}
|
|
15
15
|
export function Inspector(props) {
|
|
16
|
-
const { loop } = props;
|
|
16
|
+
const { loop, projects } = props;
|
|
17
17
|
if (!loop) {
|
|
18
18
|
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 }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorEmpty") }) })] }));
|
|
19
19
|
}
|
|
@@ -23,9 +23,11 @@ export function Inspector(props) {
|
|
|
23
23
|
const lastExit = loop.lastExitCode !== null ? String(loop.lastExitCode) : t("format.dash");
|
|
24
24
|
const nextRun = loop.nextRunAt ? t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) }) : t("format.dash");
|
|
25
25
|
const pid = loop.pid ? String(loop.pid) : t("format.dash");
|
|
26
|
-
|
|
26
|
+
const projectDirectory = projects?.find((p) => p.id === loop.projectId)?.directory;
|
|
27
|
+
const effectiveCwd = resolveEffectiveCwd(loop.cwd, projectDirectory);
|
|
28
|
+
const showEffective = loop.cwd !== effectiveCwd;
|
|
27
29
|
const fullCmd = commandLine(loop.command, loop.commandArgs);
|
|
28
30
|
const desc = describeLoop(loop);
|
|
29
31
|
const showCommand = desc !== fullCmd;
|
|
30
|
-
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:
|
|
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 }) })] }));
|
|
31
33
|
}
|
|
@@ -35,7 +35,15 @@ export function ProjectFormView(props) {
|
|
|
35
35
|
return (_jsx(SelectValueField, { label: value || null, placeholder: t("project.wizard.colorPrompt"), isActive: isActive }));
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
|
-
|
|
38
|
+
{
|
|
39
|
+
key: "directory",
|
|
40
|
+
prompt: t("project.wizard.directoryPrompt"),
|
|
41
|
+
hint: t("project.wizard.directoryHint"),
|
|
42
|
+
required: false,
|
|
43
|
+
inputType: "text",
|
|
44
|
+
defaultValue: mode === "create" ? process.cwd() : (editProject?.directory || undefined),
|
|
45
|
+
},
|
|
46
|
+
], [editProject, mode]);
|
|
39
47
|
const handleComplete = useCallback((values) => {
|
|
40
48
|
const name = (values.name ?? "").trim();
|
|
41
49
|
if (!name)
|
|
@@ -43,12 +51,12 @@ export function ProjectFormView(props) {
|
|
|
43
51
|
const colorKey = values.color ?? "cyan";
|
|
44
52
|
const color = PROJECT_COLORS[colorKey] ?? PROJECT_COLORS.cyan;
|
|
45
53
|
if (mode === "edit" && editProject) {
|
|
46
|
-
updateProject(editProject.id, name, color)
|
|
54
|
+
updateProject(editProject.id, name, color, values.directory?.trim() || undefined)
|
|
47
55
|
.then(() => onDone(true, name))
|
|
48
56
|
.catch(() => { });
|
|
49
57
|
}
|
|
50
58
|
else {
|
|
51
|
-
createProject(name, color)
|
|
59
|
+
createProject(name, color, values.directory?.trim() || undefined)
|
|
52
60
|
.then(() => onDone(false, name))
|
|
53
61
|
.catch(() => { });
|
|
54
62
|
}
|
|
@@ -91,7 +91,7 @@ export function ProjectsPage(props) {
|
|
|
91
91
|
if (p)
|
|
92
92
|
props.onNavigateEdit(p);
|
|
93
93
|
}
|
|
94
|
-
}, children: null }))] }), _jsx(Box, { flexGrow: 1, flexDirection: "column", marginLeft: 1, children: selected ? (_jsxs(React.Fragment, { children: [_jsxs(Box, { borderStyle: "single", borderColor: theme.accent.brand, padding: 1, flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: selected.color, bold: true, children: "\u25CF" }), _jsx(Text, { color: theme.text.primary, bold: true, children: " " + selected.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "ID: " }), _jsx(Text, { color: theme.text.secondary, children: selected.id })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "Created: " }), _jsx(Text, { color: theme.text.secondary, children: selected.createdAt })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "Loops: " }), _jsx(Text, { color: theme.text.secondary, children: t("project.loopCount", { count: loopCount }) })] }), selected.isSystem && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.systemLabel") }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(FocusableButton, { label: `${t("project.editProjectLabel")} (${t("project.keyEditHint")})`, color: theme.accent.brand, onPress: () => {
|
|
94
|
+
}, children: null }))] }), _jsx(Box, { flexGrow: 1, flexDirection: "column", marginLeft: 1, children: selected ? (_jsxs(React.Fragment, { children: [_jsxs(Box, { borderStyle: "single", borderColor: theme.accent.brand, padding: 1, flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: selected.color, bold: true, children: "\u25CF" }), _jsx(Text, { color: theme.text.primary, bold: true, children: " " + selected.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "ID: " }), _jsx(Text, { color: theme.text.secondary, children: selected.id })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "Created: " }), _jsx(Text, { color: theme.text.secondary, children: selected.createdAt })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "Loops: " }), _jsx(Text, { color: theme.text.secondary, children: t("project.loopCount", { count: loopCount }) })] }), selected.directory ? (_jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldDirectory") }), _jsx(Text, { color: theme.text.secondary, children: selected.directory })] })) : null, selected.directory ? (_jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: "Dir: " }), _jsx(Text, { color: theme.text.secondary, children: selected.directory })] })) : null, selected.isSystem && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.systemLabel") }) }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [_jsx(FocusableButton, { label: `${t("project.editProjectLabel")} (${t("project.keyEditHint")})`, color: theme.accent.brand, onPress: () => {
|
|
95
95
|
if (props.onNavigateEdit && selected) {
|
|
96
96
|
props.onNavigateEdit(selected);
|
|
97
97
|
}
|
|
@@ -8,9 +8,9 @@ import { FocusableButton } from "./FocusableButton.js";
|
|
|
8
8
|
import { commandLine } from "../format.js";
|
|
9
9
|
const DIVIDER = "\u2500".repeat(40);
|
|
10
10
|
export function RightPanel(props) {
|
|
11
|
-
const { isFocused, navActive = true, activeTab, loop, selectedRunIndex, onSelectRun, onOpenRun, selectedTask, allTasks, selectedProject, projectLoopCount, onProjectEdit, onProjectDelete, } = props;
|
|
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 }), _jsx(RunHistory, { loop: loop, selectedRunIndex: selectedRunIndex, onSelectRun: onSelectRun, onOpenRun: onOpenRun, isFocused: isFocused, navActive: navActive })] })) }));
|
|
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 })] })) }));
|
|
14
14
|
}
|
|
15
15
|
function TaskInspector(props) {
|
|
16
16
|
const { task, allTasks } = props;
|
|
@@ -30,5 +30,5 @@ function ProjectInspector(props) {
|
|
|
30
30
|
if (!project) {
|
|
31
31
|
return (_jsx(Box, { padding: 1, flexGrow: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.inspectorEmpty") }) }));
|
|
32
32
|
}
|
|
33
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: project.color, bold: true, children: "\u25CF" }), _jsx(Text, { color: theme.text.primary, bold: true, children: " " + project.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldId") }), _jsx(Text, { color: theme.text.secondary, children: project.id })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldLoops") }), _jsx(Text, { color: theme.text.secondary, children: t("project.loopCount", { count: String(loopCount) }) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldCreated") }), _jsx(Text, { color: theme.text.secondary, children: project.createdAt.slice(0, 10) })] }), project.isSystem && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.systemLabel") }) })), !project.isSystem && (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [onEdit && (_jsx(FocusableButton, { label: `${t("project.editProjectLabel")} (${t("project.keyEditHint")})`, color: theme.accent.project, onPress: onEdit })), onDelete && (_jsx(FocusableButton, { label: `${t("project.deleteProjectLabel")} (${t("project.keyDeleteHint")})`, color: theme.semantic.danger, variant: "danger", onPress: onDelete }))] }))] }));
|
|
33
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: project.color, bold: true, children: "\u25CF" }), _jsx(Text, { color: theme.text.primary, bold: true, children: " " + project.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldId") }), _jsx(Text, { color: theme.text.secondary, children: project.id })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldLoops") }), _jsx(Text, { color: theme.text.secondary, children: t("project.loopCount", { count: String(loopCount) }) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldCreated") }), _jsx(Text, { color: theme.text.secondary, children: project.createdAt.slice(0, 10) })] }), project.directory && (_jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldDirectory") }), _jsx(Text, { color: theme.text.secondary, children: project.directory })] })), project.isSystem && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.systemLabel") }) })), !project.isSystem && (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [onEdit && (_jsx(FocusableButton, { label: `${t("project.editProjectLabel")} (${t("project.keyEditHint")})`, color: theme.accent.project, onPress: onEdit })), onDelete && (_jsx(FocusableButton, { label: `${t("project.deleteProjectLabel")} (${t("project.keyDeleteHint")})`, color: theme.semantic.danger, variant: "danger", onPress: onDelete }))] }))] }));
|
|
34
34
|
}
|
package/dist/tui/daemon.js
CHANGED
|
@@ -104,14 +104,14 @@ export async function listProjects() {
|
|
|
104
104
|
throw new Error(response.message);
|
|
105
105
|
return response.data;
|
|
106
106
|
}
|
|
107
|
-
export async function createProject(name, color) {
|
|
108
|
-
const response = await sendRequest({ type: "project-create", payload: { name, color } });
|
|
107
|
+
export async function createProject(name, color, directory) {
|
|
108
|
+
const response = await sendRequest({ type: "project-create", payload: { name, color, directory } });
|
|
109
109
|
if (response.type !== "ok")
|
|
110
110
|
throw new Error(response.message);
|
|
111
111
|
return response.data;
|
|
112
112
|
}
|
|
113
|
-
export async function updateProject(id, name, color) {
|
|
114
|
-
const response = await sendRequest({ type: "project-update", payload: { id, name, color } });
|
|
113
|
+
export async function updateProject(id, name, color, directory) {
|
|
114
|
+
const response = await sendRequest({ type: "project-update", payload: { id, name, color, directory } });
|
|
115
115
|
expectOk(response.message ?? t("project.error.updateFailed"), response.type);
|
|
116
116
|
}
|
|
117
117
|
export async function exportConfig() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.14",
|
|
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": {
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"ink-select-input": "^6.2.0",
|
|
57
57
|
"ink-spinner": "^5.0.0",
|
|
58
58
|
"ink-text-input": "^6.0.0",
|
|
59
|
+
"inversify-hooks": "^4.0.0",
|
|
59
60
|
"ms": "^2.1.3",
|
|
60
61
|
"react": "^19.2.7"
|
|
61
62
|
},
|