loop-task 2.1.4 → 2.1.5
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 +119 -0
- package/dist/{tui → app}/index.js +4 -5
- package/dist/app/providers/index.js +1 -0
- package/dist/cli/import-writer.js +1 -1
- package/dist/cli.js +9 -9
- package/dist/client/cli-format.js +7 -0
- package/dist/client/commands.js +2 -164
- package/dist/client/ipc.js +3 -3
- package/dist/client/project-commands.js +159 -0
- package/dist/core/{command-runner.js → command/command-runner.js} +3 -3
- package/dist/core/{foreground-loop.js → foreground/index.js} +4 -4
- package/dist/core/{log-rotator.js → logging/log-rotator.js} +1 -1
- package/dist/core/loop/chain-executor.js +67 -0
- package/dist/core/loop/delay-utils.js +65 -0
- package/dist/core/loop/loop-controller.js +191 -0
- package/dist/core/loop/loop-runner.js +133 -0
- package/dist/core/loop/run-executor.js +90 -0
- package/dist/core/loop/types.js +1 -0
- package/dist/daemon/daemon-log.js +1 -1
- package/dist/daemon/http/helpers.js +81 -0
- package/dist/daemon/http/openapi.js +92 -0
- package/dist/daemon/http/route-loops.js +218 -0
- package/dist/daemon/http/route-misc.js +36 -0
- package/dist/daemon/http/route-projects.js +55 -0
- package/dist/daemon/http/route-tasks.js +53 -0
- package/dist/daemon/http/routes.js +17 -0
- package/dist/daemon/http/server.js +64 -0
- package/dist/daemon/http/sse.js +36 -0
- package/dist/daemon/index.js +7 -9
- package/dist/{ipc/handlers → daemon/ipc}/logs-stream.js +1 -1
- package/dist/daemon/managers/loop-entry.js +24 -0
- package/dist/daemon/{manager.js → managers/loop-manager.js} +32 -152
- package/dist/daemon/managers/loop-options.js +16 -0
- package/dist/daemon/managers/loop-serialization.js +43 -0
- package/dist/daemon/{projects.js → managers/project-manager.js} +6 -4
- package/dist/daemon/{task-manager.js → managers/task-manager.js} +5 -3
- package/dist/daemon/server/handlers/index.js +86 -0
- package/dist/daemon/server/handlers/log-handlers.js +130 -0
- package/dist/daemon/server/handlers/loop-handlers.js +54 -0
- package/dist/daemon/server/handlers/project-handlers.js +37 -0
- package/dist/daemon/server/handlers/task-handlers.js +20 -0
- package/dist/daemon/server/index.js +113 -0
- package/dist/daemon/{spawner.js → spawner/index.js} +4 -4
- package/dist/daemon/{state.js → state/index.js} +4 -4
- package/dist/daemon/{file-watcher.js → watcher/index.js} +8 -6
- package/dist/duration.js +1 -1
- package/dist/{tui/state.js → entities/loops/filters.js} +0 -76
- package/dist/entities/loops/index.js +1 -0
- package/dist/entities/projects/filters.js +69 -0
- package/dist/entities/projects/index.js +1 -0
- package/dist/entities/tasks/filters.js +41 -0
- package/dist/entities/tasks/index.js +1 -0
- package/dist/{tui/components → features/chain-editor}/ChainEditor.js +7 -5
- package/dist/features/code-editor/CodeEditorModal.js +84 -0
- package/dist/{tui/components → features/code-editor}/CodeEditorPreview.js +3 -3
- package/dist/features/code-editor/useEditorKeyboard.js +147 -0
- package/dist/features/code-editor/useModalDimensions.js +9 -0
- package/dist/{tui → features/commands}/commands.js +2 -2
- package/dist/features/commands/useCommandHandlers.js +147 -0
- package/dist/features/commands/useContextualActions.js +75 -0
- package/dist/features/commands/useGlobalShortcuts.js +152 -0
- package/dist/features/forms/FormRouter.js +44 -0
- package/dist/{tui/components → features/overlays}/ContextHelpModal.js +3 -3
- package/dist/{tui/components → features/overlays}/ExportModal.js +3 -3
- package/dist/{tui/components → features/overlays}/HelpGuideModal.js +3 -3
- package/dist/{tui/components → features/overlays}/HelpModal.js +3 -3
- package/dist/features/overlays/OverlayStack.js +9 -0
- package/dist/{tui/components → features/overlays}/ProjectsModal.js +2 -2
- package/dist/{tui/components → features/overlays}/TaskPickerModal.js +2 -2
- package/dist/{tui/components → features/overlays}/WelcomeScreen.js +5 -3
- package/dist/features/overlays/useOverlayStack.js +51 -0
- package/dist/features/state/useAppState.js +150 -0
- package/dist/logger.js +0 -1
- package/dist/loop-config.js +9 -4
- package/dist/{config → shared/config}/constants.js +1 -1
- package/dist/shared/container/index.js +22 -0
- package/dist/{tui → shared}/hooks/useBreakpoint.js +1 -1
- package/dist/shared/hooks/useInject.js +6 -0
- package/dist/{tui → shared}/hooks/useLogStream.js +7 -5
- package/dist/{hooks → shared/hooks}/useLoopFormValidation.js +2 -2
- package/dist/{tui → shared}/hooks/useLoopPolling.js +6 -4
- package/dist/shared/{useUndoRedo.js → hooks/useUndoRedo.js} +2 -4
- package/dist/shared/providers/InversifyProvider.js +6 -0
- package/dist/shared/services/export-service.js +43 -0
- package/dist/shared/services/log-service.js +28 -0
- package/dist/shared/services/loop-service.js +78 -0
- package/dist/shared/services/project-service.js +39 -0
- package/dist/shared/services/task-service.js +41 -0
- package/dist/shared/services/types.js +7 -0
- package/dist/{tui/components → shared/ui}/Button.js +1 -1
- package/dist/{tui/components → shared/ui}/DebugPanel.js +1 -1
- package/dist/{tui/components → shared/ui}/FocusableButton.js +1 -1
- package/dist/{tui/components → shared/ui}/FocusableInput.js +1 -1
- package/dist/{tui/components → shared/ui}/FocusableList.js +1 -1
- package/dist/{tui/components → shared/ui}/Modal.js +1 -1
- package/dist/{tui/components → shared/ui}/SelectModal.js +2 -2
- package/dist/{tui/components → shared/ui}/Toast.js +1 -1
- package/dist/shared/ui/state.js +9 -0
- package/dist/{tui → shared}/utils/paste.js +2 -2
- package/dist/{tui → shared}/utils/validation.js +1 -1
- package/dist/widgets/command-input/CommandDropdown.js +69 -0
- package/dist/widgets/command-input/CommandInput.js +129 -0
- package/dist/widgets/command-input/HintBar.js +9 -0
- package/dist/widgets/command-input/InputModes.js +68 -0
- package/dist/{tui/components → widgets/commands-browser}/CommandsBrowserModal.js +3 -3
- package/dist/{tui/components → widgets/header}/Header.js +3 -3
- package/dist/{tui/components → widgets/header}/TabBar.js +1 -1
- package/dist/{tui/components → widgets/left-panel}/LeftPanel.js +3 -3
- package/dist/{tui/components → widgets/left-panel}/Navigator.js +3 -3
- package/dist/{tui/components → widgets/left-panel}/ProjectsPage.js +9 -34
- package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
- package/dist/{tui/components → widgets/left-panel}/TaskBrowser.js +4 -4
- package/dist/{tui/components → widgets/log-modal}/LogModal.js +8 -6
- package/dist/widgets/loop-form/CreateForm.js +75 -0
- package/dist/widgets/loop-form/TextField.js +9 -0
- package/dist/{tui/components → widgets/loop-form}/WizardForm.js +5 -10
- package/dist/widgets/loop-form/useCreateSteps.js +107 -0
- package/dist/widgets/loop-form/useHandleComplete.js +74 -0
- package/dist/{tui/components → widgets/project-form}/ProjectForm.js +9 -7
- package/dist/{tui/components → widgets/right-panel}/Inspector.js +4 -4
- package/dist/{tui/components → widgets/right-panel}/RightPanel.js +4 -4
- package/dist/{tui/components → widgets/right-panel}/RunHistory.js +52 -4
- package/dist/{tui/components → widgets/task-form}/TaskForm.js +11 -9
- package/package.json +2 -1
- package/dist/core/loop-controller.js +0 -511
- package/dist/daemon/http-server.js +0 -599
- package/dist/daemon/server.js +0 -374
- package/dist/tui/App.js +0 -680
- package/dist/tui/components/CodeEditorModal.js +0 -244
- package/dist/tui/components/CommandInput.js +0 -273
- package/dist/tui/components/CreateForm.js +0 -224
- package/dist/tui/daemon.js +0 -142
- package/dist/tui/format.js +0 -111
- /package/dist/{tui/router.js → app/router/index.js} +0 -0
- /package/dist/{tui → app}/types.js +0 -0
- /package/dist/core/{resolve-cwd.js → command/resolve-cwd.js} +0 -0
- /package/dist/core/{context-parser.js → context/context-parser.js} +0 -0
- /package/dist/core/{template.js → context/template.js} +0 -0
- /package/dist/core/{log-parser.js → logging/log-parser.js} +0 -0
- /package/dist/core/{scheduling.js → scheduling/index.js} +0 -0
- /package/dist/{ipc → daemon/ipc}/send.js +0 -0
- /package/dist/{config → shared/config}/paths.js +0 -0
- /package/dist/{i18n → shared/i18n}/en.json +0 -0
- /package/dist/{i18n → shared/i18n}/index.js +0 -0
- /package/dist/{board → shared/ui}/format.js +0 -0
- /package/dist/{tui → shared/ui}/hooks/useHoverState.js +0 -0
- /package/dist/{tui → shared/ui}/theme.js +0 -0
- /package/dist/{tui → shared}/utils/syntax.js +0 -0
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useCallback, useMemo, useRef } from "react";
|
|
3
3
|
import { Box, Text, useInput } from "ink";
|
|
4
|
-
import { darkTheme as theme } from "
|
|
5
|
-
import { t } from "../../i18n/index.js";
|
|
6
|
-
import { useLoopFormValidation } from "../../hooks/useLoopFormValidation.js";
|
|
4
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
import { useLoopFormValidation } from "../../shared/hooks/useLoopFormValidation.js";
|
|
7
7
|
import { copyToClipboard } from "../../shared/clipboard.js";
|
|
8
|
-
import { sanitizePaste } from "
|
|
9
|
-
|
|
10
|
-
const borderColor = isActive ? theme.accent.brand : theme.border.dim;
|
|
11
|
-
const backgroundColor = isActive ? theme.bg.input : undefined;
|
|
12
|
-
const showHint = value.length === 0;
|
|
13
|
-
return (_jsxs(Box, { borderStyle: "single", borderColor: borderColor, backgroundColor: backgroundColor, paddingLeft: 1, overflow: "hidden", width: "100%", children: [_jsx(Text, { color: showHint ? theme.text.muted : theme.text.primary, children: showHint ? hint : value }), isActive ? _jsx(Text, { inverse: true, children: " " }) : null] }));
|
|
14
|
-
}
|
|
8
|
+
import { sanitizePaste } from "../../shared/utils/paste.js";
|
|
9
|
+
import { TextField } from "./TextField.js";
|
|
15
10
|
export function WizardForm(props) {
|
|
16
11
|
const { title, steps, onComplete, onCancel, disabled = false } = props;
|
|
17
12
|
const [values, setValues] = useState({});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { t } from "../../shared/i18n/index.js";
|
|
4
|
+
import { SelectValueField } from "../../shared/ui/SelectModal.js";
|
|
5
|
+
import { CodeEditorPreview } from "../../features/code-editor/CodeEditorPreview.js";
|
|
6
|
+
export function useCreateSteps(params) {
|
|
7
|
+
const { initial, selectedTaskId, resolvedTaskName, commandValue, projects, setOpenSelect, setTaskPickerOpen, setCommandEditorOpen, fieldCallbacksRef, taskModeInitial, } = params;
|
|
8
|
+
return useMemo(() => {
|
|
9
|
+
const list = [
|
|
10
|
+
{
|
|
11
|
+
key: "interval",
|
|
12
|
+
prompt: t("wizard.intervalPrompt"),
|
|
13
|
+
hint: t("wizard.intervalHint"),
|
|
14
|
+
required: true,
|
|
15
|
+
inputType: "text",
|
|
16
|
+
defaultValue: initial.interval ?? undefined,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: "taskMode",
|
|
20
|
+
prompt: t("wizard.taskModePrompt"),
|
|
21
|
+
hint: t("board.hintTaskMode"),
|
|
22
|
+
required: true,
|
|
23
|
+
defaultValue: taskModeInitial,
|
|
24
|
+
onActivate: () => setOpenSelect("taskMode"),
|
|
25
|
+
renderCustom: ({ value, isActive, onChange, onAdvance }) => {
|
|
26
|
+
fieldCallbacksRef.current.taskMode = { value, onChange, onAdvance };
|
|
27
|
+
return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.taskModePrompt"), isActive: isActive }));
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: "taskId",
|
|
32
|
+
prompt: resolvedTaskName
|
|
33
|
+
? t("board.selectedTask", { name: resolvedTaskName })
|
|
34
|
+
: t("board.chooseTask"),
|
|
35
|
+
hint: t("board.hintTask"),
|
|
36
|
+
required: true,
|
|
37
|
+
inputType: "text",
|
|
38
|
+
defaultValue: selectedTaskId ?? initial.taskId ?? undefined,
|
|
39
|
+
skip: (values) => !values.taskMode?.includes("Existing"),
|
|
40
|
+
onActivate: () => setTaskPickerOpen(true),
|
|
41
|
+
renderCustom: ({ isActive }) => (_jsx(SelectValueField, { label: resolvedTaskName, placeholder: t("board.chooseTask"), isActive: isActive })),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "command",
|
|
45
|
+
prompt: t("wizard.commandPrompt"),
|
|
46
|
+
hint: t("wizard.commandHint"),
|
|
47
|
+
required: true,
|
|
48
|
+
inputType: "text",
|
|
49
|
+
defaultValue: commandValue || undefined,
|
|
50
|
+
skip: (values) => !!values.taskMode?.includes("Existing"),
|
|
51
|
+
onActivate: () => setCommandEditorOpen(true),
|
|
52
|
+
renderCustom: ({ isActive }) => (_jsx(CodeEditorPreview, { value: commandValue, hint: t("wizard.commandHint"), isActive: isActive, onActivate: () => setCommandEditorOpen(true) })),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "runNow",
|
|
56
|
+
prompt: t("wizard.runNowPrompt"),
|
|
57
|
+
hint: t("board.hintRunNow"),
|
|
58
|
+
required: true,
|
|
59
|
+
defaultValue: initial.runNow === "true" || initial.runNow === "yes"
|
|
60
|
+
? t("wizard.runNowNow")
|
|
61
|
+
: t("wizard.runNowWait"),
|
|
62
|
+
onActivate: () => setOpenSelect("runNow"),
|
|
63
|
+
renderCustom: ({ value, isActive, onChange, onAdvance }) => {
|
|
64
|
+
fieldCallbacksRef.current.runNow = { value, onChange, onAdvance };
|
|
65
|
+
return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.runNowPrompt"), isActive: isActive }));
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: "cwd",
|
|
70
|
+
prompt: t("wizard.cwdPrompt"),
|
|
71
|
+
hint: t("wizard.cwdHint"),
|
|
72
|
+
required: false,
|
|
73
|
+
inputType: "text",
|
|
74
|
+
defaultValue: initial.cwd ?? undefined,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "description",
|
|
78
|
+
prompt: t("wizard.descriptionPrompt"),
|
|
79
|
+
hint: t("wizard.descriptionHint"),
|
|
80
|
+
required: true,
|
|
81
|
+
inputType: "text",
|
|
82
|
+
defaultValue: initial.description ?? undefined,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: "maxRuns",
|
|
86
|
+
prompt: t("wizard.maxRunsPrompt"),
|
|
87
|
+
hint: t("wizard.maxRunsHint"),
|
|
88
|
+
required: false,
|
|
89
|
+
inputType: "text",
|
|
90
|
+
defaultValue: initial.maxRuns ?? undefined,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: "project",
|
|
94
|
+
prompt: t("wizard.projectPrompt"),
|
|
95
|
+
hint: t("wizard.projectHint"),
|
|
96
|
+
required: false,
|
|
97
|
+
defaultValue: projects.find((p) => p.id === (initial.project ?? "default"))?.name ?? projects[0]?.name,
|
|
98
|
+
onActivate: () => setOpenSelect("project"),
|
|
99
|
+
renderCustom: ({ value, isActive, onChange, onAdvance }) => {
|
|
100
|
+
fieldCallbacksRef.current.project = { value, onChange, onAdvance };
|
|
101
|
+
return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.projectPrompt"), isActive: isActive }));
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
return list;
|
|
106
|
+
}, [taskModeInitial, selectedTaskId, resolvedTaskName, initial, commandValue, projects]);
|
|
107
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { t } from "../../shared/i18n/index.js";
|
|
3
|
+
import { parseDuration } from "../../duration.js";
|
|
4
|
+
import { parseCommandLine, joinCommandLines } from "../../loop-config.js";
|
|
5
|
+
export function useHandleComplete(params) {
|
|
6
|
+
const { selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, projects, loopService, } = params;
|
|
7
|
+
return useCallback((values) => {
|
|
8
|
+
const intervalInput = values.interval ?? "";
|
|
9
|
+
if (!intervalInput.trim())
|
|
10
|
+
return;
|
|
11
|
+
let interval;
|
|
12
|
+
try {
|
|
13
|
+
interval = parseDuration(intervalInput.trim());
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const intervalHuman = intervalInput.trim();
|
|
19
|
+
const isExistingTask = !!values.taskMode?.includes("Existing");
|
|
20
|
+
if (isExistingTask && !selectedTaskId && !values.taskId?.trim())
|
|
21
|
+
return;
|
|
22
|
+
const cmdValue = values.command ?? commandValue;
|
|
23
|
+
if (!isExistingTask && !cmdValue.trim())
|
|
24
|
+
return;
|
|
25
|
+
const cmd = isExistingTask
|
|
26
|
+
? ""
|
|
27
|
+
: joinCommandLines(cmdValue);
|
|
28
|
+
let cmdOnly = "";
|
|
29
|
+
let args = [];
|
|
30
|
+
if (cmd.trim()) {
|
|
31
|
+
try {
|
|
32
|
+
const tokens = parseCommandLine(cmd.trim());
|
|
33
|
+
cmdOnly = tokens[0] ?? "";
|
|
34
|
+
args = tokens.slice(1);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const runNowValue = values.runNow === t("wizard.runNowNow");
|
|
41
|
+
const projectName = values.project ?? "";
|
|
42
|
+
const project = projects.find((p) => p.name === projectName);
|
|
43
|
+
const projectId = project?.id ?? currentProjectId;
|
|
44
|
+
const options = {
|
|
45
|
+
interval,
|
|
46
|
+
taskId: isExistingTask
|
|
47
|
+
? (selectedTaskId ?? values.taskId?.trim() ?? null)
|
|
48
|
+
: null,
|
|
49
|
+
command: cmdOnly,
|
|
50
|
+
commandArgs: args,
|
|
51
|
+
commandRaw: isExistingTask ? undefined : cmdValue,
|
|
52
|
+
cwd: (values.cwd ?? "").trim() || process.cwd(),
|
|
53
|
+
immediate: runNowValue,
|
|
54
|
+
maxRuns: (values.maxRuns ?? "").trim()
|
|
55
|
+
? parseInt(values.maxRuns, 10)
|
|
56
|
+
: null,
|
|
57
|
+
verbose: false,
|
|
58
|
+
description: (values.description ?? "").trim(),
|
|
59
|
+
projectId,
|
|
60
|
+
offset: null,
|
|
61
|
+
};
|
|
62
|
+
const desc = (values.description ?? "").trim() || [cmdOnly, ...args].join(" ").trim();
|
|
63
|
+
if (mode === "edit" && editId) {
|
|
64
|
+
loopService.update(editId, options, intervalHuman)
|
|
65
|
+
.then((id) => onDone(true, id, desc))
|
|
66
|
+
.catch(() => { });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
loopService.create(options, intervalHuman)
|
|
70
|
+
.then((id) => onDone(false, id, desc))
|
|
71
|
+
.catch(() => { });
|
|
72
|
+
}
|
|
73
|
+
}, [selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, projects]);
|
|
74
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo, useCallback, useState, useRef } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { useInject } from "../../shared/hooks/useInject.js";
|
|
4
|
+
import { TYPES } from "../../shared/services/types.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
import { WizardForm } from "../loop-form/WizardForm.js";
|
|
7
|
+
import { SelectModal, SelectValueField } from "../../shared/ui/SelectModal.js";
|
|
8
|
+
import { PROJECT_COLORS, PROJECT_COLOR_KEYS } from "../../shared/config/constants.js";
|
|
8
9
|
export function ProjectFormView(props) {
|
|
9
10
|
const { mode, editProject, onCancel, onDone } = props;
|
|
11
|
+
const projectService = useInject(TYPES.ProjectService);
|
|
10
12
|
const colorKeyFor = (color) => {
|
|
11
13
|
const found = PROJECT_COLOR_KEYS.find((k) => PROJECT_COLORS[k] === color);
|
|
12
14
|
return found ?? "cyan";
|
|
@@ -51,12 +53,12 @@ export function ProjectFormView(props) {
|
|
|
51
53
|
const colorKey = values.color ?? "cyan";
|
|
52
54
|
const color = PROJECT_COLORS[colorKey] ?? PROJECT_COLORS.cyan;
|
|
53
55
|
if (mode === "edit" && editProject) {
|
|
54
|
-
|
|
56
|
+
projectService.update(editProject.id, name, color, values.directory?.trim() || undefined)
|
|
55
57
|
.then(() => onDone(true, name))
|
|
56
58
|
.catch(() => { });
|
|
57
59
|
}
|
|
58
60
|
else {
|
|
59
|
-
|
|
61
|
+
projectService.create(name, color, values.directory?.trim() || undefined)
|
|
60
62
|
.then(() => onDone(false, name))
|
|
61
63
|
.catch(() => { });
|
|
62
64
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { darkTheme as theme, statusColor } from "
|
|
4
|
-
import { describeLoop, commandLine, timeAgo, timeUntil } from "
|
|
5
|
-
import { t } from "../../i18n/index.js";
|
|
6
|
-
import { resolveEffectiveCwd } from "../../core/resolve-cwd.js";
|
|
3
|
+
import { darkTheme as theme, statusColor } from "../../shared/ui/theme.js";
|
|
4
|
+
import { describeLoop, commandLine, timeAgo, timeUntil } from "../../shared/ui/format.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
import { resolveEffectiveCwd } from "../../core/command/resolve-cwd.js";
|
|
7
7
|
const LABEL_WIDTH = 11;
|
|
8
8
|
function Field(props) {
|
|
9
9
|
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), _jsx(Text, { color: theme.text.primary, children: props.children })] }));
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { darkTheme as theme, tabAccentColor } from "
|
|
4
|
-
import { t } from "../../i18n/index.js";
|
|
3
|
+
import { darkTheme as theme, tabAccentColor } from "../../shared/ui/theme.js";
|
|
4
|
+
import { t } from "../../shared/i18n/index.js";
|
|
5
5
|
import { Inspector } from "./Inspector.js";
|
|
6
6
|
import { RunHistory } from "./RunHistory.js";
|
|
7
|
-
import { FocusableButton } from "
|
|
8
|
-
import { commandLine } from "
|
|
7
|
+
import { FocusableButton } from "../../shared/ui/FocusableButton.js";
|
|
8
|
+
import { commandLine } from "../../shared/ui/format.js";
|
|
9
9
|
const DIVIDER = "\u2500".repeat(40);
|
|
10
10
|
export function RightPanel(props) {
|
|
11
11
|
const { isFocused, navActive = true, activeTab, loop, selectedRunIndex, onSelectRun, onOpenRun, selectedTask, allTasks, selectedProject, projectLoopCount, onProjectEdit, onProjectDelete, projects, } = props;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import { ScrollList } from "ink-scroll-list";
|
|
4
|
-
import { darkTheme as theme } from "
|
|
5
|
-
import { formatRunTime, formatRunDuration, formatFileSize } from "
|
|
6
|
-
import { t } from "../../i18n/index.js";
|
|
4
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
5
|
+
import { formatRunTime, formatRunDuration, formatFileSize } from "../../shared/ui/format.js";
|
|
6
|
+
import { t } from "../../shared/i18n/index.js";
|
|
7
7
|
function runIcon(run) {
|
|
8
8
|
if (run.status === "running")
|
|
9
9
|
return "\u21bb";
|
|
@@ -14,6 +14,54 @@ function runIconColor(run) {
|
|
|
14
14
|
return theme.accent.loop;
|
|
15
15
|
return run.exitCode === 0 ? theme.semantic.success : theme.semantic.danger;
|
|
16
16
|
}
|
|
17
|
+
const CHAIN_LABEL_MAX = 28;
|
|
18
|
+
function truncateChainLabel(label) {
|
|
19
|
+
if (label.length <= CHAIN_LABEL_MAX)
|
|
20
|
+
return label;
|
|
21
|
+
const ellipsis = "...";
|
|
22
|
+
const keep = CHAIN_LABEL_MAX - ellipsis.length;
|
|
23
|
+
return label.slice(0, keep) + ellipsis;
|
|
24
|
+
}
|
|
25
|
+
export function groupRunsByCycle(runs) {
|
|
26
|
+
const byRun = new Map();
|
|
27
|
+
for (const r of runs) {
|
|
28
|
+
const group = byRun.get(r.runNumber);
|
|
29
|
+
if (group)
|
|
30
|
+
group.push(r);
|
|
31
|
+
else
|
|
32
|
+
byRun.set(r.runNumber, [r]);
|
|
33
|
+
}
|
|
34
|
+
const result = [];
|
|
35
|
+
for (const group of byRun.values()) {
|
|
36
|
+
if (group.length === 1) {
|
|
37
|
+
result.push(group[0]);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const first = group[0];
|
|
41
|
+
const last = group[group.length - 1];
|
|
42
|
+
const anyRunning = group.some((r) => r.status === "running");
|
|
43
|
+
const totalDuration = group.reduce((sum, r) => sum + r.duration, 0);
|
|
44
|
+
const totalLogSize = group.reduce((sum, r) => sum + r.logSize, 0);
|
|
45
|
+
const chainNames = group
|
|
46
|
+
.map((r) => r.chainName)
|
|
47
|
+
.filter((n) => Boolean(n));
|
|
48
|
+
const chainLabel = chainNames.length > 0
|
|
49
|
+
? truncateChainLabel(`\u2192 ${chainNames.join(" \u2192 ")}`)
|
|
50
|
+
: undefined;
|
|
51
|
+
result.push({
|
|
52
|
+
runNumber: first.runNumber,
|
|
53
|
+
startedAt: first.startedAt,
|
|
54
|
+
exitCode: anyRunning ? -1 : last.exitCode,
|
|
55
|
+
duration: totalDuration,
|
|
56
|
+
logSize: totalLogSize,
|
|
57
|
+
status: anyRunning ? "running" : "completed",
|
|
58
|
+
logOffset: first.logOffset,
|
|
59
|
+
chainGroupId: first.chainGroupId,
|
|
60
|
+
chainName: chainLabel,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
17
65
|
const SPARK_CHARS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
18
66
|
function sparkline(durations) {
|
|
19
67
|
if (durations.length === 0)
|
|
@@ -65,7 +113,7 @@ function computeTrends(runs) {
|
|
|
65
113
|
const LIMIT = 15;
|
|
66
114
|
export function RunHistory(props) {
|
|
67
115
|
const { loop, selectedRunIndex, onSelectRun, onOpenRun, isFocused, navActive = true } = props;
|
|
68
|
-
const runs = loop?.runHistory ?? [];
|
|
116
|
+
const runs = groupRunsByCycle(loop?.runHistory ?? []);
|
|
69
117
|
const reversed = [...runs].reverse();
|
|
70
118
|
const n = reversed.length;
|
|
71
119
|
useInput((input, key) => {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useCallback, useState, useRef } from "react";
|
|
3
|
-
import { WizardForm } from "
|
|
4
|
-
import { SelectModal, SelectValueField } from "
|
|
5
|
-
import { CodeEditorPreview } from "
|
|
6
|
-
import { CodeEditorModal } from "
|
|
7
|
-
import {
|
|
3
|
+
import { WizardForm } from "../loop-form/WizardForm.js";
|
|
4
|
+
import { SelectModal, SelectValueField } from "../../shared/ui/SelectModal.js";
|
|
5
|
+
import { CodeEditorPreview } from "../../features/code-editor/CodeEditorPreview.js";
|
|
6
|
+
import { CodeEditorModal } from "../../features/code-editor/CodeEditorModal.js";
|
|
7
|
+
import { useInject } from "../../shared/hooks/useInject.js";
|
|
8
|
+
import { TYPES } from "../../shared/services/types.js";
|
|
8
9
|
import crypto from "node:crypto";
|
|
9
|
-
import { t } from "../../i18n/index.js";
|
|
10
|
+
import { t } from "../../shared/i18n/index.js";
|
|
10
11
|
import { joinCommandLines, parseCommandLine } from "../../loop-config.js";
|
|
11
12
|
function parseArgs(cmd) {
|
|
12
13
|
const tokens = [];
|
|
@@ -22,6 +23,7 @@ function joinCommand(task) {
|
|
|
22
23
|
}
|
|
23
24
|
export function TaskForm(props) {
|
|
24
25
|
const { mode, editTask, onCancel, onDone } = props;
|
|
26
|
+
const taskService = useInject(TYPES.TaskService);
|
|
25
27
|
const [tasks, setTasks] = useState([]);
|
|
26
28
|
const [commandValue, setCommandValue] = useState(editTask
|
|
27
29
|
? (editTask.steps?.length
|
|
@@ -29,7 +31,7 @@ export function TaskForm(props) {
|
|
|
29
31
|
: (editTask.commandRaw ?? joinCommand(editTask)))
|
|
30
32
|
: "");
|
|
31
33
|
useEffect(() => {
|
|
32
|
-
|
|
34
|
+
taskService.list().then(setTasks).catch(() => setTasks([]));
|
|
33
35
|
}, []);
|
|
34
36
|
const chainOptions = useMemo(() => [t("wizard.chainNone"), ...tasks.map((task) => task.name)], [tasks]);
|
|
35
37
|
const chainSelectOptions = useMemo(() => chainOptions.map((v) => ({ value: v, label: v })), [chainOptions]);
|
|
@@ -147,13 +149,13 @@ export function TaskForm(props) {
|
|
|
147
149
|
};
|
|
148
150
|
}
|
|
149
151
|
if (mode === "edit" && editTask) {
|
|
150
|
-
|
|
152
|
+
taskService.update(editTask.id, payload)
|
|
151
153
|
.then(() => onDone(true, editTask.id))
|
|
152
154
|
.catch(() => { });
|
|
153
155
|
}
|
|
154
156
|
else {
|
|
155
157
|
const id = crypto.randomUUID().slice(0, 8);
|
|
156
|
-
|
|
158
|
+
taskService.create({ id, ...payload })
|
|
157
159
|
.then((result) => onDone(false, result.id))
|
|
158
160
|
.catch(() => { });
|
|
159
161
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loop-task",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"description": "Loop engineering toolkit. Run any command on a cadence, in the background, managed from a terminal board. Schedule tests, builds, syncs, or agent prompts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"ink-select-input": "^6.2.0",
|
|
57
57
|
"ink-spinner": "^5.0.0",
|
|
58
58
|
"ink-text-input": "^6.0.0",
|
|
59
|
+
"inversify": "^8.1.1",
|
|
59
60
|
"inversify-hooks": "^4.0.0",
|
|
60
61
|
"ms": "^2.1.3",
|
|
61
62
|
"react": "^19.2.7"
|