@plainconceptsplatform/loop-task 2.6.0
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/LICENSE +21 -0
- package/README.md +534 -0
- package/dist/app/App.js +132 -0
- package/dist/app/BoardLayout.js +5 -0
- package/dist/app/index.js +48 -0
- package/dist/app/providers/index.js +1 -0
- package/dist/app/router/index.js +16 -0
- package/dist/app/types.js +1 -0
- package/dist/cli/import-validator.js +152 -0
- package/dist/cli/import-writer.js +81 -0
- package/dist/cli.js +573 -0
- package/dist/client/cli-format.js +7 -0
- package/dist/client/commands.js +162 -0
- package/dist/client/ipc.js +96 -0
- package/dist/client/project-commands.js +164 -0
- package/dist/core/command/command-runner.js +300 -0
- package/dist/core/command/process-tree.js +73 -0
- package/dist/core/command/resolve-cwd.js +8 -0
- package/dist/core/command/stdout-capture-transform.js +73 -0
- package/dist/core/context/context-parser.js +65 -0
- package/dist/core/context/log-context.js +3 -0
- package/dist/core/context/template.js +20 -0
- package/dist/core/context/validate-context.js +19 -0
- package/dist/core/foreground/index.js +81 -0
- package/dist/core/logging/bounded-log-reader.js +368 -0
- package/dist/core/logging/log-follower.js +26 -0
- package/dist/core/logging/log-parser.js +29 -0
- package/dist/core/logging/log-rotator.js +34 -0
- package/dist/core/logging/rotating-log-stream.js +137 -0
- package/dist/core/loop/chain-executor.js +171 -0
- package/dist/core/loop/delay-utils.js +65 -0
- package/dist/core/loop/loop-controller.js +240 -0
- package/dist/core/loop/loop-runner.js +194 -0
- package/dist/core/loop/run-executor.js +144 -0
- package/dist/core/loop/types.js +1 -0
- package/dist/core/scheduling/index.js +16 -0
- package/dist/daemon/daemon-log.js +11 -0
- package/dist/daemon/diagnostics.js +85 -0
- package/dist/daemon/http/helpers.js +81 -0
- package/dist/daemon/http/openapi.js +119 -0
- package/dist/daemon/http/route-loops.js +311 -0
- package/dist/daemon/http/route-misc.js +60 -0
- package/dist/daemon/http/route-projects.js +55 -0
- package/dist/daemon/http/route-tasks.js +141 -0
- package/dist/daemon/http/routes.js +19 -0
- package/dist/daemon/http/server.js +82 -0
- package/dist/daemon/http/sse.js +36 -0
- package/dist/daemon/index.js +189 -0
- package/dist/daemon/ipc/send.js +5 -0
- package/dist/daemon/managers/loop-entry.js +27 -0
- package/dist/daemon/managers/loop-manager.js +346 -0
- package/dist/daemon/managers/loop-options.js +17 -0
- package/dist/daemon/managers/loop-serialization.js +53 -0
- package/dist/daemon/managers/project-manager.js +170 -0
- package/dist/daemon/managers/task-manager.js +94 -0
- package/dist/daemon/mcp/index.js +2 -0
- package/dist/daemon/mcp/openapi-sync.js +50 -0
- package/dist/daemon/mcp/server.js +167 -0
- package/dist/daemon/mcp/tools.js +382 -0
- package/dist/daemon/recipe/deferred-reload.js +32 -0
- package/dist/daemon/recipe/diagram-reader.js +26 -0
- package/dist/daemon/recipe/file-writer.js +52 -0
- package/dist/daemon/recipe/id-remapper.js +46 -0
- package/dist/daemon/recipe/runtime-state.js +23 -0
- package/dist/daemon/recipe/scanner.js +165 -0
- package/dist/daemon/recipe/task-store.js +33 -0
- package/dist/daemon/recipe/validator.js +35 -0
- package/dist/daemon/server/handlers/diagnostics-handlers.js +7 -0
- package/dist/daemon/server/handlers/index.js +101 -0
- package/dist/daemon/server/handlers/log-handlers.js +129 -0
- package/dist/daemon/server/handlers/loop-handlers.js +64 -0
- package/dist/daemon/server/handlers/project-handlers.js +42 -0
- package/dist/daemon/server/handlers/settings-handlers.js +8 -0
- package/dist/daemon/server/handlers/task-handlers.js +20 -0
- package/dist/daemon/server/handlers/telemetry-handlers.js +5 -0
- package/dist/daemon/server/index.js +117 -0
- package/dist/daemon/settings-manager.js +73 -0
- package/dist/daemon/spawner/index.js +204 -0
- package/dist/daemon/state/index.js +209 -0
- package/dist/daemon/telemetry/agent-integrations/agent-integration.js +1 -0
- package/dist/daemon/telemetry/agent-integrations/claude-code-integration.js +55 -0
- package/dist/daemon/telemetry/agent-integrations/detect-agent-integration.js +20 -0
- package/dist/daemon/telemetry/agent-integrations/index.js +3 -0
- package/dist/daemon/telemetry/agent-integrations/opencode-integration.js +72 -0
- package/dist/daemon/telemetry/index.js +6 -0
- package/dist/daemon/telemetry/noop-telemetry-adapter.js +53 -0
- package/dist/daemon/telemetry/open-telemetry-adapter.js +464 -0
- package/dist/daemon/telemetry/telemetry-manager.js +147 -0
- package/dist/daemon/telemetry/telemetry-redaction.js +67 -0
- package/dist/daemon/telemetry/telemetry-types.js +45 -0
- package/dist/daemon/telemetry/telemetry.js +1 -0
- package/dist/daemon/watcher/index.js +250 -0
- package/dist/duration.js +24 -0
- package/dist/entities/loops/filters.js +109 -0
- 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/entry.js +16 -0
- package/dist/esm-loader.js +39 -0
- package/dist/features/chain-editor/ChainEditor.js +116 -0
- package/dist/features/chain-editor/mermaidToAscii.js +375 -0
- package/dist/features/chain-editor/renderChainDiagram.js +106 -0
- package/dist/features/code-editor/CodeEditorModal.js +84 -0
- package/dist/features/code-editor/CodeEditorPreview.js +14 -0
- package/dist/features/code-editor/useEditorKeyboard.js +150 -0
- package/dist/features/code-editor/useModalDimensions.js +9 -0
- package/dist/features/commands/commands.js +132 -0
- package/dist/features/commands/useCommandHandlers.js +280 -0
- package/dist/features/commands/useContextualActions.js +78 -0
- package/dist/features/commands/useGlobalShortcuts.js +154 -0
- package/dist/features/forms/FormRouter.js +46 -0
- package/dist/features/overlays/ContextHelpModal.js +8 -0
- package/dist/features/overlays/DiagramModal.js +45 -0
- package/dist/features/overlays/ExportModal.js +51 -0
- package/dist/features/overlays/HelpGuideModal.js +23 -0
- package/dist/features/overlays/HelpModal.js +59 -0
- package/dist/features/overlays/OverlayStack.js +10 -0
- package/dist/features/overlays/ProjectsModal.js +42 -0
- package/dist/features/overlays/TaskPickerModal.js +62 -0
- package/dist/features/overlays/WelcomeScreen.js +74 -0
- package/dist/features/overlays/useOverlayStack.js +55 -0
- package/dist/features/state/useAppState.js +151 -0
- package/dist/logger.js +25 -0
- package/dist/loop-config.js +152 -0
- package/dist/shared/clipboard.js +111 -0
- package/dist/shared/config/constants.js +104 -0
- package/dist/shared/config/paths.js +67 -0
- package/dist/shared/container/index.js +24 -0
- package/dist/shared/fs-utils.js +44 -0
- package/dist/shared/hooks/useBreakpoint.js +11 -0
- package/dist/shared/hooks/useDaemonSettings.js +41 -0
- package/dist/shared/hooks/useInject.js +6 -0
- package/dist/shared/hooks/useLogStream.js +37 -0
- package/dist/shared/hooks/useLoopFormValidation.js +131 -0
- package/dist/shared/hooks/useLoopPolling.js +30 -0
- package/dist/shared/hooks/useMouseScroll.js +56 -0
- package/dist/shared/hooks/useUndoRedo.js +77 -0
- package/dist/shared/i18n/en.json +709 -0
- package/dist/shared/i18n/index.js +11 -0
- 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/settings-service.js +102 -0
- package/dist/shared/services/task-service.js +41 -0
- package/dist/shared/services/types.js +8 -0
- package/dist/shared/sleep.js +17 -0
- package/dist/shared/tail.js +4 -0
- package/dist/shared/ui/Button.js +18 -0
- package/dist/shared/ui/DebugPanel.js +9 -0
- package/dist/shared/ui/FocusableButton.js +18 -0
- package/dist/shared/ui/FocusableInput.js +72 -0
- package/dist/shared/ui/FocusableList.js +48 -0
- package/dist/shared/ui/Modal.js +13 -0
- package/dist/shared/ui/SelectModal.js +78 -0
- package/dist/shared/ui/Toast.js +37 -0
- package/dist/shared/ui/format.js +117 -0
- package/dist/shared/ui/hooks/useHoverState.js +11 -0
- package/dist/shared/ui/state.js +9 -0
- package/dist/shared/ui/theme.js +96 -0
- package/dist/shared/utils/log-lines.js +13 -0
- package/dist/shared/utils/paste.js +37 -0
- package/dist/shared/utils/syntax.js +128 -0
- package/dist/shared/utils/validation.js +56 -0
- package/dist/types.js +1 -0
- package/dist/visual-evidence/capture.js +51 -0
- package/dist/visual-evidence/cli.js +85 -0
- package/dist/visual-evidence/evidence-required.js +71 -0
- package/dist/visual-evidence/index.js +10 -0
- package/dist/visual-evidence/launch.js +61 -0
- package/dist/visual-evidence/manifest.js +32 -0
- package/dist/visual-evidence/openspec-resolver.js +45 -0
- package/dist/visual-evidence/publish.js +126 -0
- package/dist/visual-evidence/run.js +121 -0
- package/dist/visual-evidence/scenario-registry.js +132 -0
- package/dist/widgets/command-input/CommandDropdown.js +69 -0
- package/dist/widgets/command-input/CommandInput.js +133 -0
- package/dist/widgets/command-input/HintBar.js +9 -0
- package/dist/widgets/command-input/InputModes.js +72 -0
- package/dist/widgets/commands-browser/CommandsBrowserModal.js +85 -0
- package/dist/widgets/header/Header.js +54 -0
- package/dist/widgets/header/TabBar.js +20 -0
- package/dist/widgets/left-panel/LeftPanel.js +34 -0
- package/dist/widgets/left-panel/Navigator.js +107 -0
- package/dist/widgets/left-panel/ProjectsPage.js +76 -0
- package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
- package/dist/widgets/left-panel/TaskBrowser.js +132 -0
- package/dist/widgets/log-modal/LogModal.js +211 -0
- package/dist/widgets/loop-form/CreateForm.js +82 -0
- package/dist/widgets/loop-form/TextField.js +9 -0
- package/dist/widgets/loop-form/WizardForm.js +199 -0
- package/dist/widgets/loop-form/useCreateSteps.js +135 -0
- package/dist/widgets/loop-form/useHandleComplete.js +94 -0
- package/dist/widgets/project-form/ProjectForm.js +83 -0
- package/dist/widgets/right-panel/Inspector.js +41 -0
- package/dist/widgets/right-panel/RightPanel.js +38 -0
- package/dist/widgets/right-panel/RunHistory.js +187 -0
- package/dist/widgets/task-form/TaskForm.js +272 -0
- package/package.json +119 -0
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
import { validateContext } from "../../core/context/validate-context.js";
|
|
7
|
+
export function useCreateSteps(params) {
|
|
8
|
+
const { initial, selectedTaskId, resolvedTaskName, commandValue, contextValue, projects, setOpenSelect, setTaskPickerOpen, setCommandEditorOpen, setContextEditorOpen, fieldCallbacksRef, taskModeInitial, } = params;
|
|
9
|
+
return useMemo(() => {
|
|
10
|
+
const list = [
|
|
11
|
+
{
|
|
12
|
+
key: "interval",
|
|
13
|
+
prompt: t("wizard.intervalPrompt"),
|
|
14
|
+
hint: t("wizard.intervalHint"),
|
|
15
|
+
required: true,
|
|
16
|
+
inputType: "text",
|
|
17
|
+
defaultValue: initial.interval ?? undefined,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
key: "taskMode",
|
|
21
|
+
prompt: t("wizard.taskModePrompt"),
|
|
22
|
+
hint: t("board.hintTaskMode"),
|
|
23
|
+
required: true,
|
|
24
|
+
defaultValue: taskModeInitial,
|
|
25
|
+
onActivate: () => setOpenSelect("taskMode"),
|
|
26
|
+
renderCustom: ({ value, isActive, onChange, onAdvance }) => {
|
|
27
|
+
fieldCallbacksRef.current.taskMode = { value, onChange, onAdvance };
|
|
28
|
+
return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.taskModePrompt"), isActive: isActive }));
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: "taskId",
|
|
33
|
+
prompt: resolvedTaskName
|
|
34
|
+
? t("board.selectedTask", { name: resolvedTaskName })
|
|
35
|
+
: t("board.chooseTask"),
|
|
36
|
+
hint: t("board.hintTask"),
|
|
37
|
+
required: true,
|
|
38
|
+
inputType: "text",
|
|
39
|
+
defaultValue: selectedTaskId ?? initial.taskId ?? undefined,
|
|
40
|
+
skip: (values) => !values.taskMode?.includes("Existing"),
|
|
41
|
+
onActivate: () => setTaskPickerOpen(true),
|
|
42
|
+
renderCustom: ({ isActive }) => (_jsx(SelectValueField, { label: resolvedTaskName, placeholder: t("board.chooseTask"), isActive: isActive })),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "command",
|
|
46
|
+
prompt: t("wizard.commandPrompt"),
|
|
47
|
+
hint: t("wizard.commandHint"),
|
|
48
|
+
required: true,
|
|
49
|
+
inputType: "text",
|
|
50
|
+
defaultValue: commandValue || undefined,
|
|
51
|
+
skip: (values) => !!values.taskMode?.includes("Existing"),
|
|
52
|
+
onActivate: () => setCommandEditorOpen(true),
|
|
53
|
+
renderCustom: ({ isActive }) => (_jsx(CodeEditorPreview, { value: commandValue, hint: t("wizard.commandHint"), isActive: isActive, onActivate: () => setCommandEditorOpen(true) })),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: "runNow",
|
|
57
|
+
prompt: t("wizard.runNowPrompt"),
|
|
58
|
+
hint: t("board.hintRunNow"),
|
|
59
|
+
required: true,
|
|
60
|
+
defaultValue: initial.runNow === "true" || initial.runNow === "yes"
|
|
61
|
+
? t("wizard.runNowNow")
|
|
62
|
+
: t("wizard.runNowWait"),
|
|
63
|
+
skip: (values) => {
|
|
64
|
+
const v = (values.interval ?? "").trim().toLowerCase();
|
|
65
|
+
return v === "manual" || v === "0";
|
|
66
|
+
},
|
|
67
|
+
onActivate: () => setOpenSelect("runNow"),
|
|
68
|
+
renderCustom: ({ value, isActive, onChange, onAdvance }) => {
|
|
69
|
+
fieldCallbacksRef.current.runNow = { value, onChange, onAdvance };
|
|
70
|
+
return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.runNowPrompt"), isActive: isActive }));
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: "cwd",
|
|
75
|
+
prompt: t("wizard.cwdPrompt"),
|
|
76
|
+
hint: t("wizard.cwdHint"),
|
|
77
|
+
required: false,
|
|
78
|
+
inputType: "text",
|
|
79
|
+
defaultValue: initial.cwd ?? undefined,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "description",
|
|
83
|
+
prompt: t("wizard.descriptionPrompt"),
|
|
84
|
+
hint: t("wizard.descriptionHint"),
|
|
85
|
+
required: true,
|
|
86
|
+
inputType: "text",
|
|
87
|
+
defaultValue: initial.description ?? undefined,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: "maxRuns",
|
|
91
|
+
prompt: t("wizard.maxRunsPrompt"),
|
|
92
|
+
hint: t("wizard.maxRunsHint"),
|
|
93
|
+
required: false,
|
|
94
|
+
inputType: "text",
|
|
95
|
+
defaultValue: initial.maxRuns ?? undefined,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: "project",
|
|
99
|
+
prompt: t("wizard.projectPrompt"),
|
|
100
|
+
hint: t("wizard.projectHint"),
|
|
101
|
+
required: false,
|
|
102
|
+
defaultValue: projects.find((p) => p.id === (initial.project ?? "default"))?.name ?? projects[0]?.name,
|
|
103
|
+
onActivate: () => setOpenSelect("project"),
|
|
104
|
+
renderCustom: ({ value, isActive, onChange, onAdvance }) => {
|
|
105
|
+
fieldCallbacksRef.current.project = { value, onChange, onAdvance };
|
|
106
|
+
return (_jsx(SelectValueField, { label: value || null, placeholder: t("wizard.projectPrompt"), isActive: isActive }));
|
|
107
|
+
},
|
|
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
|
+
},
|
|
132
|
+
];
|
|
133
|
+
return list;
|
|
134
|
+
}, [taskModeInitial, selectedTaskId, resolvedTaskName, initial, commandValue, contextValue, projects]);
|
|
135
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
import { validateContext } from "../../core/context/validate-context.js";
|
|
6
|
+
export function useHandleComplete(params) {
|
|
7
|
+
const { selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, projects, loopService, } = params;
|
|
8
|
+
return useCallback((values) => {
|
|
9
|
+
const intervalInput = values.interval ?? "";
|
|
10
|
+
if (!intervalInput.trim())
|
|
11
|
+
return;
|
|
12
|
+
let interval;
|
|
13
|
+
try {
|
|
14
|
+
interval = parseDuration(intervalInput.trim());
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const isManual = interval === 0;
|
|
20
|
+
const intervalHuman = isManual ? "manual" : intervalInput.trim();
|
|
21
|
+
const isExistingTask = !!values.taskMode?.includes("Existing");
|
|
22
|
+
if (isExistingTask && !selectedTaskId && !values.taskId?.trim())
|
|
23
|
+
return;
|
|
24
|
+
const cmdValue = values.command ?? commandValue;
|
|
25
|
+
if (!isExistingTask && !cmdValue.trim())
|
|
26
|
+
return;
|
|
27
|
+
const cmd = isExistingTask
|
|
28
|
+
? ""
|
|
29
|
+
: joinCommandLines(cmdValue);
|
|
30
|
+
let cmdOnly = "";
|
|
31
|
+
let args = [];
|
|
32
|
+
if (cmd.trim()) {
|
|
33
|
+
try {
|
|
34
|
+
const tokens = parseCommandLine(cmd.trim());
|
|
35
|
+
cmdOnly = tokens[0] ?? "";
|
|
36
|
+
args = tokens.slice(1);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const runNowValue = values.runNow === t("wizard.runNowNow");
|
|
43
|
+
const projectName = values.project ?? "";
|
|
44
|
+
const project = projects.find((p) => p.name === projectName);
|
|
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();
|
|
61
|
+
const options = {
|
|
62
|
+
interval,
|
|
63
|
+
taskId: isExistingTask
|
|
64
|
+
? (selectedTaskId ?? values.taskId?.trim() ?? null)
|
|
65
|
+
: null,
|
|
66
|
+
command: cmdOnly,
|
|
67
|
+
commandArgs: args,
|
|
68
|
+
commandRaw: isExistingTask ? undefined : cmdValue,
|
|
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,
|
|
73
|
+
maxRuns: (values.maxRuns ?? "").trim()
|
|
74
|
+
? parseInt(values.maxRuns, 10)
|
|
75
|
+
: null,
|
|
76
|
+
verbose: false,
|
|
77
|
+
description: (values.description ?? "").trim(),
|
|
78
|
+
projectId,
|
|
79
|
+
offset: null,
|
|
80
|
+
context,
|
|
81
|
+
};
|
|
82
|
+
const desc = (values.description ?? "").trim() || [cmdOnly, ...args].join(" ").trim();
|
|
83
|
+
if (mode === "edit" && editId) {
|
|
84
|
+
loopService.update(editId, options, intervalHuman)
|
|
85
|
+
.then((id) => onDone(true, id, desc))
|
|
86
|
+
.catch(() => { });
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
loopService.create(options, intervalHuman)
|
|
90
|
+
.then((id) => onDone(false, id, desc))
|
|
91
|
+
.catch(() => { });
|
|
92
|
+
}
|
|
93
|
+
}, [selectedTaskId, mode, editId, currentProjectId, onDone, commandValue, projects]);
|
|
94
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useCallback, useState, useRef } from "react";
|
|
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";
|
|
9
|
+
export function ProjectFormView(props) {
|
|
10
|
+
const { mode, editProject, onCancel, onDone } = props;
|
|
11
|
+
const projectService = useInject(TYPES.ProjectService);
|
|
12
|
+
const colorKeyFor = (color) => {
|
|
13
|
+
const found = PROJECT_COLOR_KEYS.find((k) => PROJECT_COLORS[k] === color);
|
|
14
|
+
return found ?? "cyan";
|
|
15
|
+
};
|
|
16
|
+
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
|
17
|
+
const colorFieldRef = useRef(null);
|
|
18
|
+
const colorOptions = useMemo(() => PROJECT_COLOR_KEYS.map((k) => ({ value: k, label: k })), []);
|
|
19
|
+
const steps = useMemo(() => [
|
|
20
|
+
{
|
|
21
|
+
key: "name",
|
|
22
|
+
prompt: t("project.wizard.namePrompt"),
|
|
23
|
+
hint: t("project.wizard.nameHint"),
|
|
24
|
+
required: true,
|
|
25
|
+
inputType: "text",
|
|
26
|
+
defaultValue: editProject?.name ?? undefined,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "color",
|
|
30
|
+
prompt: t("project.wizard.colorPrompt"),
|
|
31
|
+
hint: t("project.wizard.colorHint"),
|
|
32
|
+
required: true,
|
|
33
|
+
defaultValue: colorKeyFor(editProject?.color),
|
|
34
|
+
onActivate: () => setColorPickerOpen(true),
|
|
35
|
+
renderCustom: ({ value, isActive, onChange, onAdvance }) => {
|
|
36
|
+
colorFieldRef.current = { value, onChange, onAdvance };
|
|
37
|
+
return (_jsx(SelectValueField, { label: value || null, placeholder: t("project.wizard.colorPrompt"), isActive: isActive }));
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "directory",
|
|
42
|
+
prompt: t("project.wizard.directoryPrompt"),
|
|
43
|
+
hint: t("project.wizard.directoryHint"),
|
|
44
|
+
required: false,
|
|
45
|
+
inputType: "text",
|
|
46
|
+
defaultValue: mode === "create" ? process.cwd() : (editProject?.directory || undefined),
|
|
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
|
+
},
|
|
56
|
+
], [editProject, mode]);
|
|
57
|
+
const handleComplete = useCallback((values) => {
|
|
58
|
+
const name = (values.name ?? "").trim();
|
|
59
|
+
if (!name)
|
|
60
|
+
return;
|
|
61
|
+
const colorKey = values.color ?? "cyan";
|
|
62
|
+
const color = PROJECT_COLORS[colorKey] ?? PROJECT_COLORS.cyan;
|
|
63
|
+
const githubSource = values.githubSource?.trim() || undefined;
|
|
64
|
+
if (mode === "edit" && editProject) {
|
|
65
|
+
projectService.update(editProject.id, name, color, values.directory?.trim() || undefined, githubSource)
|
|
66
|
+
.then(() => onDone(true, name))
|
|
67
|
+
.catch(() => { });
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
projectService.create(name, color, values.directory?.trim() || undefined, githubSource)
|
|
71
|
+
.then(() => onDone(false, name))
|
|
72
|
+
.catch(() => { });
|
|
73
|
+
}
|
|
74
|
+
}, [mode, editProject, onDone]);
|
|
75
|
+
const title = mode === "edit"
|
|
76
|
+
? t("project.editTitle")
|
|
77
|
+
: t("project.createTitle");
|
|
78
|
+
return (_jsxs(_Fragment, { children: [_jsx(WizardForm, { title: title, steps: steps, onComplete: handleComplete, onCancel: onCancel, disabled: colorPickerOpen }), colorPickerOpen ? (_jsx(SelectModal, { title: t("project.wizard.colorPrompt"), options: colorOptions, initialValue: colorFieldRef.current?.value, onSelect: (option) => {
|
|
79
|
+
colorFieldRef.current?.onChange(option.value);
|
|
80
|
+
colorFieldRef.current?.onAdvance();
|
|
81
|
+
setColorPickerOpen(false);
|
|
82
|
+
}, onClose: () => setColorPickerOpen(false) })) : null] }));
|
|
83
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { darkTheme as theme, statusColor } from "../../shared/ui/theme.js";
|
|
4
|
+
import { describeLoop, commandLine, timeAgo, timeUntil, truncate } from "../../shared/ui/format.js";
|
|
5
|
+
import { t } from "../../shared/i18n/index.js";
|
|
6
|
+
function labelWidth(bp) {
|
|
7
|
+
return bp === "wide" ? 11 : 8;
|
|
8
|
+
}
|
|
9
|
+
function dividerLen(bp) {
|
|
10
|
+
if (bp === "wide")
|
|
11
|
+
return 40;
|
|
12
|
+
if (bp === "compact")
|
|
13
|
+
return 28;
|
|
14
|
+
return 18;
|
|
15
|
+
}
|
|
16
|
+
function Field(props) {
|
|
17
|
+
return (_jsxs(Box, { overflow: "hidden", children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(props.lw) }), _jsx(Text, { color: theme.text.primary, wrap: "truncate", children: props.children })] }));
|
|
18
|
+
}
|
|
19
|
+
function MutedField(props) {
|
|
20
|
+
return (_jsxs(Box, { overflow: "hidden", children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(props.lw) }), _jsx(Text, { color: theme.text.muted, wrap: "truncate", children: props.children })] }));
|
|
21
|
+
}
|
|
22
|
+
export function Inspector(props) {
|
|
23
|
+
const { loop, breakpoint = "wide" } = props;
|
|
24
|
+
const lw = labelWidth(breakpoint);
|
|
25
|
+
const divLen = dividerLen(breakpoint);
|
|
26
|
+
const DIVIDER = "\u2500".repeat(divLen);
|
|
27
|
+
const truncLen = breakpoint === "wide" ? 38 : breakpoint === "compact" ? 25 : 15;
|
|
28
|
+
if (!loop) {
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", paddingY: 0, children: [_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorTitle") }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.inspectorEmpty") }) })] }));
|
|
30
|
+
}
|
|
31
|
+
const sColor = statusColor(loop.status);
|
|
32
|
+
const maxRunsLabel = loop.maxRuns ? String(loop.maxRuns) : t("board.unlimited");
|
|
33
|
+
const lastRun = loop.lastRunAt ? timeAgo(loop.lastRunAt) : t("format.dash");
|
|
34
|
+
const lastExit = loop.lastExitCode !== null ? String(loop.lastExitCode) : t("format.dash");
|
|
35
|
+
const nextRun = loop.nextRunAt ? t("format.timingNext", { timeAgo: timeUntil(loop.nextRunAt) }) : t("format.dash");
|
|
36
|
+
const fullCmd = truncate(commandLine(loop.command, loop.commandArgs), truncLen);
|
|
37
|
+
const desc = truncate(describeLoop(loop), truncLen);
|
|
38
|
+
// Fields omitted in compact/minimal to save vertical space
|
|
39
|
+
const showOptional = breakpoint === "wide";
|
|
40
|
+
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: [loop.isRecipe ? (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.semantic.warning, children: "Recipe".padEnd(lw) }), _jsx(Text, { color: theme.semantic.warning, children: loop.recipeFile ?? "" })] })) : null, _jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: t("board.fieldStatus").padEnd(lw) }), _jsx(Text, { color: sColor, children: loop.status })] }), _jsx(Field, { label: t("board.fieldRuns"), lw: lw, children: _jsxs(Text, { color: theme.text.primary, children: [loop.runCount, " / ", maxRunsLabel] }) }), _jsx(Field, { label: t("board.fieldInterval"), lw: lw, children: _jsx(Text, { color: theme.text.primary, children: loop.intervalHuman }) }), _jsx(Field, { label: t("board.fieldLastExit"), lw: lw, children: _jsx(Text, { color: theme.text.primary, children: lastExit }) }), showOptional ? (_jsx(Field, { label: t("board.fieldLastRun"), lw: lw, children: _jsx(Text, { color: theme.text.primary, children: lastRun }) })) : null, showOptional ? (_jsx(Field, { label: t("board.fieldNextRun"), lw: lw, children: _jsx(Text, { color: theme.text.primary, children: nextRun }) })) : null, (loop.silentChainCount ?? 0) > 0 ? (_jsx(Field, { label: t("board.fieldSilentChains"), lw: lw, children: _jsx(Text, { color: theme.text.muted, children: t("board.silentChainCount", { count: (loop.silentChainCount ?? 0).toLocaleString() }) }) })) : null, _jsx(MutedField, { label: t("board.fieldDesc"), lw: lw, children: desc }), _jsx(MutedField, { label: t("board.fieldCommand"), lw: lw, children: fullCmd })] }), _jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: DIVIDER }) })] }));
|
|
41
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useStdout } from "ink";
|
|
3
|
+
import { darkTheme as theme, tabAccentColor } from "../../shared/ui/theme.js";
|
|
4
|
+
import { t } from "../../shared/i18n/index.js";
|
|
5
|
+
import { Inspector } from "./Inspector.js";
|
|
6
|
+
import { RunHistory } from "./RunHistory.js";
|
|
7
|
+
import { FocusableButton } from "../../shared/ui/FocusableButton.js";
|
|
8
|
+
import { commandLine } from "../../shared/ui/format.js";
|
|
9
|
+
export function RightPanel(props) {
|
|
10
|
+
const { isFocused, navActive = true, activeTab, breakpoint = "wide", loop, selectedRunIndex, onSelectRun, onOpenRun, selectedTask, allTasks, selectedProject, projectLoopCount, onProjectEdit, onProjectDelete, projects, } = props;
|
|
11
|
+
const borderColor = isFocused ? tabAccentColor(activeTab) : theme.border.default;
|
|
12
|
+
const { stdout } = useStdout();
|
|
13
|
+
const panelHeight = (stdout?.rows ?? 24) - 8;
|
|
14
|
+
const panelWidth = breakpoint === "wide" ? "40%" : "100%";
|
|
15
|
+
return (_jsx(Box, { flexDirection: "column", width: panelWidth, height: breakpoint === "wide" ? panelHeight : undefined, borderStyle: "single", borderColor: borderColor, children: activeTab === "projects" ? (_jsx(ProjectInspector, { project: selectedProject ?? null, loopCount: projectLoopCount ?? 0, onEdit: onProjectEdit, onDelete: onProjectDelete })) : activeTab === "tasks" ? (_jsx(TaskInspector, { task: selectedTask ?? null, allTasks: allTasks ?? [], breakpoint: breakpoint })) : (_jsxs(_Fragment, { children: [_jsx(Inspector, { loop: loop, projects: projects, breakpoint: breakpoint }), _jsx(RunHistory, { loop: loop, selectedRunIndex: selectedRunIndex, onSelectRun: onSelectRun, onOpenRun: onOpenRun, isFocused: isFocused, navActive: navActive, breakpoint: breakpoint })] })) }));
|
|
16
|
+
}
|
|
17
|
+
function TaskInspector(props) {
|
|
18
|
+
const { task, allTasks, breakpoint = "wide" } = props;
|
|
19
|
+
const divLen = breakpoint === "wide" ? 40 : breakpoint === "compact" ? 30 : 20;
|
|
20
|
+
const taskDivider = "\u2500".repeat(divLen);
|
|
21
|
+
if (!task) {
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, padding: 1, children: [_jsx(Text, { color: theme.accent.task, bold: true, children: t("board.taskInspectorTitle") }), _jsx(Text, { color: theme.text.muted, children: taskDivider }), _jsx(Text, { color: theme.text.muted, children: t("board.taskInspectorEmpty") })] }));
|
|
23
|
+
}
|
|
24
|
+
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: taskDivider }) }), _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 }) }), breakpoint !== "minimal" ? (_jsx(Field, { label: t("board.taskFieldCommand"), children: _jsx(Text, { color: theme.text.primary, children: task.commandRaw
|
|
25
|
+
? task.commandRaw.split("\n").filter(Boolean).join(" ")
|
|
26
|
+
: commandLine(task.command, task.commandArgs) }) })) : null, breakpoint === "wide" ? (_jsx(Field, { label: t("board.taskFieldCreated"), children: _jsx(Text, { color: theme.text.primary, children: task.createdAt.slice(0, 10) }) })) : null, _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: taskDivider }) })] }));
|
|
27
|
+
}
|
|
28
|
+
const LABEL_WIDTH = 9;
|
|
29
|
+
function Field(props) {
|
|
30
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.text.muted, children: props.label.padEnd(LABEL_WIDTH) }), props.children] }));
|
|
31
|
+
}
|
|
32
|
+
function ProjectInspector(props) {
|
|
33
|
+
const { project, loopCount, onEdit, onDelete } = props;
|
|
34
|
+
if (!project) {
|
|
35
|
+
return (_jsx(Box, { padding: 1, flexGrow: 1, children: _jsx(Text, { color: theme.text.muted, children: t("project.inspectorEmpty") }) }));
|
|
36
|
+
}
|
|
37
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: project.color, bold: true, children: "\u25CF" }), _jsx(Text, { color: theme.text.primary, bold: true, children: " " + project.name })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldId") }), _jsx(Text, { color: theme.text.secondary, children: project.id })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldLoops") }), _jsx(Text, { color: theme.text.secondary, children: t("project.loopCount", { count: String(loopCount) }) })] }), _jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldCreated") }), _jsx(Text, { color: theme.text.secondary, children: project.createdAt.slice(0, 10) })] }), project.directory && (_jsxs(Box, { children: [_jsx(Text, { color: theme.text.muted, children: t("project.fieldDirectory") }), _jsx(Text, { color: theme.text.secondary, children: project.directory })] })), project.isSystem && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.semantic.warning, children: t("project.systemLabel") }) })), !project.isSystem && (_jsxs(Box, { marginTop: 1, flexDirection: "row", children: [onEdit && (_jsx(FocusableButton, { label: `${t("project.editProjectLabel")} (${t("project.keyEditHint")})`, color: theme.accent.project, onPress: onEdit })), onDelete && (_jsx(FocusableButton, { label: `${t("project.deleteProjectLabel")} (${t("project.keyDeleteHint")})`, color: theme.semantic.danger, variant: "danger", onPress: onDelete }))] }))] }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
+
import { ScrollList } from "ink-scroll-list";
|
|
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
|
+
import { useMouseScroll } from "../../shared/hooks/useMouseScroll.js";
|
|
8
|
+
function runIcon(run) {
|
|
9
|
+
if (run.status === "running")
|
|
10
|
+
return "\u21bb";
|
|
11
|
+
return run.exitCode === 0 ? "\u2713" : "\u2717";
|
|
12
|
+
}
|
|
13
|
+
function runIconColor(run) {
|
|
14
|
+
if (run.status === "running")
|
|
15
|
+
return theme.accent.loop;
|
|
16
|
+
return run.exitCode === 0 ? theme.semantic.success : theme.semantic.danger;
|
|
17
|
+
}
|
|
18
|
+
function chainLabelMax(bp) {
|
|
19
|
+
if (bp === "wide")
|
|
20
|
+
return 28;
|
|
21
|
+
if (bp === "compact")
|
|
22
|
+
return 16;
|
|
23
|
+
return 8;
|
|
24
|
+
}
|
|
25
|
+
function truncateChainLabel(label, maxLen) {
|
|
26
|
+
if (label.length <= maxLen)
|
|
27
|
+
return label;
|
|
28
|
+
const ellipsis = "...";
|
|
29
|
+
const keep = maxLen - ellipsis.length;
|
|
30
|
+
return label.slice(0, keep) + ellipsis;
|
|
31
|
+
}
|
|
32
|
+
export function groupRunsByCycle(runs, bp = "wide") {
|
|
33
|
+
const byRun = new Map();
|
|
34
|
+
for (const r of runs) {
|
|
35
|
+
const group = byRun.get(r.runNumber);
|
|
36
|
+
if (group)
|
|
37
|
+
group.push(r);
|
|
38
|
+
else
|
|
39
|
+
byRun.set(r.runNumber, [r]);
|
|
40
|
+
}
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const group of byRun.values()) {
|
|
43
|
+
if (group.length === 1) {
|
|
44
|
+
result.push(group[0]);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const first = group[0];
|
|
48
|
+
const last = group[group.length - 1];
|
|
49
|
+
const anyRunning = group.some((r) => r.status === "running");
|
|
50
|
+
const totalDuration = group.reduce((sum, r) => sum + r.duration, 0);
|
|
51
|
+
const totalLogSize = group.reduce((sum, r) => sum + r.logSize, 0);
|
|
52
|
+
const chainNames = group
|
|
53
|
+
.map((r) => r.chainName)
|
|
54
|
+
.filter((n) => Boolean(n));
|
|
55
|
+
const chainLabel = chainNames.length > 0
|
|
56
|
+
? truncateChainLabel(`\u2192 ${chainNames.join(" \u2192 ")}`, chainLabelMax(bp))
|
|
57
|
+
: undefined;
|
|
58
|
+
result.push({
|
|
59
|
+
runNumber: first.runNumber,
|
|
60
|
+
startedAt: first.startedAt,
|
|
61
|
+
exitCode: anyRunning ? -1 : last.exitCode,
|
|
62
|
+
duration: totalDuration,
|
|
63
|
+
logSize: totalLogSize,
|
|
64
|
+
status: anyRunning ? "running" : "completed",
|
|
65
|
+
logOffset: first.logOffset,
|
|
66
|
+
chainGroupId: first.chainGroupId,
|
|
67
|
+
chainName: chainLabel,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
const SPARK_CHARS = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
73
|
+
function sparkline(durations) {
|
|
74
|
+
if (durations.length === 0)
|
|
75
|
+
return "";
|
|
76
|
+
const max = Math.max(...durations, 1);
|
|
77
|
+
const min = Math.min(...durations, 0);
|
|
78
|
+
const range = max - min || 1;
|
|
79
|
+
return durations.map((d) => {
|
|
80
|
+
const level = Math.round(((d - min) / range) * (SPARK_CHARS.length - 1));
|
|
81
|
+
return SPARK_CHARS[level] ?? SPARK_CHARS[0];
|
|
82
|
+
}).join("");
|
|
83
|
+
}
|
|
84
|
+
function computeTrends(runs) {
|
|
85
|
+
if (runs.length === 0) {
|
|
86
|
+
return { sparkline: "", avgDuration: 0, successStreak: 0, failureStreak: 0, lastFailureAgo: null };
|
|
87
|
+
}
|
|
88
|
+
const recent = runs.slice(-20);
|
|
89
|
+
const durations = recent.map((r) => r.duration).filter((d) => d > 0);
|
|
90
|
+
const avgDuration = durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0;
|
|
91
|
+
let successStreak = 0;
|
|
92
|
+
let failureStreak = 0;
|
|
93
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
94
|
+
const r = runs[i];
|
|
95
|
+
if (r.exitCode === 0 && failureStreak === 0)
|
|
96
|
+
successStreak++;
|
|
97
|
+
else if (r.exitCode !== 0 && successStreak === 0)
|
|
98
|
+
failureStreak++;
|
|
99
|
+
else
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
let lastFailureAgo = null;
|
|
103
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
104
|
+
if (runs[i].exitCode !== 0) {
|
|
105
|
+
const diff = Date.now() - new Date(runs[i].startedAt).getTime();
|
|
106
|
+
const hours = Math.floor(diff / 3600000);
|
|
107
|
+
const mins = Math.floor(diff / 60000);
|
|
108
|
+
lastFailureAgo = hours > 0 ? `${hours}h ago` : `${mins}m ago`;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
sparkline: sparkline(durations),
|
|
114
|
+
avgDuration: Math.round(avgDuration),
|
|
115
|
+
successStreak,
|
|
116
|
+
failureStreak,
|
|
117
|
+
lastFailureAgo,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function RunHistory(props) {
|
|
121
|
+
const { loop, selectedRunIndex, onSelectRun, onOpenRun, isFocused, navActive = true, breakpoint = "wide" } = props;
|
|
122
|
+
const { stdout } = useStdout();
|
|
123
|
+
const terminalHeight = stdout?.rows ?? 24;
|
|
124
|
+
const LIMIT = Math.max(3, terminalHeight - 22);
|
|
125
|
+
const runs = groupRunsByCycle(loop?.runHistory ?? [], breakpoint);
|
|
126
|
+
const reversed = [...runs].reverse();
|
|
127
|
+
const n = reversed.length;
|
|
128
|
+
const showSize = breakpoint === "wide";
|
|
129
|
+
const timePad = breakpoint === "wide" ? 10 : 8;
|
|
130
|
+
const durPad = breakpoint === "wide" ? 10 : 7;
|
|
131
|
+
useInput((input, key) => {
|
|
132
|
+
if (n === 0)
|
|
133
|
+
return;
|
|
134
|
+
// Swallow SGR mouse sequences so they don't interfere
|
|
135
|
+
if (input.includes("[<")) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (key.upArrow || input === "k") {
|
|
139
|
+
const next = selectedRunIndex <= 0 ? n - 1 : selectedRunIndex - 1;
|
|
140
|
+
onSelectRun(next);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (key.downArrow || input === "j") {
|
|
144
|
+
const next = selectedRunIndex >= n - 1 ? 0 : selectedRunIndex + 1;
|
|
145
|
+
onSelectRun(next);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (key.ctrl && key.return) {
|
|
149
|
+
const run = reversed[selectedRunIndex];
|
|
150
|
+
if (run)
|
|
151
|
+
onOpenRun(run);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}, { isActive: isFocused && navActive });
|
|
155
|
+
useMouseScroll({
|
|
156
|
+
onScrollUp: () => {
|
|
157
|
+
if (n === 0 || !(isFocused && navActive))
|
|
158
|
+
return;
|
|
159
|
+
const next = selectedRunIndex <= 0 ? n - 1 : selectedRunIndex - 1;
|
|
160
|
+
onSelectRun(next);
|
|
161
|
+
},
|
|
162
|
+
onScrollDown: () => {
|
|
163
|
+
if (n === 0 || !(isFocused && navActive))
|
|
164
|
+
return;
|
|
165
|
+
const next = selectedRunIndex >= n - 1 ? 0 : selectedRunIndex + 1;
|
|
166
|
+
onSelectRun(next);
|
|
167
|
+
},
|
|
168
|
+
isActive: isFocused && navActive,
|
|
169
|
+
});
|
|
170
|
+
const title = isFocused ? t("board.runHistoryTitleHint") : t("board.runHistoryTitle");
|
|
171
|
+
const trends = computeTrends(runs);
|
|
172
|
+
function renderRun(run, isSelected) {
|
|
173
|
+
const fg = isSelected ? theme.text.inverse : theme.text.primary;
|
|
174
|
+
const time = formatRunTime(run.startedAt);
|
|
175
|
+
const duration = formatRunDuration(run.duration);
|
|
176
|
+
const size = formatFileSize(run.logSize);
|
|
177
|
+
const icon = runIcon(run);
|
|
178
|
+
const iconColor = runIconColor(run);
|
|
179
|
+
const chain = run.chainName ?? "";
|
|
180
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: fg, children: time.padEnd(timePad) }), _jsxs(Text, { color: isSelected ? theme.text.inverse : iconColor, children: [icon, " "] }), _jsx(Text, { color: fg, children: duration.padEnd(durPad) }), showSize ? _jsx(Text, { color: fg, children: size.padEnd(8) }) : null, _jsx(Text, { color: fg, children: chain })] }));
|
|
181
|
+
}
|
|
182
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { paddingLeft: 1, paddingRight: 1, justifyContent: "space-between", children: [_jsx(Text, { color: theme.text.muted, children: title }), (trends.failureStreak > 0 || trends.lastFailureAgo) ? (_jsxs(Text, { color: theme.semantic.warning, children: [trends.failureStreak > 0 ? t("board.runHistoryFailStreak", { count: String(trends.failureStreak) }) : "", trends.failureStreak > 0 && trends.lastFailureAgo ? " " : "", trends.lastFailureAgo ? t("board.runHistoryLastFail", { ago: trends.lastFailureAgo }) : ""] })) : null] }), reversed.length === 0 ? (_jsx(Box, { paddingLeft: 1, children: _jsx(Text, { color: theme.text.muted, children: t("board.runHistoryEmpty") }) })) : (_jsxs(Box, { flexDirection: "column", children: [trends.sparkline ? (_jsx(Box, { paddingLeft: 1, marginBottom: 0, children: breakpoint !== "minimal" ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.text.muted, children: "Durations: " }), _jsx(Text, { color: theme.accent.loop, children: trends.sparkline }), _jsxs(Text, { color: theme.text.muted, children: [" avg:", formatRunDuration(trends.avgDuration), " "] }), trends.successStreak > 0 ? (_jsxs(Text, { color: theme.semantic.success, children: ["streak:", trends.successStreak, " ok"] })) : null] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.accent.loop, children: trends.sparkline }), _jsxs(Text, { color: theme.text.muted, children: [" avg:", formatRunDuration(trends.avgDuration)] })] })) })) : null, _jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: theme.text.muted, children: t("board.runHistoryTime").padEnd(timePad) }), _jsx(Text, { color: theme.text.muted, children: " " }), _jsx(Text, { color: theme.text.muted, children: t("board.runHistoryDuration").padEnd(durPad) }), showSize ? _jsx(Text, { color: theme.text.muted, children: t("board.runHistorySize").padEnd(8) }) : null, _jsx(Text, { color: theme.text.muted, children: t("board.runHistoryDesc") })] }), _jsx(ScrollList, { selectedIndex: selectedRunIndex, height: LIMIT, children: reversed.map((run, i) => {
|
|
183
|
+
const isSelected = i === selectedRunIndex;
|
|
184
|
+
const indicator = isSelected ? "\u203a " : " ";
|
|
185
|
+
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 }), renderRun(run, isSelected)] }, i));
|
|
186
|
+
}) })] }))] }));
|
|
187
|
+
}
|