loop-task 2.1.11 → 2.1.13
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 +3 -1
- package/dist/cli.js +88 -2
- package/dist/client/commands.js +8 -1
- package/dist/client/project-commands.js +7 -2
- package/dist/core/context/template.js +1 -1
- package/dist/core/context/validate-context.js +19 -0
- package/dist/core/loop/chain-executor.js +50 -17
- package/dist/core/loop/loop-controller.js +18 -0
- package/dist/core/loop/loop-runner.js +8 -0
- package/dist/core/loop/run-executor.js +1 -1
- package/dist/core/scheduling/index.js +4 -0
- package/dist/daemon/http/openapi.js +31 -4
- package/dist/daemon/http/route-loops.js +106 -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 +75 -0
- package/dist/daemon/http/routes.js +3 -2
- package/dist/daemon/http/server.js +20 -2
- package/dist/daemon/index.js +48 -7
- package/dist/daemon/managers/loop-entry.js +2 -0
- package/dist/daemon/managers/loop-manager.js +4 -4
- package/dist/daemon/managers/loop-options.js +1 -0
- package/dist/daemon/managers/loop-serialization.js +1 -0
- 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 +162 -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 +46 -0
- package/dist/duration.js +5 -0
- package/dist/entities/loops/filters.js +2 -0
- package/dist/entities/tasks/filters.js +1 -1
- package/dist/features/commands/commands.js +4 -1
- package/dist/features/commands/useCommandHandlers.js +32 -0
- package/dist/features/commands/useGlobalShortcuts.js +0 -1
- package/dist/features/forms/FormRouter.js +2 -0
- package/dist/features/overlays/ExportModal.js +1 -1
- package/dist/loop-config.js +8 -4
- 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 +31 -4
- 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/ui/format.js +2 -0
- 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/CreateForm.js +9 -2
- package/dist/widgets/loop-form/WizardForm.js +12 -2
- package/dist/widgets/loop-form/useCreateSteps.js +30 -2
- package/dist/widgets/loop-form/useHandleComplete.js +23 -3
- 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 +69 -4
- package/package.json +4 -2
package/dist/duration.js
CHANGED
|
@@ -5,6 +5,9 @@ export function parseDuration(input) {
|
|
|
5
5
|
if (!trimmed) {
|
|
6
6
|
throw new Error(t("errors.durationEmpty"));
|
|
7
7
|
}
|
|
8
|
+
if (trimmed === "manual" || trimmed === "0") {
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
8
11
|
const result = ms(trimmed);
|
|
9
12
|
if (typeof result !== "number" || isNaN(result)) {
|
|
10
13
|
throw new Error(t("errors.durationInvalid", { input }));
|
|
@@ -15,5 +18,7 @@ export function parseDuration(input) {
|
|
|
15
18
|
return result;
|
|
16
19
|
}
|
|
17
20
|
export function formatDuration(value) {
|
|
21
|
+
if (value === 0)
|
|
22
|
+
return t("format.durationManual");
|
|
18
23
|
return ms(value, { long: true });
|
|
19
24
|
}
|
|
@@ -23,7 +23,7 @@ function compare(left, right, sort) {
|
|
|
23
23
|
return byCmd;
|
|
24
24
|
return left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
|
|
25
25
|
}
|
|
26
|
-
// "created"
|
|
26
|
+
// "created" newest first
|
|
27
27
|
return right.createdAt.localeCompare(left.createdAt);
|
|
28
28
|
}
|
|
29
29
|
export function applyTaskFilters(tasks, filters) {
|
|
@@ -4,7 +4,10 @@ function globalCommands() {
|
|
|
4
4
|
return [
|
|
5
5
|
{ label: t('cmd.help'), value: 'help', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+p' },
|
|
6
6
|
{ label: t('cmd.debug'), value: 'debug', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+b' },
|
|
7
|
-
{ label: t('cmd.api'), value: 'api', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL
|
|
7
|
+
{ label: t('cmd.api'), value: 'api', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
|
|
8
|
+
{ label: t('cmd.toggleApi'), value: 'toggle-api', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
|
|
9
|
+
{ label: t('cmd.mcp'), value: 'mcp', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
|
|
10
|
+
{ label: t('cmd.toggleMcp'), value: 'toggle-mcp', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL },
|
|
8
11
|
{ label: t('cmd.export'), value: 'export', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+x' },
|
|
9
12
|
{ label: t('cmd.import'), value: 'import', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+i' },
|
|
10
13
|
{ label: t('cmd.status'), value: 'status', hint: '', tier: COMMAND_TIER_GLOBAL, category: COMMAND_CATEGORY_GLOBAL, shortcut: 'ctrl+y' },
|
|
@@ -2,6 +2,8 @@ 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
|
import { groupRunsByCycle } from "../../widgets/right-panel/RunHistory.js";
|
|
5
|
+
import { container } from "../../shared/container/index.js";
|
|
6
|
+
import { TYPES } from "../../shared/services/types.js";
|
|
5
7
|
export function useCommandHandlers(context) {
|
|
6
8
|
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;
|
|
7
9
|
const commandHandlers = {
|
|
@@ -128,6 +130,36 @@ export function useCommandHandlers(context) {
|
|
|
128
130
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
129
131
|
pushToast("info", `API: ${baseUrl} | Swagger: ${baseUrl}/api/docs | OpenAPI: ${baseUrl}/api/openapi.json`);
|
|
130
132
|
},
|
|
133
|
+
"toggle-api": async () => {
|
|
134
|
+
try {
|
|
135
|
+
const settingsService = container.get(TYPES.SettingsService);
|
|
136
|
+
const current = await settingsService.getHttpApiEnabled();
|
|
137
|
+
await settingsService.setHttpApiEnabled(!current);
|
|
138
|
+
pushToast("success", t(!current ? "board.toastApiEnabled" : "board.toastApiDisabled"));
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
pushToast("error", t("board.toastApiToggleError", { message: e.message }));
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
mcp: () => {
|
|
145
|
+
const transport = process.env.LOOP_CLI_MCP_TRANSPORT ?? "sse";
|
|
146
|
+
const port = process.env.LOOP_CLI_MCP_PORT ?? "8846";
|
|
147
|
+
const info = transport === "stdio"
|
|
148
|
+
? `MCP server on stdio. To use SSE (default), restart without LOOP_CLI_MCP_TRANSPORT=stdio`
|
|
149
|
+
: `MCP: connect your client to http://127.0.0.1:${port}/sse`;
|
|
150
|
+
pushToast("info", info);
|
|
151
|
+
},
|
|
152
|
+
"toggle-mcp": async () => {
|
|
153
|
+
try {
|
|
154
|
+
const settingsService = container.get(TYPES.SettingsService);
|
|
155
|
+
const current = await settingsService.getMcpApiEnabled();
|
|
156
|
+
await settingsService.setMcpApiEnabled(!current);
|
|
157
|
+
pushToast("success", t(!current ? "board.toastMcpEnabled" : "board.toastMcpDisabled"));
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
pushToast("error", t("board.toastMcpToggleError", { message: e.message }));
|
|
161
|
+
}
|
|
162
|
+
},
|
|
131
163
|
status: () => { pushToast("info", `Command "status" coming soon`); },
|
|
132
164
|
export: () => {
|
|
133
165
|
exportService.exportConfig()
|
|
@@ -46,7 +46,6 @@ export function useGlobalShortcuts(context) {
|
|
|
46
46
|
const globalShortcuts = {
|
|
47
47
|
p: () => { setCommandsBrowserOpen(true); },
|
|
48
48
|
b: () => handleCommand("debug"),
|
|
49
|
-
g: () => handleCommand("api"),
|
|
50
49
|
x: () => handleCommand("export"),
|
|
51
50
|
i: () => handleCommand("import"),
|
|
52
51
|
y: () => handleCommand("status"),
|
|
@@ -15,6 +15,7 @@ function createInitialValues(editTarget, currentProjectId) {
|
|
|
15
15
|
runNow: "y",
|
|
16
16
|
maxRuns: "",
|
|
17
17
|
project: currentProjectId,
|
|
18
|
+
context: "",
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
return {
|
|
@@ -27,6 +28,7 @@ function createInitialValues(editTarget, currentProjectId) {
|
|
|
27
28
|
runNow: "y",
|
|
28
29
|
maxRuns: editTarget.maxRuns?.toString() ?? "",
|
|
29
30
|
project: editTarget.projectId ?? "default",
|
|
31
|
+
context: editTarget.context ? JSON.stringify(editTarget.context) : "",
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
export function FormRouter(props) {
|
|
@@ -15,7 +15,7 @@ export function ExportModal(props) {
|
|
|
15
15
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
16
16
|
useInput((input, key) => {
|
|
17
17
|
// Bracketed paste: content wrapped in ESC[200~ ... ESC[201~. Must come
|
|
18
|
-
// before the escape check
|
|
18
|
+
// before the escape check the leading ESC trips key.escape and would
|
|
19
19
|
// close the modal on a right-click paste.
|
|
20
20
|
if (input.includes("\x1b[200~")) {
|
|
21
21
|
return;
|
package/dist/loop-config.js
CHANGED
|
@@ -96,7 +96,7 @@ export function joinCommandLines(text) {
|
|
|
96
96
|
// - Segment ending with \: backslash consumed, joins to next with NO added space
|
|
97
97
|
// - Segment NOT ending with \: joins to next with a single space
|
|
98
98
|
// Leading whitespace on each line is trimmed (indentation); trailing whitespace
|
|
99
|
-
// before \ is preserved (it's part of the content
|
|
99
|
+
// before \ is preserved (it's part of the content e.g. separates tokens).
|
|
100
100
|
let result = "";
|
|
101
101
|
let prevContinued = false; // true if previous segment ended with \
|
|
102
102
|
for (const seg of segments) {
|
|
@@ -129,20 +129,24 @@ export function buildLoopOptions(intervalHuman, input = {}) {
|
|
|
129
129
|
if (!description) {
|
|
130
130
|
throw new Error(t("errors.descriptionEmpty"));
|
|
131
131
|
}
|
|
132
|
+
const parsedInterval = parseDuration(intervalHuman);
|
|
133
|
+
const isManual = parsedInterval === 0;
|
|
134
|
+
const normalizedIntervalHuman = isManual ? "manual" : intervalHuman;
|
|
132
135
|
return {
|
|
133
|
-
intervalHuman,
|
|
136
|
+
intervalHuman: normalizedIntervalHuman,
|
|
134
137
|
options: {
|
|
135
|
-
interval:
|
|
138
|
+
interval: parsedInterval,
|
|
136
139
|
taskId,
|
|
137
140
|
command,
|
|
138
141
|
commandArgs,
|
|
139
142
|
cwd: input.cwd ?? "",
|
|
140
|
-
immediate: input.now ?? false,
|
|
143
|
+
immediate: isManual ? false : (input.now ?? false),
|
|
141
144
|
maxRuns: parseMaxRuns(input.maxRuns),
|
|
142
145
|
verbose: input.verbose ?? false,
|
|
143
146
|
description,
|
|
144
147
|
projectId: input.projectId ?? "default",
|
|
145
148
|
offset: input.offset ?? null,
|
|
149
|
+
context: input.context,
|
|
146
150
|
},
|
|
147
151
|
};
|
|
148
152
|
}
|
package/dist/shared/clipboard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { platform } from "node:os";
|
|
3
3
|
/**
|
|
4
|
-
* Write text to the system clipboard. Never throws
|
|
4
|
+
* Write text to the system clipboard. Never throws returns false if no
|
|
5
5
|
* clipboard tool is available (e.g. a headless SSH box without xclip/xsel).
|
|
6
6
|
*
|
|
7
7
|
* Linux toolchain tried in order: xclip → xsel → wl-copy (Wayland) →
|
|
@@ -28,7 +28,7 @@ export function copyToClipboard(text) {
|
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
// Linux / BSD: try each clipboard tool in order. All calls are guarded
|
|
31
|
+
// Linux / BSD: try each clipboard tool in order. All calls are guarded
|
|
32
32
|
// a missing binary must never throw, since callers invoke this from
|
|
33
33
|
// synchronous keypress handlers (an uncaught throw here kills the app).
|
|
34
34
|
const tries = [
|
|
@@ -30,6 +30,9 @@ export function tasksJson() {
|
|
|
30
30
|
export function projectsJson() {
|
|
31
31
|
return path.join(getDataDir(), "projects.json");
|
|
32
32
|
}
|
|
33
|
+
export function settingsJson() {
|
|
34
|
+
return path.join(getDataDir(), "settings.json");
|
|
35
|
+
}
|
|
33
36
|
export function logFile(id) {
|
|
34
37
|
return path.join(getLogsDir(), `${id}.log`);
|
|
35
38
|
}
|
|
@@ -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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"cli.startDescription": "Start the background daemon (restores persisted loops)",
|
|
5
5
|
"cli.newDescription": "Create a new loop in the background",
|
|
6
6
|
"cli.runDescription": "Run a loop in the foreground",
|
|
7
|
-
"cli.argInterval": "Interval between runs (e.g. 30s, 5m, 1h)",
|
|
7
|
+
"cli.argInterval": "Interval between runs (e.g. 30s, 5m, 1h, or manual)",
|
|
8
8
|
"cli.argCommand": "Command to execute",
|
|
9
9
|
"cli.optNow": "Run immediately before waiting",
|
|
10
10
|
"cli.optMaxRuns": "Stop after N executions",
|
|
@@ -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",
|
|
@@ -83,7 +86,7 @@
|
|
|
83
86
|
"loop.debugDuration": "Duration: {duration}",
|
|
84
87
|
"loop.nextRun": "Next run in {duration} (at {time})",
|
|
85
88
|
"errors.durationEmpty": "Duration cannot be empty",
|
|
86
|
-
"errors.durationInvalid": "Invalid duration: \"{input}\". Use formats like 10s, 5m, 1h, 1d, 1w",
|
|
89
|
+
"errors.durationInvalid": "Invalid duration: \"{input}\". Use formats like 10s, 5m, 1h, 1d, 1w, or manual",
|
|
87
90
|
"errors.durationNotPositive": "Duration must be positive, got: \"{input}\"",
|
|
88
91
|
"errors.maxRunsInvalid": "--max-runs must be a positive integer",
|
|
89
92
|
"errors.unbalancedQuote": "Unbalanced quote in command",
|
|
@@ -117,6 +120,7 @@
|
|
|
117
120
|
"format.daysAhead": "{days}d",
|
|
118
121
|
"format.timingPaused": "paused",
|
|
119
122
|
"format.timingIdle": "not scheduled",
|
|
123
|
+
"format.durationManual": "manual trigger only",
|
|
120
124
|
"format.timingNext": "in {timeAgo}",
|
|
121
125
|
"format.timingLast": "last {timeAgo}",
|
|
122
126
|
"format.timingNew": "new",
|
|
@@ -143,6 +147,7 @@
|
|
|
143
147
|
"board.headerSince": "SINCE",
|
|
144
148
|
"board.headerRuns": "RUNS",
|
|
145
149
|
"board.headerSkipped": "SKIP",
|
|
150
|
+
"board.headerSilent": "SILENT",
|
|
146
151
|
"board.headerStatus": "STATUS",
|
|
147
152
|
"board.headerDescription": "DESCRIPTION",
|
|
148
153
|
"board.headerTiming": "TIMING",
|
|
@@ -173,6 +178,7 @@
|
|
|
173
178
|
"board.fieldLastRun": "Last run:",
|
|
174
179
|
"board.fieldLastExit": "Last exit:",
|
|
175
180
|
"board.fieldNextRun": "Next run:",
|
|
181
|
+
"board.fieldSilentChains": "Silent:",
|
|
176
182
|
"board.fieldPid": "PID:",
|
|
177
183
|
"board.inherit": "(inherit)",
|
|
178
184
|
"board.unlimited": "∞",
|
|
@@ -209,6 +215,7 @@
|
|
|
209
215
|
"board.detailFieldLastExit": "Last exit: ",
|
|
210
216
|
"board.detailFieldNextRun": "Next run: ",
|
|
211
217
|
"board.detailFieldPid": "PID: ",
|
|
218
|
+
"board.detailFieldSilentChains": "Silent: ",
|
|
212
219
|
"board.detailSummaryStatus": "status",
|
|
213
220
|
"board.detailSummaryTiming": "next",
|
|
214
221
|
"board.detailSummaryInterval": "every",
|
|
@@ -257,7 +264,7 @@
|
|
|
257
264
|
"board.taskCreateTitle": " New Task ",
|
|
258
265
|
"board.taskEditTitle": " Edit Task ",
|
|
259
266
|
"board.hintTaskMode": "type a command inline, or pick an existing task",
|
|
260
|
-
"board.hintInterval": "How often to run, e.g. 30s, 5m, 30m, 1h, 1d",
|
|
267
|
+
"board.hintInterval": "How often to run, e.g. 30s, 5m, 30m, 1h, 1d, or manual",
|
|
261
268
|
"board.hintCommand": "Full command line. Quote args with spaces",
|
|
262
269
|
"board.hintDescription": "Short label shown in the list. Blank uses the command",
|
|
263
270
|
"board.hintCwd": "Leave blank to inherit from project directory, or set a specific path",
|
|
@@ -399,6 +406,7 @@
|
|
|
399
406
|
"board.taskFieldId": "ID:",
|
|
400
407
|
"board.taskFieldCommand": "Command:",
|
|
401
408
|
"board.taskFieldChain": "Chain:",
|
|
409
|
+
"board.taskFieldSilent": "Silent:",
|
|
402
410
|
"board.taskFieldCreated": "Created:",
|
|
403
411
|
"board.taskHeaderName": "NAME",
|
|
404
412
|
"board.taskHeaderCommand": "COMMAND",
|
|
@@ -414,6 +422,9 @@
|
|
|
414
422
|
"board.selectedTask": "Task: {name}",
|
|
415
423
|
"board.taskChainsNone": "-",
|
|
416
424
|
"board.taskChainsFormat": "✓:{success} ✗:{failure}",
|
|
425
|
+
"board.silentChainCount": "{count} silent-chain runs",
|
|
426
|
+
"board.silentChainYes": "yes",
|
|
427
|
+
"board.silentChainNo": "no",
|
|
417
428
|
"board.logModalCopied": "Log copied to clipboard",
|
|
418
429
|
"project.selectProject": " Select Project ",
|
|
419
430
|
"project.projectsLabel": "Projects",
|
|
@@ -450,6 +461,8 @@
|
|
|
450
461
|
"project.wizard.colorHint": "Choose a color to identify this project",
|
|
451
462
|
"project.wizard.directoryPrompt": "Working directory? (optional)",
|
|
452
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",
|
|
453
466
|
"project.error.updateFailed": "Failed to update project",
|
|
454
467
|
"project.error.deleteFailed": "Failed to delete project",
|
|
455
468
|
"project.toastCreated": "Project \"{name}\" created",
|
|
@@ -509,6 +522,15 @@
|
|
|
509
522
|
"cmd.show": "Show a hidden panel",
|
|
510
523
|
"cmd.hide": "Hide a panel",
|
|
511
524
|
"cmd.api": "API endpoints info",
|
|
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}",
|
|
512
534
|
"cmd.status": "Daemon status info (Soon...)",
|
|
513
535
|
"cmd.filterStatus": "Filter by status",
|
|
514
536
|
"cmd.sort": "Order sort by field",
|
|
@@ -574,7 +596,7 @@
|
|
|
574
596
|
"wizard.newTask": "New task",
|
|
575
597
|
"wizard.stepOf": "Step {current} of {total}",
|
|
576
598
|
"wizard.intervalPrompt": "How often should it run?",
|
|
577
|
-
"wizard.intervalHint": "e.g. 30s, 5m, 30m, 1h, 1d",
|
|
599
|
+
"wizard.intervalHint": "e.g. 30s, 5m, 30m, 1h, 1d, or manual for trigger-only",
|
|
578
600
|
"wizard.commandPrompt": "What command should run?",
|
|
579
601
|
"wizard.commandHint": "Full command line. Quote args with spaces",
|
|
580
602
|
"wizard.taskModePrompt": "Inline command or existing task?",
|
|
@@ -596,6 +618,11 @@
|
|
|
596
618
|
"wizard.taskCommandPrompt": "What command should this task run?",
|
|
597
619
|
"wizard.onSuccessPrompt": "On success, chain to? (optional)",
|
|
598
620
|
"wizard.onFailurePrompt": "On failure, chain to? (optional)",
|
|
621
|
+
"wizard.silentChainPrompt": "Silent chain? (suppress log output)",
|
|
622
|
+
"wizard.silentChainHint": "Skip logging for this task when chained",
|
|
623
|
+
"wizard.contextPrompt": "Initial context? (optional)",
|
|
624
|
+
"wizard.contextHint": "JSON object with string values, e.g. {\"env\":\"staging\"}",
|
|
625
|
+
"wizard.contextInvalid": "Invalid context: must be a flat JSON object with non-nested, non-array values",
|
|
599
626
|
"wizard.created": "Created",
|
|
600
627
|
"wizard.filled": "{filled}/{total} filled",
|
|
601
628
|
"wizard.footerHints": "tab next . ctrl+s save . esc cancel",
|
|
@@ -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) {
|
package/dist/shared/ui/format.js
CHANGED
|
@@ -66,6 +66,8 @@ export function statusColor(status) {
|
|
|
66
66
|
return STATUS_COLORS[status] ?? "#ffffff";
|
|
67
67
|
}
|
|
68
68
|
export function timingLabel(loop) {
|
|
69
|
+
if (loop.interval === 0)
|
|
70
|
+
return t("format.durationManual");
|
|
69
71
|
if (loop.status === "paused")
|
|
70
72
|
return t("format.timingPaused");
|
|
71
73
|
if (loop.status === "idle")
|
|
@@ -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;
|