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
|
@@ -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~")) {
|
|
@@ -15,7 +15,9 @@ export function CreateView(props) {
|
|
|
15
15
|
const [taskPickerOpen, setTaskPickerOpen] = useState(false);
|
|
16
16
|
const [openSelect, setOpenSelect] = useState(null);
|
|
17
17
|
const [commandEditorOpen, setCommandEditorOpen] = useState(false);
|
|
18
|
+
const [contextEditorOpen, setContextEditorOpen] = useState(false);
|
|
18
19
|
const [commandValue, setCommandValue] = useState(initial.command ?? "");
|
|
20
|
+
const [contextValue, setContextValue] = useState(initial.context ?? "");
|
|
19
21
|
const fieldCallbacksRef = useRef({});
|
|
20
22
|
const taskModeInitial = initial.taskMode === "existing" ? "Existing task" : "Inline command";
|
|
21
23
|
const resolvedTaskName = useMemo(() => {
|
|
@@ -32,10 +34,12 @@ export function CreateView(props) {
|
|
|
32
34
|
selectedTaskId,
|
|
33
35
|
resolvedTaskName,
|
|
34
36
|
commandValue,
|
|
37
|
+
contextValue,
|
|
35
38
|
projects: props.projects,
|
|
36
39
|
setOpenSelect,
|
|
37
40
|
setTaskPickerOpen,
|
|
38
41
|
setCommandEditorOpen,
|
|
42
|
+
setContextEditorOpen,
|
|
39
43
|
fieldCallbacksRef,
|
|
40
44
|
taskModeInitial,
|
|
41
45
|
});
|
|
@@ -61,7 +65,7 @@ export function CreateView(props) {
|
|
|
61
65
|
const selectTitleFor = (field) => field === "taskMode" ? t("wizard.taskModePrompt")
|
|
62
66
|
: field === "runNow" ? t("wizard.runNowPrompt")
|
|
63
67
|
: t("wizard.projectPrompt");
|
|
64
|
-
return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: mode === "edit" ? t("wizard.editLoop") : t("wizard.newLoop"), steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: taskPickerOpen || openSelect !== null || commandEditorOpen }), taskPickerOpen ? (_jsx(TaskPickerModal, { tasks: tasks, onSelect: (task) => {
|
|
68
|
+
return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: mode === "edit" ? t("wizard.editLoop") : t("wizard.newLoop"), steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: taskPickerOpen || openSelect !== null || commandEditorOpen || contextEditorOpen }), taskPickerOpen ? (_jsx(TaskPickerModal, { tasks: tasks, onSelect: (task) => {
|
|
65
69
|
onChooseTask({ id: task.id, name: task.name });
|
|
66
70
|
setTaskPickerOpen(false);
|
|
67
71
|
}, onClose: () => setTaskPickerOpen(false) })) : null, openSelect ? (_jsx(SelectModal, { title: selectTitleFor(openSelect), options: selectOptionsFor(openSelect), initialValue: fieldCallbacksRef.current[openSelect]?.value, onSelect: (option) => {
|
|
@@ -71,5 +75,8 @@ export function CreateView(props) {
|
|
|
71
75
|
}, onClose: () => setOpenSelect(null) })) : null, commandEditorOpen ? (_jsx(CodeEditorModal, { initialValue: commandValue, onSave: (v) => {
|
|
72
76
|
setCommandValue(v);
|
|
73
77
|
setCommandEditorOpen(false);
|
|
74
|
-
}, onCancel: () => setCommandEditorOpen(false) })) : null
|
|
78
|
+
}, onCancel: () => setCommandEditorOpen(false) })) : null, contextEditorOpen ? (_jsx(CodeEditorModal, { initialValue: contextValue, onSave: (v) => {
|
|
79
|
+
setContextValue(v);
|
|
80
|
+
setContextEditorOpen(false);
|
|
81
|
+
}, onCancel: () => setContextEditorOpen(false) })) : null] }));
|
|
75
82
|
}
|
|
@@ -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
|
|
@@ -59,6 +59,9 @@ export function WizardForm(props) {
|
|
|
59
59
|
const err = validateField(s.key, resolvedValues);
|
|
60
60
|
if (err)
|
|
61
61
|
errors[s.key] = err;
|
|
62
|
+
if (s.validate && !s.validate(valueFor(s))) {
|
|
63
|
+
errors[s.key] = s.validationError ?? t("wizard.validationError");
|
|
64
|
+
}
|
|
62
65
|
}
|
|
63
66
|
if (Object.keys(errors).length > 0) {
|
|
64
67
|
setValidationErrors(errors);
|
|
@@ -99,6 +102,13 @@ export function WizardForm(props) {
|
|
|
99
102
|
setError(step.key, err);
|
|
100
103
|
return;
|
|
101
104
|
}
|
|
105
|
+
if (step.validate) {
|
|
106
|
+
const value = valueFor(step);
|
|
107
|
+
if (!step.validate(value)) {
|
|
108
|
+
setError(step.key, step.validationError ?? t("wizard.validationError"));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
102
112
|
clearError(step.key);
|
|
103
113
|
}
|
|
104
114
|
const next = findNextField(activeField, delta);
|
|
@@ -153,7 +163,7 @@ export function WizardForm(props) {
|
|
|
153
163
|
setValue(step.key, valueFor(step) + sanitizePaste(input));
|
|
154
164
|
return;
|
|
155
165
|
}
|
|
156
|
-
// Multi-char containing CR/LF with no bracketed markers
|
|
166
|
+
// Multi-char containing CR/LF with no bracketed markers ignore
|
|
157
167
|
if (input.length > 1 && (input.includes("\r") || input.includes("\n")))
|
|
158
168
|
return;
|
|
159
169
|
// Multi-char printable input = unbracketed single-line paste (e.g. right-click)
|
|
@@ -3,8 +3,9 @@ import { useMemo } from "react";
|
|
|
3
3
|
import { t } from "../../shared/i18n/index.js";
|
|
4
4
|
import { SelectValueField } from "../../shared/ui/SelectModal.js";
|
|
5
5
|
import { CodeEditorPreview } from "../../features/code-editor/CodeEditorPreview.js";
|
|
6
|
+
import { validateContext } from "../../core/context/validate-context.js";
|
|
6
7
|
export function useCreateSteps(params) {
|
|
7
|
-
const { initial, selectedTaskId, resolvedTaskName, commandValue, projects, setOpenSelect, setTaskPickerOpen, setCommandEditorOpen, fieldCallbacksRef, taskModeInitial, } = params;
|
|
8
|
+
const { initial, selectedTaskId, resolvedTaskName, commandValue, contextValue, projects, setOpenSelect, setTaskPickerOpen, setCommandEditorOpen, setContextEditorOpen, fieldCallbacksRef, taskModeInitial, } = params;
|
|
8
9
|
return useMemo(() => {
|
|
9
10
|
const list = [
|
|
10
11
|
{
|
|
@@ -59,6 +60,10 @@ export function useCreateSteps(params) {
|
|
|
59
60
|
defaultValue: initial.runNow === "true" || initial.runNow === "yes"
|
|
60
61
|
? t("wizard.runNowNow")
|
|
61
62
|
: t("wizard.runNowWait"),
|
|
63
|
+
skip: (values) => {
|
|
64
|
+
const v = (values.interval ?? "").trim().toLowerCase();
|
|
65
|
+
return v === "manual" || v === "0";
|
|
66
|
+
},
|
|
62
67
|
onActivate: () => setOpenSelect("runNow"),
|
|
63
68
|
renderCustom: ({ value, isActive, onChange, onAdvance }) => {
|
|
64
69
|
fieldCallbacksRef.current.runNow = { value, onChange, onAdvance };
|
|
@@ -101,7 +106,30 @@ export function useCreateSteps(params) {
|
|
|
101
106
|
return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.projectPrompt"), isActive: isActive }));
|
|
102
107
|
},
|
|
103
108
|
},
|
|
109
|
+
{
|
|
110
|
+
key: "context",
|
|
111
|
+
prompt: t("wizard.contextPrompt"),
|
|
112
|
+
hint: t("wizard.contextHint"),
|
|
113
|
+
required: false,
|
|
114
|
+
inputType: "text",
|
|
115
|
+
defaultValue: contextValue || undefined,
|
|
116
|
+
onActivate: () => setContextEditorOpen(true),
|
|
117
|
+
validate: (value) => {
|
|
118
|
+
if (!value?.trim())
|
|
119
|
+
return true;
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(value);
|
|
122
|
+
const result = validateContext(parsed);
|
|
123
|
+
return result.valid;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
validationError: t("wizard.contextInvalid"),
|
|
130
|
+
renderCustom: ({ isActive }) => (_jsx(CodeEditorPreview, { value: contextValue, hint: t("wizard.contextHint"), isActive: isActive, onActivate: () => setContextEditorOpen(true) })),
|
|
131
|
+
},
|
|
104
132
|
];
|
|
105
133
|
return list;
|
|
106
|
-
}, [taskModeInitial, selectedTaskId, resolvedTaskName, initial, commandValue, projects]);
|
|
134
|
+
}, [taskModeInitial, selectedTaskId, resolvedTaskName, initial, commandValue, contextValue, projects]);
|
|
107
135
|
}
|
|
@@ -2,6 +2,7 @@ import { useCallback } from "react";
|
|
|
2
2
|
import { t } from "../../shared/i18n/index.js";
|
|
3
3
|
import { parseDuration } from "../../duration.js";
|
|
4
4
|
import { parseCommandLine, joinCommandLines } from "../../loop-config.js";
|
|
5
|
+
import { validateContext } from "../../core/context/validate-context.js";
|
|
5
6
|
export function useHandleComplete(params) {
|
|
6
7
|
const { selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, projects, loopService, } = params;
|
|
7
8
|
return useCallback((values) => {
|
|
@@ -15,7 +16,8 @@ export function useHandleComplete(params) {
|
|
|
15
16
|
catch {
|
|
16
17
|
return;
|
|
17
18
|
}
|
|
18
|
-
const
|
|
19
|
+
const isManual = interval === 0;
|
|
20
|
+
const intervalHuman = isManual ? "manual" : intervalInput.trim();
|
|
19
21
|
const isExistingTask = !!values.taskMode?.includes("Existing");
|
|
20
22
|
if (isExistingTask && !selectedTaskId && !values.taskId?.trim())
|
|
21
23
|
return;
|
|
@@ -41,6 +43,21 @@ export function useHandleComplete(params) {
|
|
|
41
43
|
const projectName = values.project ?? "";
|
|
42
44
|
const project = projects.find((p) => p.name === projectName);
|
|
43
45
|
const projectId = project?.id ?? currentProjectId;
|
|
46
|
+
let context;
|
|
47
|
+
const contextStr = (values.context ?? "").trim();
|
|
48
|
+
if (contextStr) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(contextStr);
|
|
51
|
+
const result = validateContext(parsed);
|
|
52
|
+
if (result.valid) {
|
|
53
|
+
context = result.context;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// invalid context, skip
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const cwdInput = (values.cwd ?? "").trim();
|
|
44
61
|
const options = {
|
|
45
62
|
interval,
|
|
46
63
|
taskId: isExistingTask
|
|
@@ -49,8 +66,10 @@ export function useHandleComplete(params) {
|
|
|
49
66
|
command: cmdOnly,
|
|
50
67
|
commandArgs: args,
|
|
51
68
|
commandRaw: isExistingTask ? undefined : cmdValue,
|
|
52
|
-
|
|
53
|
-
|
|
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()),
|
|
72
|
+
immediate: isManual ? false : runNowValue,
|
|
54
73
|
maxRuns: (values.maxRuns ?? "").trim()
|
|
55
74
|
? parseInt(values.maxRuns, 10)
|
|
56
75
|
: null,
|
|
@@ -58,6 +77,7 @@ export function useHandleComplete(params) {
|
|
|
58
77
|
description: (values.description ?? "").trim(),
|
|
59
78
|
projectId,
|
|
60
79
|
offset: null,
|
|
80
|
+
context,
|
|
61
81
|
};
|
|
62
82
|
const desc = (values.description ?? "").trim() || [cmdOnly, ...args].join(" ").trim();
|
|
63
83
|
if (mode === "edit" && editId) {
|
|
@@ -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) {
|
|
@@ -6,6 +6,7 @@ import { CodeEditorPreview } from "../../features/code-editor/CodeEditorPreview.
|
|
|
6
6
|
import { CodeEditorModal } from "../../features/code-editor/CodeEditorModal.js";
|
|
7
7
|
import { useInject } from "../../shared/hooks/useInject.js";
|
|
8
8
|
import { TYPES } from "../../shared/services/types.js";
|
|
9
|
+
import { validateContext } from "../../core/context/validate-context.js";
|
|
9
10
|
import crypto from "node:crypto";
|
|
10
11
|
import { t } from "../../shared/i18n/index.js";
|
|
11
12
|
import { joinCommandLines, parseCommandLine } from "../../loop-config.js";
|
|
@@ -36,7 +37,11 @@ export function TaskForm(props) {
|
|
|
36
37
|
const chainOptions = useMemo(() => [t("wizard.chainNone"), ...tasks.map((task) => task.name)], [tasks]);
|
|
37
38
|
const chainSelectOptions = useMemo(() => chainOptions.map((v) => ({ value: v, label: v })), [chainOptions]);
|
|
38
39
|
const [commandEditorOpen, setCommandEditorOpen] = useState(false);
|
|
40
|
+
const [contextEditorOpen, setContextEditorOpen] = useState(false);
|
|
41
|
+
const [contextValue, setContextValue] = useState(editTask?.context ? JSON.stringify(editTask.context) : "");
|
|
42
|
+
const [silentChainValue, setSilentChainValue] = useState(editTask?.silentChain ? t("board.silentChainYes") : "");
|
|
39
43
|
const [openChainField, setOpenChainField] = useState(null);
|
|
44
|
+
const [openSilentChain, setOpenSilentChain] = useState(false);
|
|
40
45
|
const chainFieldsRef = useRef({});
|
|
41
46
|
const resolveChainId = useCallback((val) => {
|
|
42
47
|
if (!val || val === t("wizard.chainNone"))
|
|
@@ -94,9 +99,41 @@ export function TaskForm(props) {
|
|
|
94
99
|
return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.onFailurePrompt"), isActive: isActive }));
|
|
95
100
|
},
|
|
96
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
|
+
},
|
|
111
|
+
{
|
|
112
|
+
key: "context",
|
|
113
|
+
prompt: t("wizard.contextPrompt"),
|
|
114
|
+
hint: t("wizard.contextHint"),
|
|
115
|
+
required: false,
|
|
116
|
+
inputType: "text",
|
|
117
|
+
defaultValue: contextValue || undefined,
|
|
118
|
+
onActivate: () => setContextEditorOpen(true),
|
|
119
|
+
validate: (value) => {
|
|
120
|
+
if (!value?.trim())
|
|
121
|
+
return true;
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(value);
|
|
124
|
+
const result = validateContext(parsed);
|
|
125
|
+
return result.valid;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
validationError: t("wizard.contextInvalid"),
|
|
132
|
+
renderCustom: ({ isActive }) => (_jsx(CodeEditorPreview, { value: contextValue, hint: t("wizard.contextHint"), isActive: isActive, onActivate: () => setContextEditorOpen(true) })),
|
|
133
|
+
},
|
|
97
134
|
];
|
|
98
135
|
return list;
|
|
99
|
-
}, [commandValue, chainOptions, editTask, resolveChainName]);
|
|
136
|
+
}, [commandValue, chainOptions, editTask, resolveChainName, contextValue, silentChainValue]);
|
|
100
137
|
const handleComplete = useCallback((values) => {
|
|
101
138
|
const name = values.name?.trim() ?? "";
|
|
102
139
|
const rawCommand = joinCommandLines(commandValue);
|
|
@@ -104,6 +141,21 @@ export function TaskForm(props) {
|
|
|
104
141
|
return;
|
|
105
142
|
const onSuccessTaskId = resolveChainId(values.onSuccess ?? "");
|
|
106
143
|
const onFailureTaskId = resolveChainId(values.onFailure ?? "");
|
|
144
|
+
const silentChain = values.silentChain === t("board.silentChainYes");
|
|
145
|
+
let context;
|
|
146
|
+
const contextStr = contextValue?.trim();
|
|
147
|
+
if (contextStr) {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(contextStr);
|
|
150
|
+
const result = validateContext(parsed);
|
|
151
|
+
if (result.valid) {
|
|
152
|
+
context = result.context;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// invalid context, skip
|
|
157
|
+
}
|
|
158
|
+
}
|
|
107
159
|
const commandSegments = commandValue
|
|
108
160
|
.split(/\n&&\n|\n&&|&&\n|&&/)
|
|
109
161
|
.map(s => s.trim())
|
|
@@ -136,6 +188,8 @@ export function TaskForm(props) {
|
|
|
136
188
|
steps,
|
|
137
189
|
onSuccessTaskId,
|
|
138
190
|
onFailureTaskId,
|
|
191
|
+
silentChain: silentChain || undefined,
|
|
192
|
+
context,
|
|
139
193
|
};
|
|
140
194
|
}
|
|
141
195
|
else {
|
|
@@ -146,6 +200,8 @@ export function TaskForm(props) {
|
|
|
146
200
|
commandRaw: commandValue,
|
|
147
201
|
onSuccessTaskId,
|
|
148
202
|
onFailureTaskId,
|
|
203
|
+
silentChain: silentChain || undefined,
|
|
204
|
+
context,
|
|
149
205
|
};
|
|
150
206
|
}
|
|
151
207
|
if (mode === "edit" && editTask) {
|
|
@@ -159,16 +215,25 @@ export function TaskForm(props) {
|
|
|
159
215
|
.then((result) => onDone(false, result.id))
|
|
160
216
|
.catch(() => { });
|
|
161
217
|
}
|
|
162
|
-
}, [commandValue, resolveChainId, mode, editTask, onDone]);
|
|
218
|
+
}, [commandValue, resolveChainId, mode, editTask, onDone, contextValue, silentChainValue]);
|
|
163
219
|
const title = mode === "edit"
|
|
164
220
|
? t("board.taskEditTitle")
|
|
165
221
|
: t("board.taskCreateTitle");
|
|
166
|
-
return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: title, steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: openChainField !== null || commandEditorOpen }), 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) => {
|
|
167
223
|
chainFieldsRef.current[openChainField]?.onChange(option.value);
|
|
168
224
|
chainFieldsRef.current[openChainField]?.onAdvance();
|
|
169
225
|
setOpenChainField(null);
|
|
170
226
|
}, onClose: () => setOpenChainField(null) })) : null, commandEditorOpen ? (_jsx(CodeEditorModal, { initialValue: commandValue, onSave: (v) => {
|
|
171
227
|
setCommandValue(v);
|
|
172
228
|
setCommandEditorOpen(false);
|
|
173
|
-
}, onCancel: () => setCommandEditorOpen(false) })) : null
|
|
229
|
+
}, onCancel: () => setCommandEditorOpen(false) })) : null, contextEditorOpen ? (_jsx(CodeEditorModal, { initialValue: contextValue, onSave: (v) => {
|
|
230
|
+
setContextValue(v);
|
|
231
|
+
setContextEditorOpen(false);
|
|
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] }));
|
|
174
239
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.13",
|
|
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",
|