loop-task 2.1.12 → 2.1.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/README.md +27 -9
- package/dist/app/App.js +3 -1
- package/dist/cli.js +89 -2
- package/dist/client/commands.js +4 -1
- package/dist/client/project-commands.js +7 -2
- package/dist/core/context/template.js +1 -1
- package/dist/core/context/validate-context.js +2 -2
- package/dist/core/loop/chain-executor.js +11 -4
- package/dist/core/loop/loop-controller.js +9 -0
- package/dist/daemon/http/openapi.js +30 -3
- package/dist/daemon/http/route-loops.js +85 -0
- package/dist/daemon/http/route-misc.js +16 -0
- package/dist/daemon/http/route-projects.js +2 -2
- package/dist/daemon/http/route-tasks.js +58 -0
- package/dist/daemon/http/routes.js +3 -2
- package/dist/daemon/http/server.js +20 -2
- package/dist/daemon/index.js +69 -7
- package/dist/daemon/managers/loop-entry.js +2 -0
- package/dist/daemon/managers/loop-manager.js +4 -4
- package/dist/daemon/managers/project-manager.js +16 -2
- package/dist/daemon/mcp/index.js +2 -0
- package/dist/daemon/mcp/openapi-sync.js +50 -0
- package/dist/daemon/mcp/server.js +167 -0
- package/dist/daemon/mcp/tools.js +368 -0
- package/dist/daemon/server/handlers/index.js +8 -1
- package/dist/daemon/server/handlers/project-handlers.js +10 -5
- package/dist/daemon/server/handlers/settings-handlers.js +8 -0
- package/dist/daemon/server/index.js +3 -1
- package/dist/daemon/settings-manager.js +54 -0
- package/dist/entities/tasks/filters.js +1 -1
- package/dist/features/commands/commands.js +4 -2
- package/dist/features/commands/useCommandHandlers.js +32 -1
- package/dist/features/commands/useGlobalShortcuts.js +0 -2
- package/dist/features/overlays/ExportModal.js +1 -1
- package/dist/loop-config.js +1 -1
- package/dist/shared/clipboard.js +2 -2
- package/dist/shared/config/paths.js +3 -0
- package/dist/shared/container/index.js +2 -0
- package/dist/shared/hooks/useDaemonSettings.js +39 -0
- package/dist/shared/hooks/useUndoRedo.js +1 -1
- package/dist/shared/i18n/en.json +25 -3
- package/dist/shared/services/project-service.js +4 -4
- package/dist/shared/services/settings-service.js +49 -0
- package/dist/shared/services/types.js +1 -0
- package/dist/shared/ui/FocusableInput.js +1 -1
- package/dist/shared/ui/SelectModal.js +1 -1
- package/dist/shared/utils/syntax.js +3 -3
- package/dist/widgets/header/Header.js +15 -2
- package/dist/widgets/left-panel/Navigator.js +3 -2
- package/dist/widgets/left-panel/TaskBrowser.js +3 -2
- package/dist/widgets/log-modal/LogModal.js +1 -1
- package/dist/widgets/loop-form/WizardForm.js +2 -2
- package/dist/widgets/loop-form/useHandleComplete.js +4 -1
- package/dist/widgets/project-form/ProjectForm.js +11 -2
- package/dist/widgets/right-panel/Inspector.js +1 -1
- package/dist/widgets/right-panel/RightPanel.js +1 -1
- package/dist/widgets/task-form/TaskForm.js +24 -4
- package/package.json +4 -2
|
@@ -5,12 +5,14 @@ import { IpcTaskService } from "../services/task-service.js";
|
|
|
5
5
|
import { IpcProjectService } from "../services/project-service.js";
|
|
6
6
|
import { IpcLogService } from "../services/log-service.js";
|
|
7
7
|
import { IpcExportService } from "../services/export-service.js";
|
|
8
|
+
import { IpcSettingsService } from "../services/settings-service.js";
|
|
8
9
|
export function createContainer() {
|
|
9
10
|
const container = new Container();
|
|
10
11
|
container.bind(TYPES.LoopService).to(IpcLoopService);
|
|
11
12
|
container.bind(TYPES.TaskService).to(IpcTaskService);
|
|
12
13
|
container.bind(TYPES.ProjectService).to(IpcProjectService);
|
|
13
14
|
container.bind(TYPES.LogService).to(IpcLogService);
|
|
15
|
+
container.bind(TYPES.SettingsService).to(IpcSettingsService);
|
|
14
16
|
container.bind(TYPES.ExportService).toDynamicValue(() => {
|
|
15
17
|
const loopService = container.get(TYPES.LoopService);
|
|
16
18
|
const taskService = container.get(TYPES.TaskService);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useInject } from "./useInject.js";
|
|
3
|
+
import { TYPES } from "../services/types.js";
|
|
4
|
+
import { POLL_MS } from "../config/constants.js";
|
|
5
|
+
export function useDaemonSettings() {
|
|
6
|
+
const settingsService = useInject(TYPES.SettingsService);
|
|
7
|
+
const [state, setState] = useState({
|
|
8
|
+
httpApiEnabled: false,
|
|
9
|
+
mcpApiEnabled: false,
|
|
10
|
+
reachable: false,
|
|
11
|
+
});
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
let cancelled = false;
|
|
14
|
+
const poll = async () => {
|
|
15
|
+
try {
|
|
16
|
+
const settings = await settingsService.getSettings();
|
|
17
|
+
if (!cancelled) {
|
|
18
|
+
setState({
|
|
19
|
+
httpApiEnabled: settings.httpApiEnabled,
|
|
20
|
+
mcpApiEnabled: settings.mcpApiEnabled,
|
|
21
|
+
reachable: true,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
if (!cancelled) {
|
|
27
|
+
setState((prev) => ({ ...prev, reachable: false }));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
void poll();
|
|
32
|
+
const timer = setInterval(() => void poll(), POLL_MS);
|
|
33
|
+
return () => {
|
|
34
|
+
cancelled = true;
|
|
35
|
+
clearInterval(timer);
|
|
36
|
+
};
|
|
37
|
+
}, [settingsService]);
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useRef, useCallback } from "react";
|
|
2
2
|
/**
|
|
3
|
-
* Pure undo/redo stack logic
|
|
3
|
+
* Pure undo/redo stack logic exported for direct unit testing
|
|
4
4
|
* without needing React DOM or renderHook.
|
|
5
5
|
*/
|
|
6
6
|
export class UndoRedoStack {
|
package/dist/shared/i18n/en.json
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"cli.statusInterval": " Interval: {interval} ({duration})",
|
|
30
30
|
"cli.statusStatus": " Status: {status}",
|
|
31
31
|
"cli.statusRuns": " Runs: {runs} / {maxRuns}",
|
|
32
|
+
"cli.statusSilentChains": " Silent: {count} silent-chain runs",
|
|
32
33
|
"cli.statusCreated": " Created: {created}",
|
|
33
34
|
"cli.statusLastRun": " Last run: {lastRun} {exit} {duration}",
|
|
34
35
|
"cli.statusNextRun": " Next run: {nextRun}",
|
|
@@ -47,6 +48,8 @@
|
|
|
47
48
|
"cli.projectColorDescription": "Change a project's color",
|
|
48
49
|
"cli.projectDeleteDescription": "Delete a project (loops move to Default)",
|
|
49
50
|
"cli.optProjectColor": "Project color (name or #hex)",
|
|
51
|
+
"cli.optProjectDirectory": "Project working directory",
|
|
52
|
+
"cli.optProjectGithubSource": "GitHub repository in owner/repo format",
|
|
50
53
|
"cli.projectArgName": "Project name",
|
|
51
54
|
"cli.projectArgNewName": "New project name",
|
|
52
55
|
"cli.projectArgIdOrName": "Project id or name",
|
|
@@ -144,6 +147,7 @@
|
|
|
144
147
|
"board.headerSince": "SINCE",
|
|
145
148
|
"board.headerRuns": "RUNS",
|
|
146
149
|
"board.headerSkipped": "SKIP",
|
|
150
|
+
"board.headerSilent": "SILENT",
|
|
147
151
|
"board.headerStatus": "STATUS",
|
|
148
152
|
"board.headerDescription": "DESCRIPTION",
|
|
149
153
|
"board.headerTiming": "TIMING",
|
|
@@ -174,6 +178,7 @@
|
|
|
174
178
|
"board.fieldLastRun": "Last run:",
|
|
175
179
|
"board.fieldLastExit": "Last exit:",
|
|
176
180
|
"board.fieldNextRun": "Next run:",
|
|
181
|
+
"board.fieldSilentChains": "Silent:",
|
|
177
182
|
"board.fieldPid": "PID:",
|
|
178
183
|
"board.inherit": "(inherit)",
|
|
179
184
|
"board.unlimited": "∞",
|
|
@@ -210,6 +215,7 @@
|
|
|
210
215
|
"board.detailFieldLastExit": "Last exit: ",
|
|
211
216
|
"board.detailFieldNextRun": "Next run: ",
|
|
212
217
|
"board.detailFieldPid": "PID: ",
|
|
218
|
+
"board.detailFieldSilentChains": "Silent: ",
|
|
213
219
|
"board.detailSummaryStatus": "status",
|
|
214
220
|
"board.detailSummaryTiming": "next",
|
|
215
221
|
"board.detailSummaryInterval": "every",
|
|
@@ -291,7 +297,7 @@
|
|
|
291
297
|
"codeEditor.fieldHint": "press enter to open editor",
|
|
292
298
|
"codeEditor.copied": "Copied!",
|
|
293
299
|
"codeEditor.cleared": "Cleared",
|
|
294
|
-
"codeEditor.clipboardEmpty": "Clipboard empty
|
|
300
|
+
"codeEditor.clipboardEmpty": "Clipboard empty, right-click to paste",
|
|
295
301
|
"codeEditor.commandsTitle": "Parallel Commands",
|
|
296
302
|
"codeEditor.commandsHint": "tab: next . shift+tab: prev . ctrl+s: save all . esc: cancel",
|
|
297
303
|
"codeEditor.commandLabel": "Command",
|
|
@@ -400,6 +406,7 @@
|
|
|
400
406
|
"board.taskFieldId": "ID:",
|
|
401
407
|
"board.taskFieldCommand": "Command:",
|
|
402
408
|
"board.taskFieldChain": "Chain:",
|
|
409
|
+
"board.taskFieldSilent": "Silent:",
|
|
403
410
|
"board.taskFieldCreated": "Created:",
|
|
404
411
|
"board.taskHeaderName": "NAME",
|
|
405
412
|
"board.taskHeaderCommand": "COMMAND",
|
|
@@ -415,6 +422,9 @@
|
|
|
415
422
|
"board.selectedTask": "Task: {name}",
|
|
416
423
|
"board.taskChainsNone": "-",
|
|
417
424
|
"board.taskChainsFormat": "✓:{success} ✗:{failure}",
|
|
425
|
+
"board.silentChainCount": "{count} silent-chain runs",
|
|
426
|
+
"board.silentChainYes": "yes",
|
|
427
|
+
"board.silentChainNo": "no",
|
|
418
428
|
"board.logModalCopied": "Log copied to clipboard",
|
|
419
429
|
"project.selectProject": " Select Project ",
|
|
420
430
|
"project.projectsLabel": "Projects",
|
|
@@ -451,6 +461,8 @@
|
|
|
451
461
|
"project.wizard.colorHint": "Choose a color to identify this project",
|
|
452
462
|
"project.wizard.directoryPrompt": "Working directory? (optional)",
|
|
453
463
|
"project.wizard.directoryHint": "Default directory for loops in this project. Leave blank to inherit",
|
|
464
|
+
"project.wizard.githubSourcePrompt": "GitHub Source? (optional)",
|
|
465
|
+
"project.wizard.githubSourceHint": "Repository in owner/repo format, e.g. CKGrafico/loop-task",
|
|
454
466
|
"project.error.updateFailed": "Failed to update project",
|
|
455
467
|
"project.error.deleteFailed": "Failed to delete project",
|
|
456
468
|
"project.toastCreated": "Project \"{name}\" created",
|
|
@@ -510,7 +522,15 @@
|
|
|
510
522
|
"cmd.show": "Show a hidden panel",
|
|
511
523
|
"cmd.hide": "Hide a panel",
|
|
512
524
|
"cmd.api": "API endpoints info",
|
|
513
|
-
"cmd.
|
|
525
|
+
"cmd.toggleApi": "Toggle HTTP API on/off",
|
|
526
|
+
"cmd.mcp": "MCP server info",
|
|
527
|
+
"cmd.toggleMcp": "Toggle MCP server on/off",
|
|
528
|
+
"board.toastApiEnabled": "HTTP API enabled",
|
|
529
|
+
"board.toastApiDisabled": "HTTP API disabled",
|
|
530
|
+
"board.toastApiToggleError": "Failed to toggle API: {message}",
|
|
531
|
+
"board.toastMcpEnabled": "MCP server enabled",
|
|
532
|
+
"board.toastMcpDisabled": "MCP server disabled",
|
|
533
|
+
"board.toastMcpToggleError": "Failed to toggle MCP: {message}",
|
|
514
534
|
"cmd.filterStatus": "Filter by status",
|
|
515
535
|
"cmd.sort": "Order sort by field",
|
|
516
536
|
"cmd.project": "Filter by project",
|
|
@@ -529,7 +549,7 @@
|
|
|
529
549
|
"export.modalTitle": "Export Loops JSON",
|
|
530
550
|
"export.filePath": "Saved to: {path}",
|
|
531
551
|
"export.error": "Export failed: {message}",
|
|
532
|
-
"export.truncated": "[truncated
|
|
552
|
+
"export.truncated": "[truncated, {total} lines]",
|
|
533
553
|
"export.hint": "up/down:scroll c:copy esc:close",
|
|
534
554
|
"cmd.quit": "Exit loop-task",
|
|
535
555
|
"cmd.save": "Save changes",
|
|
@@ -597,6 +617,8 @@
|
|
|
597
617
|
"wizard.taskCommandPrompt": "What command should this task run?",
|
|
598
618
|
"wizard.onSuccessPrompt": "On success, chain to? (optional)",
|
|
599
619
|
"wizard.onFailurePrompt": "On failure, chain to? (optional)",
|
|
620
|
+
"wizard.silentChainPrompt": "Silent chain? (suppress log output)",
|
|
621
|
+
"wizard.silentChainHint": "Skip logging for this task when chained",
|
|
600
622
|
"wizard.contextPrompt": "Initial context? (optional)",
|
|
601
623
|
"wizard.contextHint": "JSON object with string values, e.g. {\"env\":\"staging\"}",
|
|
602
624
|
"wizard.contextInvalid": "Invalid context: must be a flat JSON object with non-nested, non-array values",
|
|
@@ -14,14 +14,14 @@ let IpcProjectService = class IpcProjectService {
|
|
|
14
14
|
throw new Error(response.message);
|
|
15
15
|
return response.data;
|
|
16
16
|
}
|
|
17
|
-
async create(name, color, directory) {
|
|
18
|
-
const response = await sendRequest({ type: "project-create", payload: { name, color, directory } });
|
|
17
|
+
async create(name, color, directory, githubSource) {
|
|
18
|
+
const response = await sendRequest({ type: "project-create", payload: { name, color, directory, githubSource } });
|
|
19
19
|
if (response.type !== "ok")
|
|
20
20
|
throw new Error(response.message);
|
|
21
21
|
return response.data;
|
|
22
22
|
}
|
|
23
|
-
async update(id, name, color, directory) {
|
|
24
|
-
const response = await sendRequest({ type: "project-update", payload: { id, name, color, directory } });
|
|
23
|
+
async update(id, name, color, directory, githubSource) {
|
|
24
|
+
const response = await sendRequest({ type: "project-update", payload: { id, name, color, directory, githubSource } });
|
|
25
25
|
if (response.type !== "ok") {
|
|
26
26
|
throw new Error(response.message ?? t("project.error.updateFailed"));
|
|
27
27
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { injectable } from "inversify";
|
|
8
|
+
import { sendRequest } from "../../client/ipc.js";
|
|
9
|
+
let IpcSettingsService = class IpcSettingsService {
|
|
10
|
+
async getSettings() {
|
|
11
|
+
const response = await sendRequest({ type: "settings-get" });
|
|
12
|
+
if (response.type !== "ok") {
|
|
13
|
+
throw new Error(response.message ?? "Failed to get settings");
|
|
14
|
+
}
|
|
15
|
+
return response.data;
|
|
16
|
+
}
|
|
17
|
+
async getHttpApiEnabled() {
|
|
18
|
+
const response = await sendRequest({ type: "settings-get" });
|
|
19
|
+
if (response.type !== "ok") {
|
|
20
|
+
throw new Error(response.message ?? "Failed to get settings");
|
|
21
|
+
}
|
|
22
|
+
return response.data.httpApiEnabled;
|
|
23
|
+
}
|
|
24
|
+
async setHttpApiEnabled(enabled) {
|
|
25
|
+
const response = await sendRequest({ type: "settings-set", settings: { httpApiEnabled: enabled } });
|
|
26
|
+
if (response.type !== "ok") {
|
|
27
|
+
throw new Error(response.message ?? "Failed to set settings");
|
|
28
|
+
}
|
|
29
|
+
return response.data;
|
|
30
|
+
}
|
|
31
|
+
async getMcpApiEnabled() {
|
|
32
|
+
const response = await sendRequest({ type: "settings-get" });
|
|
33
|
+
if (response.type !== "ok") {
|
|
34
|
+
throw new Error(response.message ?? "Failed to get settings");
|
|
35
|
+
}
|
|
36
|
+
return response.data.mcpApiEnabled;
|
|
37
|
+
}
|
|
38
|
+
async setMcpApiEnabled(enabled) {
|
|
39
|
+
const response = await sendRequest({ type: "settings-set", settings: { mcpApiEnabled: enabled } });
|
|
40
|
+
if (response.type !== "ok") {
|
|
41
|
+
throw new Error(response.message ?? "Failed to set settings");
|
|
42
|
+
}
|
|
43
|
+
return response.data;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
IpcSettingsService = __decorate([
|
|
47
|
+
injectable()
|
|
48
|
+
], IpcSettingsService);
|
|
49
|
+
export { IpcSettingsService };
|
|
@@ -23,7 +23,7 @@ export function FocusableInput(props) {
|
|
|
23
23
|
}
|
|
24
24
|
if (key.ctrl || key.escape)
|
|
25
25
|
return;
|
|
26
|
-
// Multi-char containing CR/LF with no bracketed markers
|
|
26
|
+
// Multi-char containing CR/LF with no bracketed markers ignore
|
|
27
27
|
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
28
28
|
return;
|
|
29
29
|
if (key.return) {
|
|
@@ -7,7 +7,7 @@ const MAX_VISIBLE = 10;
|
|
|
7
7
|
/**
|
|
8
8
|
* Filterable modal picker: type to narrow, up/down to navigate, enter to
|
|
9
9
|
* select, esc to cancel. The single interaction pattern for every enumerated
|
|
10
|
-
* field in the board
|
|
10
|
+
* field in the board a fixed set of two options and a list of dozens both
|
|
11
11
|
* use this, so users only ever learn one gesture for "change this value".
|
|
12
12
|
*/
|
|
13
13
|
export function SelectModal(props) {
|
|
@@ -10,7 +10,7 @@ function scanTokens(line) {
|
|
|
10
10
|
let i = 0;
|
|
11
11
|
const len = line.length;
|
|
12
12
|
while (i < len) {
|
|
13
|
-
// Whitespace run
|
|
13
|
+
// Whitespace run preserve it as a token
|
|
14
14
|
if (line[i] === " " || line[i] === "\t") {
|
|
15
15
|
let j = i;
|
|
16
16
|
while (j < len && (line[j] === " " || line[j] === "\t"))
|
|
@@ -58,7 +58,7 @@ function scanTokens(line) {
|
|
|
58
58
|
i = j;
|
|
59
59
|
continue;
|
|
60
60
|
}
|
|
61
|
-
// Unquoted word
|
|
61
|
+
// Unquoted word consume until whitespace or operator
|
|
62
62
|
let j = i;
|
|
63
63
|
while (j < len) {
|
|
64
64
|
if (line[j] === " " || line[j] === "\t")
|
|
@@ -90,7 +90,7 @@ function scanTokens(line) {
|
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* Tokenize a command line for syntax highlighting purposes.
|
|
93
|
-
* This is a cosmetic tokenizer
|
|
93
|
+
* This is a cosmetic tokenizer the real parsing for execution
|
|
94
94
|
* lives in src/loop-config.ts parseCommandLine.
|
|
95
95
|
*
|
|
96
96
|
* Whitespace is not returned (use highlightSegments for rendering
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text, useStdout } from "ink";
|
|
4
4
|
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
@@ -27,15 +27,28 @@ function daemonText(status) {
|
|
|
27
27
|
case "error": return "offline";
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
function ServiceStatus({ label, enabled, online }) {
|
|
31
|
+
// Unknown until the daemon is reachable and reports the setting.
|
|
32
|
+
const known = online && enabled !== undefined;
|
|
33
|
+
const on = known && enabled === true;
|
|
34
|
+
const color = !known
|
|
35
|
+
? theme.text.muted
|
|
36
|
+
: on
|
|
37
|
+
? theme.semantic.success
|
|
38
|
+
: theme.semantic.danger;
|
|
39
|
+
const symbol = !known ? "\u25CB" : on ? "\u25CF" : "\u25CB";
|
|
40
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: label }), _jsx(Text, { color: color, children: symbol })] }));
|
|
41
|
+
}
|
|
30
42
|
export function Header(props) {
|
|
31
43
|
const { stdout } = useStdout();
|
|
32
44
|
const width = stdout?.columns ?? 80;
|
|
33
45
|
const compact = width < HEADER_COMPACT_WIDTH;
|
|
46
|
+
const online = props.daemonStatus === "connected";
|
|
34
47
|
const entries = [
|
|
35
48
|
{ label: t("board.runningLabel"), value: props.counts.running, color: theme.semantic.success },
|
|
36
49
|
{ label: t("board.waitingLabel"), value: props.counts.waiting, color: theme.accent.loop },
|
|
37
50
|
{ label: t("board.pausedLabel"), value: props.counts.paused, color: theme.semantic.warning },
|
|
38
51
|
{ label: t("board.idleLabel"), value: props.counts.idle, color: theme.semantic.idle },
|
|
39
52
|
];
|
|
40
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: t("board.appName") }), _jsx(Text, { color: theme.text.muted, children: VERSION }), _jsx(Text, { color: daemonColor(props.daemonStatus), children: daemonSymbol(props.daemonStatus) }), _jsx(Text, { color: theme.text.secondary, children: daemonText(props.daemonStatus) }), !compact && entries.map((e) => e.value > 0 ? (_jsxs(React.Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: e.label }), _jsx(Text, { color: e.color, children: e.value })] }, e.label)) : null)] }), _jsx(TabBar, { activeTab: props.activeTab, onTabChange: props.onTabChange, counts: props.tabCounts })] }), _jsx(Box, { children: _jsx(Text, { color: theme.border.default, children: "\u2500".repeat(width) }) })] }));
|
|
53
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: theme.accent.brand, bold: true, children: t("board.appName") }), _jsx(Text, { color: theme.text.muted, children: VERSION }), _jsx(Text, { color: daemonColor(props.daemonStatus), children: daemonSymbol(props.daemonStatus) }), _jsx(Text, { color: theme.text.secondary, children: daemonText(props.daemonStatus) }), _jsx(ServiceStatus, { label: "api", enabled: props.httpApiEnabled, online: online }), _jsx(ServiceStatus, { label: "mcp", enabled: props.mcpApiEnabled, online: online }), !compact && entries.map((e) => e.value > 0 ? (_jsxs(React.Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: e.label }), _jsx(Text, { color: e.color, children: e.value })] }, e.label)) : null)] }), _jsx(TabBar, { activeTab: props.activeTab, onTabChange: props.onTabChange, counts: props.tabCounts })] }), _jsx(Box, { children: _jsx(Text, { color: theme.border.default, children: "\u2500".repeat(width) }) })] }));
|
|
41
54
|
}
|
|
@@ -9,6 +9,7 @@ const DESC_WIDTH = 32;
|
|
|
9
9
|
const SINCE_WIDTH = 13;
|
|
10
10
|
const RUNS_WIDTH = 4;
|
|
11
11
|
const SKIPPED_WIDTH = 4;
|
|
12
|
+
const SILENT_WIDTH = 4;
|
|
12
13
|
const STATUS_WIDTH = 8;
|
|
13
14
|
const COL_GAP = 1;
|
|
14
15
|
const LIMIT = 15;
|
|
@@ -58,9 +59,9 @@ export function Navigator(props) {
|
|
|
58
59
|
: isSelected
|
|
59
60
|
? theme.text.inverse
|
|
60
61
|
: projectColor(loop);
|
|
61
|
-
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: dotColor, children: dotChar }), _jsx(Text, { color: fg, children: desc.padEnd(DESC_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: since.padEnd(SINCE_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: String(loop.runCount).padStart(RUNS_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: fg, children: String(loop.skippedCount).padStart(SKIPPED_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: isSelected ? theme.text.inverse : sColor, children: sLabel.padEnd(STATUS_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: timing }), loop.status === "running" ? (_jsxs(Text, { color: theme.semantic.success, children: [" ", _jsx(Spinner, { type: "dots" })] })) : null] }));
|
|
62
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: dotColor, children: dotChar }), _jsx(Text, { color: fg, children: desc.padEnd(DESC_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: since.padEnd(SINCE_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: String(loop.runCount).padStart(RUNS_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: fg, children: String(loop.skippedCount).padStart(SKIPPED_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: fg, children: String(loop.silentChainCount ?? 0).padStart(SILENT_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: isSelected ? theme.text.inverse : sColor, children: sLabel.padEnd(STATUS_WIDTH + COL_GAP) }), _jsx(Text, { color: fg, children: timing }), loop.status === "running" ? (_jsxs(Text, { color: theme.semantic.success, children: [" ", _jsx(Spinner, { type: "dots" })] })) : null] }));
|
|
62
63
|
}
|
|
63
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: title }) }), visible.length === 0 ? (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.noMatch") }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: t("board.headerDescription").padEnd(DESC_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerSince").padEnd(SINCE_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerRuns").padStart(RUNS_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerSkipped").padStart(SKIPPED_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerStatus").padEnd(STATUS_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerTiming") })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(ScrollList, { selectedIndex: selectedIndex, height: LIMIT, children: visible.map((loop, i) => {
|
|
64
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: title }) }), visible.length === 0 ? (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.noMatch") }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: t("board.headerDescription").padEnd(DESC_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerSince").padEnd(SINCE_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerRuns").padStart(RUNS_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerSkipped").padStart(SKIPPED_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerSilent").padStart(SILENT_WIDTH) + " ".repeat(COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerStatus").padEnd(STATUS_WIDTH + COL_GAP) }), _jsx(Text, { color: theme.text.muted, children: t("board.headerTiming") })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(ScrollList, { selectedIndex: selectedIndex, height: LIMIT, children: visible.map((loop, i) => {
|
|
64
65
|
const isSelected = i === selectedIndex;
|
|
65
66
|
const indicator = isSelected ? "\u203a " : " ";
|
|
66
67
|
return (_jsxs(Box, { backgroundColor: isSelected ? (isFocused && navActive ? theme.bg.active : isFocused ? theme.bg.hover : undefined) : undefined, children: [_jsx(Text, { color: isSelected ? theme.text.inverse : theme.text.primary, children: indicator }), renderLoop(loop, isSelected)] }, i));
|
|
@@ -53,8 +53,9 @@ export function TaskNavigator(props) {
|
|
|
53
53
|
? cmd.slice(0, COMMAND_WIDTH - 3) + "..."
|
|
54
54
|
: cmd.padEnd(COMMAND_WIDTH);
|
|
55
55
|
const chains = chainsLabel(task);
|
|
56
|
+
const silent = task.silentChain ? " [s]" : "";
|
|
56
57
|
const fg = isSelected ? theme.text.inverse : theme.text.primary;
|
|
57
|
-
return (_jsxs(Box, { backgroundColor: isSelected ? (isFocused && navActive ? theme.bg.activeTask : isFocused ? theme.bg.hover : undefined) : undefined, children: [_jsx(Text, { color: fg, children: name }), _jsx(Text, { color: fg, children: cmdDisplay }),
|
|
58
|
+
return (_jsxs(Box, { backgroundColor: isSelected ? (isFocused && navActive ? theme.bg.activeTask : isFocused ? theme.bg.hover : undefined) : undefined, children: [_jsx(Text, { color: fg, children: name }), _jsx(Text, { color: fg, children: cmdDisplay }), _jsxs(Text, { color: fg, children: [chains, silent] })] }));
|
|
58
59
|
}
|
|
59
60
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: title }) }), visible.length === 0 ? (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.taskBrowserEmpty") }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: t("board.taskHeaderName").padEnd(NAME_WIDTH) }), _jsx(Text, { color: theme.text.muted, children: t("board.taskHeaderCommand").padEnd(COMMAND_WIDTH) }), _jsx(Text, { color: theme.text.muted, children: t("board.taskHeaderChains") })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(ScrollList, { height: LIMIT, selectedIndex: selectedIndex, scrollAlignment: "auto", children: visible.map((task, i) => (_jsx(React.Fragment, { children: renderTask(task, i) }, task.id))) }) })] }))] }));
|
|
60
61
|
}
|
|
@@ -75,7 +76,7 @@ export function TaskInspector(props) {
|
|
|
75
76
|
};
|
|
76
77
|
const onSuccess = task.onSuccessTaskId ? resolveName(task.onSuccessTaskId) ?? task.onSuccessTaskId : t("board.taskNone");
|
|
77
78
|
const onFailure = task.onFailureTaskId ? resolveName(task.onFailureTaskId) ?? task.onFailureTaskId : t("board.taskNone");
|
|
78
|
-
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "column", children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(InspectorField, { label: t("board.fieldId"), children: _jsx(Text, { color: theme.text.primary, children: task.id }) }), _jsx(InspectorField, { label: t("board.taskLabelName") + ": ", children: _jsx(Text, { color: theme.text.primary, children: task.name }) }), _jsx(InspectorField, { label: t("board.taskLabelCommand") + ": ", children: _jsx(Text, { color: theme.text.primary, children: cmd }) }), _jsx(InspectorField, { label: t("board.taskLabelOnSuccess") + ": ", children: _jsx(Text, { color: theme.text.primary, children: onSuccess }) }), _jsx(InspectorField, { label: t("board.taskLabelOnFailure") + ": ", children: _jsx(Text, { color: theme.text.primary, children: onFailure }) })] })] }));
|
|
79
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.border.default, flexDirection: "column", children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(InspectorField, { label: t("board.fieldId"), children: _jsx(Text, { color: theme.text.primary, children: task.id }) }), _jsx(InspectorField, { label: t("board.taskLabelName") + ": ", children: _jsx(Text, { color: theme.text.primary, children: task.name }) }), _jsx(InspectorField, { label: t("board.taskLabelCommand") + ": ", children: _jsx(Text, { color: theme.text.primary, children: cmd }) }), _jsx(InspectorField, { label: t("board.taskLabelOnSuccess") + ": ", children: _jsx(Text, { color: theme.text.primary, children: onSuccess }) }), _jsx(InspectorField, { label: t("board.taskLabelOnFailure") + ": ", children: _jsx(Text, { color: theme.text.primary, children: onFailure }) }), _jsx(InspectorField, { label: t("board.taskFieldSilent") + ": ", children: _jsx(Text, { color: task.silentChain ? theme.semantic.warning : theme.text.muted, children: task.silentChain ? t("board.silentChainYes") : t("board.silentChainNo") }) })] })] }));
|
|
79
80
|
}
|
|
80
81
|
export function TaskActionButtons(props) {
|
|
81
82
|
const { task, selectable, onAction } = props;
|
|
@@ -62,7 +62,7 @@ export function LogModal(props) {
|
|
|
62
62
|
const visible = filtered.slice(startIdx, endIdx);
|
|
63
63
|
useInput((input, key) => {
|
|
64
64
|
// Bracketed paste: content wrapped in ESC[200~ ... ESC[201~. Must come
|
|
65
|
-
// before the escape check
|
|
65
|
+
// before the escape check the leading ESC trips key.escape and would
|
|
66
66
|
// close the modal before the paste is acknowledged. LogModal doesn't
|
|
67
67
|
// insert pastes, so just swallow the sequence.
|
|
68
68
|
if (input.includes("\x1b[200~")) {
|
|
@@ -23,7 +23,7 @@ export function WizardForm(props) {
|
|
|
23
23
|
result[s.key] = values[s.key] ?? s.defaultValue ?? "";
|
|
24
24
|
return result;
|
|
25
25
|
}, [steps, values]);
|
|
26
|
-
// Synchronous mirror of resolvedValues
|
|
26
|
+
// Synchronous mirror of resolvedValues updated inside setValue so
|
|
27
27
|
// findNextField sees the new value immediately, before React re-renders.
|
|
28
28
|
// Without this, onChange("Existing task") + onAdvance() run in the same
|
|
29
29
|
// tick: setValues queues a state update, but findNextField reads stale
|
|
@@ -163,7 +163,7 @@ export function WizardForm(props) {
|
|
|
163
163
|
setValue(step.key, valueFor(step) + sanitizePaste(input));
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
|
-
// Multi-char containing CR/LF with no bracketed markers
|
|
166
|
+
// Multi-char containing CR/LF with no bracketed markers ignore
|
|
167
167
|
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
168
168
|
return;
|
|
169
169
|
// Multi-char printable input = unbracketed single-line paste (e.g. right-click)
|
|
@@ -57,6 +57,7 @@ export function useHandleComplete(params) {
|
|
|
57
57
|
// invalid context, skip
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
const cwdInput = (values.cwd ?? "").trim();
|
|
60
61
|
const options = {
|
|
61
62
|
interval,
|
|
62
63
|
taskId: isExistingTask
|
|
@@ -65,7 +66,9 @@ export function useHandleComplete(params) {
|
|
|
65
66
|
command: cmdOnly,
|
|
66
67
|
commandArgs: args,
|
|
67
68
|
commandRaw: isExistingTask ? undefined : cmdValue,
|
|
68
|
-
|
|
69
|
+
// On create, default an empty cwd to the current directory. On edit,
|
|
70
|
+
// preserve an empty cwd, the user deliberately cleared/kept it empty.
|
|
71
|
+
cwd: cwdInput || (mode === "edit" ? "" : process.cwd()),
|
|
69
72
|
immediate: isManual ? false : runNowValue,
|
|
70
73
|
maxRuns: (values.maxRuns ?? "").trim()
|
|
71
74
|
? parseInt(values.maxRuns, 10)
|
|
@@ -45,6 +45,14 @@ export function ProjectFormView(props) {
|
|
|
45
45
|
inputType: "text",
|
|
46
46
|
defaultValue: mode === "create" ? process.cwd() : (editProject?.directory || undefined),
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
key: "githubSource",
|
|
50
|
+
prompt: t("project.wizard.githubSourcePrompt"),
|
|
51
|
+
hint: t("project.wizard.githubSourceHint"),
|
|
52
|
+
required: false,
|
|
53
|
+
inputType: "text",
|
|
54
|
+
defaultValue: editProject?.githubSource || undefined,
|
|
55
|
+
},
|
|
48
56
|
], [editProject, mode]);
|
|
49
57
|
const handleComplete = useCallback((values) => {
|
|
50
58
|
const name = (values.name ?? "").trim();
|
|
@@ -52,13 +60,14 @@ export function ProjectFormView(props) {
|
|
|
52
60
|
return;
|
|
53
61
|
const colorKey = values.color ?? "cyan";
|
|
54
62
|
const color = PROJECT_COLORS[colorKey] ?? PROJECT_COLORS.cyan;
|
|
63
|
+
const githubSource = values.githubSource?.trim() || undefined;
|
|
55
64
|
if (mode === "edit" && editProject) {
|
|
56
|
-
projectService.update(editProject.id, name, color, values.directory?.trim() || undefined)
|
|
65
|
+
projectService.update(editProject.id, name, color, values.directory?.trim() || undefined, githubSource)
|
|
57
66
|
.then(() => onDone(true, name))
|
|
58
67
|
.catch(() => { });
|
|
59
68
|
}
|
|
60
69
|
else {
|
|
61
|
-
projectService.create(name, color, values.directory?.trim() || undefined)
|
|
70
|
+
projectService.create(name, color, values.directory?.trim() || undefined, githubSource)
|
|
62
71
|
.then(() => onDone(false, name))
|
|
63
72
|
.catch(() => { });
|
|
64
73
|
}
|
|
@@ -23,5 +23,5 @@ export function Inspector(props) {
|
|
|
23
23
|
const nextRun = loop.nextRunAt ? t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) }) : t("format.dash");
|
|
24
24
|
const fullCmd = truncate(commandLine(loop.command, loop.commandArgs), 38);
|
|
25
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 }) })] }));
|
|
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 }) }), (loop.silentChainCount ?? 0) > 0 ? (_jsx(Field, { label: t("board.fieldSilentChains"), children: _jsx(Text, { color: theme.text.muted, children: t("board.silentChainCount", { count: (loop.silentChainCount ?? 0).toLocaleString() }) }) })) : null, _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 }) })] }));
|
|
27
27
|
}
|
|
@@ -21,7 +21,7 @@ function TaskInspector(props) {
|
|
|
21
21
|
}
|
|
22
22
|
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.accent.task, bold: true, children: t("board.taskInspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(Field, { label: t("board.taskFieldName"), children: _jsx(Text, { color: theme.accent.task, bold: true, children: task.name }) }), _jsx(Field, { label: t("board.taskFieldId"), children: _jsx(Text, { color: theme.text.primary, children: task.id }) }), _jsx(Field, { label: t("board.taskFieldCommand"), children: _jsx(Text, { color: theme.text.primary, children: task.commandRaw
|
|
23
23
|
? task.commandRaw.split("\n").filter(Boolean).join(" ")
|
|
24
|
-
: commandLine(task.command, task.commandArgs) }) }), _jsx(Field, { label: t("board.taskFieldCreated"), children: _jsx(Text, { color: theme.text.primary, children: task.createdAt.slice(0, 10) }) }), _jsxs(Field, { label: t("board.taskFieldChain"), children: [task.onSuccessTaskId ? (_jsx(Text, { color: theme.semantic.success, children: "\u2713 " + (allTasks.find((t) => t.id === task.onSuccessTaskId)?.name ?? task.onSuccessTaskId) })) : null, task.onFailureTaskId ? (_jsx(Text, { color: theme.semantic.danger, children: " \u2192 " + (allTasks.find((t) => t.id === task.onFailureTaskId)?.name ?? task.onFailureTaskId) })) : null, !task.onSuccessTaskId && !task.onFailureTaskId ? (_jsx(Text, { color: theme.text.muted, children: t("board.taskNone") })) : null] })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
|
|
24
|
+
: commandLine(task.command, task.commandArgs) }) }), _jsx(Field, { label: t("board.taskFieldCreated"), children: _jsx(Text, { color: theme.text.primary, children: task.createdAt.slice(0, 10) }) }), _jsxs(Field, { label: t("board.taskFieldChain"), children: [task.onSuccessTaskId ? (_jsx(Text, { color: theme.semantic.success, children: "\u2713 " + (allTasks.find((t) => t.id === task.onSuccessTaskId)?.name ?? task.onSuccessTaskId) })) : null, task.onFailureTaskId ? (_jsx(Text, { color: theme.semantic.danger, children: " \u2192 " + (allTasks.find((t) => t.id === task.onFailureTaskId)?.name ?? task.onFailureTaskId) })) : null, !task.onSuccessTaskId && !task.onFailureTaskId ? (_jsx(Text, { color: theme.text.muted, children: t("board.taskNone") })) : null] }), _jsx(Field, { label: t("board.taskFieldSilent"), children: _jsx(Text, { color: task.silentChain ? theme.semantic.warning : theme.text.muted, children: task.silentChain ? t("board.silentChainYes") : t("board.silentChainNo") }) })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
|
|
25
25
|
}
|
|
26
26
|
const LABEL_WIDTH = 9;
|
|
27
27
|
function Field(props) {
|
|
@@ -39,7 +39,9 @@ export function TaskForm(props) {
|
|
|
39
39
|
const [commandEditorOpen, setCommandEditorOpen] = useState(false);
|
|
40
40
|
const [contextEditorOpen, setContextEditorOpen] = useState(false);
|
|
41
41
|
const [contextValue, setContextValue] = useState(editTask?.context ? JSON.stringify(editTask.context) : "");
|
|
42
|
+
const [silentChainValue, setSilentChainValue] = useState(editTask?.silentChain ? t("board.silentChainYes") : "");
|
|
42
43
|
const [openChainField, setOpenChainField] = useState(null);
|
|
44
|
+
const [openSilentChain, setOpenSilentChain] = useState(false);
|
|
43
45
|
const chainFieldsRef = useRef({});
|
|
44
46
|
const resolveChainId = useCallback((val) => {
|
|
45
47
|
if (!val || val === t("wizard.chainNone"))
|
|
@@ -97,6 +99,15 @@ export function TaskForm(props) {
|
|
|
97
99
|
return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.onFailurePrompt"), isActive: isActive }));
|
|
98
100
|
},
|
|
99
101
|
},
|
|
102
|
+
{
|
|
103
|
+
key: "silentChain",
|
|
104
|
+
prompt: t("wizard.silentChainPrompt"),
|
|
105
|
+
hint: t("wizard.silentChainHint"),
|
|
106
|
+
required: false,
|
|
107
|
+
defaultValue: silentChainValue || undefined,
|
|
108
|
+
onActivate: () => setOpenSilentChain(true),
|
|
109
|
+
renderCustom: ({ value, isActive }) => (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.silentChainPrompt"), isActive: isActive })),
|
|
110
|
+
},
|
|
100
111
|
{
|
|
101
112
|
key: "context",
|
|
102
113
|
prompt: t("wizard.contextPrompt"),
|
|
@@ -122,7 +133,7 @@ export function TaskForm(props) {
|
|
|
122
133
|
},
|
|
123
134
|
];
|
|
124
135
|
return list;
|
|
125
|
-
}, [commandValue, chainOptions, editTask, resolveChainName, contextValue]);
|
|
136
|
+
}, [commandValue, chainOptions, editTask, resolveChainName, contextValue, silentChainValue]);
|
|
126
137
|
const handleComplete = useCallback((values) => {
|
|
127
138
|
const name = values.name?.trim() ?? "";
|
|
128
139
|
const rawCommand = joinCommandLines(commandValue);
|
|
@@ -130,6 +141,7 @@ export function TaskForm(props) {
|
|
|
130
141
|
return;
|
|
131
142
|
const onSuccessTaskId = resolveChainId(values.onSuccess ?? "");
|
|
132
143
|
const onFailureTaskId = resolveChainId(values.onFailure ?? "");
|
|
144
|
+
const silentChain = values.silentChain === t("board.silentChainYes");
|
|
133
145
|
let context;
|
|
134
146
|
const contextStr = contextValue?.trim();
|
|
135
147
|
if (contextStr) {
|
|
@@ -176,6 +188,7 @@ export function TaskForm(props) {
|
|
|
176
188
|
steps,
|
|
177
189
|
onSuccessTaskId,
|
|
178
190
|
onFailureTaskId,
|
|
191
|
+
silentChain: silentChain || undefined,
|
|
179
192
|
context,
|
|
180
193
|
};
|
|
181
194
|
}
|
|
@@ -187,6 +200,7 @@ export function TaskForm(props) {
|
|
|
187
200
|
commandRaw: commandValue,
|
|
188
201
|
onSuccessTaskId,
|
|
189
202
|
onFailureTaskId,
|
|
203
|
+
silentChain: silentChain || undefined,
|
|
190
204
|
context,
|
|
191
205
|
};
|
|
192
206
|
}
|
|
@@ -201,11 +215,11 @@ export function TaskForm(props) {
|
|
|
201
215
|
.then((result) => onDone(false, result.id))
|
|
202
216
|
.catch(() => { });
|
|
203
217
|
}
|
|
204
|
-
}, [commandValue, resolveChainId, mode, editTask, onDone, contextValue]);
|
|
218
|
+
}, [commandValue, resolveChainId, mode, editTask, onDone, contextValue, silentChainValue]);
|
|
205
219
|
const title = mode === "edit"
|
|
206
220
|
? t("board.taskEditTitle")
|
|
207
221
|
: t("board.taskCreateTitle");
|
|
208
|
-
return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: title, steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: openChainField !== null || commandEditorOpen || contextEditorOpen }), openChainField ? (_jsx(SelectModal, { title: openChainField === "onSuccess" ? t("wizard.onSuccessPrompt") : t("wizard.onFailurePrompt"), options: chainSelectOptions, initialValue: chainFieldsRef.current[openChainField]?.value, onSelect: (option) => {
|
|
222
|
+
return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: title, steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: openChainField !== null || commandEditorOpen || contextEditorOpen || openSilentChain }), openChainField ? (_jsx(SelectModal, { title: openChainField === "onSuccess" ? t("wizard.onSuccessPrompt") : t("wizard.onFailurePrompt"), options: chainSelectOptions, initialValue: chainFieldsRef.current[openChainField]?.value, onSelect: (option) => {
|
|
209
223
|
chainFieldsRef.current[openChainField]?.onChange(option.value);
|
|
210
224
|
chainFieldsRef.current[openChainField]?.onAdvance();
|
|
211
225
|
setOpenChainField(null);
|
|
@@ -215,5 +229,11 @@ export function TaskForm(props) {
|
|
|
215
229
|
}, onCancel: () => setCommandEditorOpen(false) })) : null, contextEditorOpen ? (_jsx(CodeEditorModal, { initialValue: contextValue, onSave: (v) => {
|
|
216
230
|
setContextValue(v);
|
|
217
231
|
setContextEditorOpen(false);
|
|
218
|
-
}, onCancel: () => setContextEditorOpen(false) })) : null
|
|
232
|
+
}, onCancel: () => setContextEditorOpen(false) })) : null, openSilentChain ? (_jsx(SelectModal, { title: t("wizard.silentChainPrompt"), options: [
|
|
233
|
+
{ value: t("board.silentChainNo"), label: t("board.silentChainNo") },
|
|
234
|
+
{ value: t("board.silentChainYes"), label: t("board.silentChainYes") },
|
|
235
|
+
], initialValue: silentChainValue || t("board.silentChainNo"), onSelect: (option) => {
|
|
236
|
+
setSilentChainValue(option.value === t("board.silentChainNo") ? "" : option.value);
|
|
237
|
+
setOpenSilentChain(false);
|
|
238
|
+
}, onClose: () => setOpenSilentChain(false) })) : null] }));
|
|
219
239
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.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": {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"node": ">=20.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
52
|
"commander": "^13.1.0",
|
|
52
53
|
"execa": "^9.6.0",
|
|
53
54
|
"ink": "^7.1.0",
|
|
@@ -59,7 +60,8 @@
|
|
|
59
60
|
"inversify": "^8.1.1",
|
|
60
61
|
"inversify-hooks": "^4.0.0",
|
|
61
62
|
"ms": "^2.1.3",
|
|
62
|
-
"react": "^19.2.7"
|
|
63
|
+
"react": "^19.2.7",
|
|
64
|
+
"zod": "^4.4.3"
|
|
63
65
|
},
|
|
64
66
|
"devDependencies": {
|
|
65
67
|
"@types/ms": "^2.1.0",
|