@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,69 @@
|
|
|
1
|
+
export const defaultProjectFilters = {
|
|
2
|
+
query: "",
|
|
3
|
+
hasLoops: "all",
|
|
4
|
+
isSystem: "all",
|
|
5
|
+
sort: "name",
|
|
6
|
+
};
|
|
7
|
+
export function applyProjectFilters(projects, loops, filters) {
|
|
8
|
+
const filtered = projects.filter((project) => {
|
|
9
|
+
if (filters.hasLoops === "with-loops") {
|
|
10
|
+
const count = loops.filter((l) => (l.projectId ?? "default") === project.id).length;
|
|
11
|
+
if (count === 0)
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (filters.hasLoops === "empty") {
|
|
15
|
+
const count = loops.filter((l) => (l.projectId ?? "default") === project.id).length;
|
|
16
|
+
if (count > 0)
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (filters.isSystem === "system" && !project.isSystem)
|
|
20
|
+
return false;
|
|
21
|
+
if (filters.isSystem === "user" && project.isSystem)
|
|
22
|
+
return false;
|
|
23
|
+
const query = filters.query.trim().toLowerCase();
|
|
24
|
+
if (query && !project.name.toLowerCase().includes(query))
|
|
25
|
+
return false;
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
return filtered.sort((a, b) => {
|
|
29
|
+
if (filters.sort === "name") {
|
|
30
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
31
|
+
}
|
|
32
|
+
if (filters.sort === "loop-count") {
|
|
33
|
+
const ca = loops.filter((l) => (l.projectId ?? "default") === a.id).length;
|
|
34
|
+
const cb = loops.filter((l) => (l.projectId ?? "default") === b.id).length;
|
|
35
|
+
if (cb !== ca)
|
|
36
|
+
return cb - ca;
|
|
37
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
38
|
+
}
|
|
39
|
+
const da = a.createdAt;
|
|
40
|
+
const db = b.createdAt;
|
|
41
|
+
if (da !== db)
|
|
42
|
+
return db.localeCompare(da);
|
|
43
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const PROJECT_SORT_CYCLE = {
|
|
47
|
+
name: "loop-count",
|
|
48
|
+
"loop-count": "created-date",
|
|
49
|
+
"created-date": "name",
|
|
50
|
+
};
|
|
51
|
+
export function cycleProjectSortMode(mode) {
|
|
52
|
+
return PROJECT_SORT_CYCLE[mode];
|
|
53
|
+
}
|
|
54
|
+
const PROJECT_HAS_LOOPS_CYCLE = {
|
|
55
|
+
all: "with-loops",
|
|
56
|
+
"with-loops": "empty",
|
|
57
|
+
empty: "all",
|
|
58
|
+
};
|
|
59
|
+
export function cycleProjectHasLoopsFilter(filter) {
|
|
60
|
+
return PROJECT_HAS_LOOPS_CYCLE[filter];
|
|
61
|
+
}
|
|
62
|
+
const PROJECT_IS_SYSTEM_CYCLE = {
|
|
63
|
+
all: "system",
|
|
64
|
+
system: "user",
|
|
65
|
+
user: "all",
|
|
66
|
+
};
|
|
67
|
+
export function cycleProjectIsSystemFilter(filter) {
|
|
68
|
+
return PROJECT_IS_SYSTEM_CYCLE[filter];
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defaultProjectFilters, applyProjectFilters, cycleProjectSortMode, cycleProjectHasLoopsFilter, cycleProjectIsSystemFilter, } from "./filters.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const defaultTaskFilters = {
|
|
2
|
+
query: "",
|
|
3
|
+
sort: "name",
|
|
4
|
+
};
|
|
5
|
+
function taskSearchableText(task) {
|
|
6
|
+
return `${task.id} ${task.name} ${task.command} ${task.commandArgs.join(" ")}`.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
function matches(task, filters) {
|
|
9
|
+
const query = filters.query.trim().toLowerCase();
|
|
10
|
+
if (!query)
|
|
11
|
+
return true;
|
|
12
|
+
return taskSearchableText(task).includes(query);
|
|
13
|
+
}
|
|
14
|
+
function compare(left, right, sort) {
|
|
15
|
+
if (sort === "name") {
|
|
16
|
+
return left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
|
|
17
|
+
}
|
|
18
|
+
if (sort === "command") {
|
|
19
|
+
const leftCmd = left.commandRaw ?? [left.command, ...left.commandArgs].join(" ");
|
|
20
|
+
const rightCmd = right.commandRaw ?? [right.command, ...right.commandArgs].join(" ");
|
|
21
|
+
const byCmd = leftCmd.localeCompare(rightCmd, undefined, { sensitivity: "base" });
|
|
22
|
+
if (byCmd !== 0)
|
|
23
|
+
return byCmd;
|
|
24
|
+
return left.name.localeCompare(right.name, undefined, { sensitivity: "base" });
|
|
25
|
+
}
|
|
26
|
+
// "created" newest first
|
|
27
|
+
return right.createdAt.localeCompare(left.createdAt);
|
|
28
|
+
}
|
|
29
|
+
export function applyTaskFilters(tasks, filters) {
|
|
30
|
+
return tasks
|
|
31
|
+
.filter((task) => matches(task, filters))
|
|
32
|
+
.sort((left, right) => compare(left, right, filters.sort));
|
|
33
|
+
}
|
|
34
|
+
const TASK_SORT_CYCLE = {
|
|
35
|
+
name: "command",
|
|
36
|
+
command: "created",
|
|
37
|
+
created: "name",
|
|
38
|
+
};
|
|
39
|
+
export function cycleTaskSortMode(mode) {
|
|
40
|
+
return TASK_SORT_CYCLE[mode];
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { defaultTaskFilters, applyTaskFilters, cycleTaskSortMode, } from "./filters.js";
|
package/dist/entry.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
if (process.env.NODE_ENV === undefined) {
|
|
4
|
+
process.env.NODE_ENV = "production";
|
|
5
|
+
process.env.LOOP_TASK_DEFAULTED_NODE_ENV = "1";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
import { register } from "node:module";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
register(pathToFileURL(join(__dirname, "esm-loader.js")).href);
|
|
15
|
+
|
|
16
|
+
import("./cli.js");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolve as pathResolve, dirname } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
|
|
5
|
+
const EXTENSIONLESS_PATTERNS = [
|
|
6
|
+
/node_modules\/react-reconciler\/constants$/,
|
|
7
|
+
/node_modules\/react-reconciler\/reflection$/,
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export function resolve(specifier, context, nextResolve) {
|
|
11
|
+
for (const pattern of EXTENSIONLESS_PATTERNS) {
|
|
12
|
+
if (pattern.test(specifier)) {
|
|
13
|
+
return nextResolve(specifier + ".js", context);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
(specifier.includes("react-reconciler/constants") || specifier.includes("react-reconciler/reflection")) &&
|
|
19
|
+
!specifier.endsWith(".js")
|
|
20
|
+
) {
|
|
21
|
+
return nextResolve(specifier + ".js", context);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
context.parentURL &&
|
|
26
|
+
specifier.startsWith(".") &&
|
|
27
|
+
!specifier.endsWith(".js") &&
|
|
28
|
+
!specifier.endsWith(".json") &&
|
|
29
|
+
!specifier.endsWith(".node")
|
|
30
|
+
) {
|
|
31
|
+
const parentDir = dirname(fileURLToPath(context.parentURL));
|
|
32
|
+
const candidate = pathResolve(parentDir, specifier + ".js");
|
|
33
|
+
if (fs.existsSync(candidate)) {
|
|
34
|
+
return nextResolve(specifier + ".js", context);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return nextResolve(specifier, context);
|
|
39
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useEffect } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import { darkTheme as theme } from "../../shared/ui/theme.js";
|
|
5
|
+
import { useInject } from "../../shared/hooks/useInject.js";
|
|
6
|
+
import { TYPES } from "../../shared/services/types.js";
|
|
7
|
+
import { commandLine } from "../../shared/ui/format.js";
|
|
8
|
+
function buildChains(tasks) {
|
|
9
|
+
const taskMap = new Map();
|
|
10
|
+
for (const task of tasks) {
|
|
11
|
+
taskMap.set(task.id, task);
|
|
12
|
+
}
|
|
13
|
+
const referenced = new Set();
|
|
14
|
+
for (const task of tasks) {
|
|
15
|
+
if (task.onSuccessTaskId)
|
|
16
|
+
referenced.add(task.onSuccessTaskId);
|
|
17
|
+
if (task.onFailureTaskId)
|
|
18
|
+
referenced.add(task.onFailureTaskId);
|
|
19
|
+
}
|
|
20
|
+
const roots = tasks.filter((task) => !referenced.has(task.id));
|
|
21
|
+
const visited = new Set();
|
|
22
|
+
function buildNode(task, branch, depth, isLast) {
|
|
23
|
+
if (visited.has(task.id)) {
|
|
24
|
+
return { task, children: [], branch, depth, isLast };
|
|
25
|
+
}
|
|
26
|
+
visited.add(task.id);
|
|
27
|
+
const children = [];
|
|
28
|
+
if (task.onSuccessTaskId) {
|
|
29
|
+
const child = taskMap.get(task.onSuccessTaskId);
|
|
30
|
+
if (child)
|
|
31
|
+
children.push(buildNode(child, "success", depth + 1, false));
|
|
32
|
+
}
|
|
33
|
+
if (task.onFailureTaskId) {
|
|
34
|
+
const child = taskMap.get(task.onFailureTaskId);
|
|
35
|
+
if (child) {
|
|
36
|
+
if (children.length > 0)
|
|
37
|
+
children[0].isLast = true;
|
|
38
|
+
children.push(buildNode(child, "failure", depth + 1, true));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (children.length > 0)
|
|
42
|
+
children[children.length - 1].isLast = true;
|
|
43
|
+
if (children.length === 0 && branch !== null)
|
|
44
|
+
isLast = true;
|
|
45
|
+
return { task, children, branch, depth, isLast };
|
|
46
|
+
}
|
|
47
|
+
return roots.map((root) => ({
|
|
48
|
+
root: buildNode(root, null, 0, true),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
function flattenTree(node, result = [], prefix = "") {
|
|
52
|
+
result.push({ node, prefix });
|
|
53
|
+
for (const child of node.children) {
|
|
54
|
+
const childPrefix = prefix + (node.isLast ? " " : "\u2502 ");
|
|
55
|
+
flattenTree(child, result, childPrefix);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
export function ChainEditor(props) {
|
|
60
|
+
const [tasks, setTasks] = useState([]);
|
|
61
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
62
|
+
const taskService = useInject(TYPES.TaskService);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
taskService.list().then(setTasks).catch(() => setTasks([]));
|
|
65
|
+
}, [taskService]);
|
|
66
|
+
const trees = useMemo(() => buildChains(tasks), [tasks]);
|
|
67
|
+
const flatNodes = useMemo(() => {
|
|
68
|
+
const all = [];
|
|
69
|
+
for (const tree of trees) {
|
|
70
|
+
flattenTree(tree.root, all);
|
|
71
|
+
}
|
|
72
|
+
return all;
|
|
73
|
+
}, [trees]);
|
|
74
|
+
useInput((_input, key) => {
|
|
75
|
+
if (_input.includes("[<"))
|
|
76
|
+
return;
|
|
77
|
+
if (key.escape) {
|
|
78
|
+
props.onClose();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (key.upArrow) {
|
|
82
|
+
setSelectedIndex((i) => i <= 0 ? flatNodes.length - 1 : i - 1);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (key.downArrow) {
|
|
86
|
+
setSelectedIndex((i) => i >= flatNodes.length - 1 ? 0 : i + 1);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.return) {
|
|
90
|
+
const node = flatNodes[selectedIndex];
|
|
91
|
+
if (node && props.onEditTask) {
|
|
92
|
+
props.onEditTask(node.node.task);
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
if (flatNodes.length === 0) {
|
|
98
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.accent.task, flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Text, { color: theme.accent.task, bold: true, children: "Chain Editor" }), _jsx(Text, { color: theme.text.muted, children: "No tasks found. Create tasks first to build chains." }), _jsx(Text, { color: theme.text.muted, children: "Press Esc to go back." })] }));
|
|
99
|
+
}
|
|
100
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: theme.accent.task, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.accent.task, bold: true, children: "Chain Editor" }), _jsx(Text, { color: theme.text.muted, children: " - up/down: navigate, enter: edit task, esc: back" })] }), flatNodes.map((item, i) => {
|
|
101
|
+
const isSelected = i === selectedIndex;
|
|
102
|
+
const bg = isSelected ? theme.bg.active : undefined;
|
|
103
|
+
const fg = isSelected ? theme.text.inverse : theme.text.primary;
|
|
104
|
+
const branchLabel = item.node.branch
|
|
105
|
+
? item.node.branch === "success"
|
|
106
|
+
? "[\u2713] "
|
|
107
|
+
: "[\u2717] "
|
|
108
|
+
: "";
|
|
109
|
+
const branchColor = item.node.branch === "success"
|
|
110
|
+
? theme.semantic.success
|
|
111
|
+
: item.node.branch === "failure"
|
|
112
|
+
? theme.semantic.danger
|
|
113
|
+
: theme.text.primary;
|
|
114
|
+
return (_jsxs(Box, { backgroundColor: bg, children: [_jsx(Text, { color: theme.text.muted, children: item.prefix }), _jsx(Text, { color: isSelected ? theme.text.inverse : branchColor, children: branchLabel }), _jsx(Text, { color: fg, children: item.node.task.name }), _jsxs(Text, { color: isSelected ? theme.text.inverse : theme.text.muted, children: [" - ", commandLine(item.node.task.command, item.node.task.commandArgs).slice(0, 40)] })] }, `${item.node.task.id}-${i}`));
|
|
115
|
+
}), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.text.muted, children: [trees.length, " root(s) | ", tasks.length, " total tasks | ", flatNodes.length, " nodes"] }) })] }));
|
|
116
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid-to-ASCII renderer for loop-task recipe diagrams.
|
|
3
|
+
*
|
|
4
|
+
* Supports the exact Mermaid subset produced by the loop-task-diagram skill v3:
|
|
5
|
+
* flowchart TD
|
|
6
|
+
* prefixStart("Start<br/>Purpose")
|
|
7
|
+
* taskId["Name<br/>Purpose"] -->|✓| target
|
|
8
|
+
* taskId("Name<br/>Purpose") -.->|✗| target
|
|
9
|
+
* terminalId(("Name<br/>Purpose"))
|
|
10
|
+
* classDef start fill:#ffffff,stroke:#172033,...
|
|
11
|
+
* classDef decision fill:#fff8e8,stroke:#c75b00,...
|
|
12
|
+
* classDef action fill:#eef0ff,stroke:#554cff,...
|
|
13
|
+
* classDef idle fill:#202c40,stroke:#738198,...
|
|
14
|
+
* classDef failure fill:#fff0f0,stroke:#ef2929,...
|
|
15
|
+
* classDef success fill:#e8f8ec,stroke:#18883c,...
|
|
16
|
+
* class nodeId className
|
|
17
|
+
*
|
|
18
|
+
* Rendering rules:
|
|
19
|
+
* - Vertical top-down layout (TD = top-down)
|
|
20
|
+
* - Solid arrows for success (✓), dashed for failure (✗)
|
|
21
|
+
* - Decision nodes []: square box
|
|
22
|
+
* - Action nodes (): rounded box
|
|
23
|
+
* - Terminal nodes (()): double-bordered box, color by class
|
|
24
|
+
* - Width capped at 80 characters
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Parse a Mermaid flowchart string into nodes and edges.
|
|
28
|
+
*/
|
|
29
|
+
function parseMermaid(mermaid) {
|
|
30
|
+
const nodes = new Map();
|
|
31
|
+
const classMap = new Map();
|
|
32
|
+
let entryId = null;
|
|
33
|
+
let firstNode = true;
|
|
34
|
+
const lines = mermaid.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
35
|
+
// First pass: collect class assignments
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
if (line.startsWith("class ")) {
|
|
38
|
+
const rest = line.slice(6).trim();
|
|
39
|
+
const parts = rest.split(/\s+/);
|
|
40
|
+
if (parts.length === 2) {
|
|
41
|
+
const role = parts[1];
|
|
42
|
+
if (["start", "decision", "action", "idle", "failure", "success"].includes(role)) {
|
|
43
|
+
for (const id of parts[0].split(",")) {
|
|
44
|
+
classMap.set(id.trim(), role);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Second pass: parse nodes and edges
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
// Skip directives
|
|
54
|
+
if (line.startsWith("flowchart") || line.startsWith("classDef") || line.startsWith("class "))
|
|
55
|
+
continue;
|
|
56
|
+
// Edge lines with box source: id["label"] -->|✓| targetId
|
|
57
|
+
const solidBoxEdge = line.match(/^(\w+)\["([^"]*?)"\]\s*-->\|([^|]+)\|\s*(\w+)$/);
|
|
58
|
+
const solidRoundEdge = line.match(/^(\w+)\("([^"]*?)"\)\s*-->\|([^|]+)\|\s*(\w+)$/);
|
|
59
|
+
const solidDoubleEdge = line.match(/^(\w+)\(\("([^"]*?)"\)\)\s*-->\|([^|]+)\|\s*(\w+)$/);
|
|
60
|
+
const dashedBoxEdge = line.match(/^(\w+)\["([^"]*?)"\]\s*-\.\->\|([^|]+)\|\s*(\w+)$/);
|
|
61
|
+
const dashedRoundEdge = line.match(/^(\w+)\("([^"]*?)"\)\s*-\.\->\|([^|]+)\|\s*(\w+)$/);
|
|
62
|
+
const dashedDoubleEdge = line.match(/^(\w+)\(\("([^"]*?)"\)\)\s*-\.\->\|([^|]+)\|\s*(\w+)$/);
|
|
63
|
+
// Simple edge (just IDs)
|
|
64
|
+
const solidSimple = line.match(/^(\w+)\s*-->\|?([^|]*)\|?\s*(\w+)$/);
|
|
65
|
+
const dashedSimple = line.match(/^(\w+)\s*-\.\->\|?([^|]*)\|?\s*(\w+)$/);
|
|
66
|
+
// Also handle bare --> without labels
|
|
67
|
+
const bareSolid = line.match(/^(\w+)\("([^"]*?)"\)\s*-->\s*(\w+)$/);
|
|
68
|
+
const bareSolidBox = line.match(/^(\w+)\["([^"]*?)"\]\s*-->\s*(\w+)$/);
|
|
69
|
+
// Standalone node declarations
|
|
70
|
+
const standaloneBox = line.match(/^(\w+)\["([^"]*?)"\]$/);
|
|
71
|
+
const standaloneRound = line.match(/^(\w+)\("([^"]*?)"\)$/);
|
|
72
|
+
const standaloneDouble = line.match(/^(\w+)\(\("([^"]*?)"\)\)$/);
|
|
73
|
+
if (solidBoxEdge) {
|
|
74
|
+
const [, fromId, labelText, , toId] = solidBoxEdge;
|
|
75
|
+
const { name, purpose } = splitLabel(labelText);
|
|
76
|
+
ensureNode(nodes, fromId, name, purpose, "box", classMap);
|
|
77
|
+
if (firstNode && !entryId) {
|
|
78
|
+
entryId = fromId;
|
|
79
|
+
firstNode = false;
|
|
80
|
+
}
|
|
81
|
+
nodes.get(fromId).successTarget = toId;
|
|
82
|
+
ensureNodeById(nodes, toId, classMap);
|
|
83
|
+
}
|
|
84
|
+
else if (solidRoundEdge) {
|
|
85
|
+
const [, fromId, labelText, , toId] = solidRoundEdge;
|
|
86
|
+
const { name, purpose } = splitLabel(labelText);
|
|
87
|
+
ensureNode(nodes, fromId, name, purpose, "round", classMap);
|
|
88
|
+
if (firstNode && !entryId) {
|
|
89
|
+
entryId = fromId;
|
|
90
|
+
firstNode = false;
|
|
91
|
+
}
|
|
92
|
+
nodes.get(fromId).successTarget = toId;
|
|
93
|
+
ensureNodeById(nodes, toId, classMap);
|
|
94
|
+
}
|
|
95
|
+
else if (dashedBoxEdge) {
|
|
96
|
+
const [, fromId, labelText, , toId] = dashedBoxEdge;
|
|
97
|
+
const { name, purpose } = splitLabel(labelText);
|
|
98
|
+
ensureNode(nodes, fromId, name, purpose, "box", classMap);
|
|
99
|
+
if (firstNode && !entryId) {
|
|
100
|
+
entryId = fromId;
|
|
101
|
+
firstNode = false;
|
|
102
|
+
}
|
|
103
|
+
nodes.get(fromId).failureTarget = toId;
|
|
104
|
+
ensureNodeById(nodes, toId, classMap);
|
|
105
|
+
}
|
|
106
|
+
else if (dashedRoundEdge) {
|
|
107
|
+
const [, fromId, labelText, , toId] = dashedRoundEdge;
|
|
108
|
+
const { name, purpose } = splitLabel(labelText);
|
|
109
|
+
ensureNode(nodes, fromId, name, purpose, "round", classMap);
|
|
110
|
+
if (firstNode && !entryId) {
|
|
111
|
+
entryId = fromId;
|
|
112
|
+
firstNode = false;
|
|
113
|
+
}
|
|
114
|
+
nodes.get(fromId).failureTarget = toId;
|
|
115
|
+
ensureNodeById(nodes, toId, classMap);
|
|
116
|
+
}
|
|
117
|
+
else if (bareSolid) {
|
|
118
|
+
const [, fromId, labelText, toId] = bareSolid;
|
|
119
|
+
const { name, purpose } = splitLabel(labelText);
|
|
120
|
+
ensureNode(nodes, fromId, name, purpose, "round", classMap);
|
|
121
|
+
if (firstNode && !entryId) {
|
|
122
|
+
entryId = fromId;
|
|
123
|
+
firstNode = false;
|
|
124
|
+
}
|
|
125
|
+
nodes.get(fromId).successTarget = toId;
|
|
126
|
+
ensureNodeById(nodes, toId, classMap);
|
|
127
|
+
}
|
|
128
|
+
else if (bareSolidBox) {
|
|
129
|
+
const [, fromId, labelText, toId] = bareSolidBox;
|
|
130
|
+
const { name, purpose } = splitLabel(labelText);
|
|
131
|
+
ensureNode(nodes, fromId, name, purpose, "box", classMap);
|
|
132
|
+
if (firstNode && !entryId) {
|
|
133
|
+
entryId = fromId;
|
|
134
|
+
firstNode = false;
|
|
135
|
+
}
|
|
136
|
+
nodes.get(fromId).successTarget = toId;
|
|
137
|
+
ensureNodeById(nodes, toId, classMap);
|
|
138
|
+
}
|
|
139
|
+
else if (standaloneDouble) {
|
|
140
|
+
const [, id, labelText] = standaloneDouble;
|
|
141
|
+
const { name, purpose } = splitLabel(labelText);
|
|
142
|
+
ensureNode(nodes, id, name, purpose, "doubleRound", classMap);
|
|
143
|
+
}
|
|
144
|
+
else if (standaloneRound) {
|
|
145
|
+
const [, id, labelText] = standaloneRound;
|
|
146
|
+
const { name, purpose } = splitLabel(labelText);
|
|
147
|
+
ensureNode(nodes, id, name, purpose, "round", classMap);
|
|
148
|
+
if (firstNode && !entryId) {
|
|
149
|
+
entryId = id;
|
|
150
|
+
firstNode = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else if (standaloneBox) {
|
|
154
|
+
const [, id, labelText] = standaloneBox;
|
|
155
|
+
const { name, purpose } = splitLabel(labelText);
|
|
156
|
+
ensureNode(nodes, id, name, purpose, "box", classMap);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { nodes, entryId };
|
|
160
|
+
}
|
|
161
|
+
function splitLabel(raw) {
|
|
162
|
+
const label = raw.replace(/<br\/>/g, "\n");
|
|
163
|
+
const idx = label.indexOf("\n");
|
|
164
|
+
if (idx === -1)
|
|
165
|
+
return { name: label, purpose: "" };
|
|
166
|
+
return { name: label.slice(0, idx), purpose: label.slice(idx + 1) };
|
|
167
|
+
}
|
|
168
|
+
function ensureNode(nodes, id, name, purpose, shape, classMap) {
|
|
169
|
+
if (!nodes.has(id)) {
|
|
170
|
+
const role = classMap.get(id) || "none";
|
|
171
|
+
nodes.set(id, {
|
|
172
|
+
id,
|
|
173
|
+
label: name,
|
|
174
|
+
purpose,
|
|
175
|
+
shape,
|
|
176
|
+
role,
|
|
177
|
+
successTarget: null,
|
|
178
|
+
failureTarget: null,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function ensureNodeById(nodes, targetId, classMap) {
|
|
183
|
+
if (!nodes.has(targetId)) {
|
|
184
|
+
ensureNode(nodes, targetId, targetId, "", "box", classMap);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Render a Mermaid flowchart as vertical ASCII art.
|
|
189
|
+
*/
|
|
190
|
+
export function renderMermaidAsAscii(mermaid) {
|
|
191
|
+
const { nodes, entryId } = parseMermaid(mermaid);
|
|
192
|
+
const lines = [];
|
|
193
|
+
const visited = new Set();
|
|
194
|
+
function renderNode(id, indent) {
|
|
195
|
+
if (visited.has(id)) {
|
|
196
|
+
lines.push(" ".repeat(indent) + `↻ back to ${id}`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
visited.add(id);
|
|
200
|
+
const node = nodes.get(id);
|
|
201
|
+
if (!node)
|
|
202
|
+
return;
|
|
203
|
+
const prefix = " ".repeat(indent);
|
|
204
|
+
const maxContentWidth = 76 - indent;
|
|
205
|
+
// Build content
|
|
206
|
+
const content = [];
|
|
207
|
+
content.push(node.label);
|
|
208
|
+
if (node.purpose)
|
|
209
|
+
content.push(" " + node.purpose);
|
|
210
|
+
const truncated = content.map((l) => l.length > maxContentWidth ? l.slice(0, maxContentWidth - 3) + "..." : l);
|
|
211
|
+
const boxWidth = Math.min(80, Math.max(...truncated.map((l) => l.length)) + 4);
|
|
212
|
+
if (node.role === "failure") {
|
|
213
|
+
// Red terminal — bold box
|
|
214
|
+
lines.push(prefix + "┏" + "━".repeat(boxWidth - 2) + "┓");
|
|
215
|
+
for (const line of truncated) {
|
|
216
|
+
const padded = line + " ".repeat(Math.max(0, boxWidth - 4 - line.length));
|
|
217
|
+
lines.push(prefix + "┃ " + padded + " ┃");
|
|
218
|
+
}
|
|
219
|
+
lines.push(prefix + "┗" + "━".repeat(boxWidth - 2) + "┛");
|
|
220
|
+
}
|
|
221
|
+
else if (node.role === "success") {
|
|
222
|
+
// Green terminal — double border
|
|
223
|
+
lines.push(prefix + "╔" + "═".repeat(boxWidth - 2) + "╗");
|
|
224
|
+
for (const line of truncated) {
|
|
225
|
+
const padded = line + " ".repeat(Math.max(0, boxWidth - 4 - line.length));
|
|
226
|
+
lines.push(prefix + "║ " + padded + " ║");
|
|
227
|
+
}
|
|
228
|
+
lines.push(prefix + "╚" + "═".repeat(boxWidth - 2) + "╝");
|
|
229
|
+
}
|
|
230
|
+
else if (node.role === "idle") {
|
|
231
|
+
// Idle terminal — dotted border
|
|
232
|
+
lines.push(prefix + "┌" + "┄".repeat(boxWidth - 2) + "┐");
|
|
233
|
+
for (const line of truncated) {
|
|
234
|
+
const padded = line + " ".repeat(Math.max(0, boxWidth - 4 - line.length));
|
|
235
|
+
lines.push(prefix + "┊ " + padded + " ┊");
|
|
236
|
+
}
|
|
237
|
+
lines.push(prefix + "└" + "┄".repeat(boxWidth - 2) + "┘");
|
|
238
|
+
}
|
|
239
|
+
else if (node.shape === "round" || node.role === "start" || node.role === "action") {
|
|
240
|
+
// Rounded / start / action — round corners
|
|
241
|
+
lines.push(prefix + "╭" + "─".repeat(boxWidth - 2) + "╮");
|
|
242
|
+
for (const line of truncated) {
|
|
243
|
+
const padded = line + " ".repeat(Math.max(0, boxWidth - 4 - line.length));
|
|
244
|
+
lines.push(prefix + "│ " + padded + " │");
|
|
245
|
+
}
|
|
246
|
+
lines.push(prefix + "╰" + "─".repeat(boxWidth - 2) + "╯");
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// Decision / default box
|
|
250
|
+
lines.push(prefix + "┌" + "─".repeat(boxWidth - 2) + "┐");
|
|
251
|
+
for (const line of truncated) {
|
|
252
|
+
const padded = line + " ".repeat(Math.max(0, boxWidth - 4 - line.length));
|
|
253
|
+
lines.push(prefix + "│ " + padded + " │");
|
|
254
|
+
}
|
|
255
|
+
lines.push(prefix + "└" + "─".repeat(boxWidth - 2) + "┘");
|
|
256
|
+
}
|
|
257
|
+
// Draw edges
|
|
258
|
+
if (node.successTarget && node.failureTarget) {
|
|
259
|
+
if (node.successTarget === node.failureTarget) {
|
|
260
|
+
lines.push(prefix + " │");
|
|
261
|
+
lines.push(prefix + " ├── ✓ ──┐");
|
|
262
|
+
lines.push(prefix + " └── ✗ ──┘");
|
|
263
|
+
renderNode(node.successTarget, indent + 2);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
lines.push(prefix + " ├──── ✓ ──▶");
|
|
267
|
+
renderNode(node.successTarget, indent + 4);
|
|
268
|
+
lines.push(prefix + " │");
|
|
269
|
+
lines.push(prefix + " └──── ✗ ──▶");
|
|
270
|
+
renderNode(node.failureTarget, indent + 4);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else if (node.successTarget) {
|
|
274
|
+
lines.push(prefix + " │");
|
|
275
|
+
lines.push(prefix + " ✓");
|
|
276
|
+
lines.push(prefix + " ▼");
|
|
277
|
+
renderNode(node.successTarget, indent);
|
|
278
|
+
}
|
|
279
|
+
else if (node.failureTarget) {
|
|
280
|
+
lines.push(prefix + " │");
|
|
281
|
+
lines.push(prefix + " ✗");
|
|
282
|
+
lines.push(prefix + " ▼");
|
|
283
|
+
renderNode(node.failureTarget, indent);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const startId = entryId || nodes.keys().next().value;
|
|
287
|
+
if (startId) {
|
|
288
|
+
renderNode(startId, 0);
|
|
289
|
+
}
|
|
290
|
+
return lines.join("\n");
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Fallback: generate a vertical ASCII diagram directly from task definitions.
|
|
294
|
+
* Used when the recipe has no `diagram:` field.
|
|
295
|
+
*/
|
|
296
|
+
export function renderTaskChainAscii(rootTaskId, tasks) {
|
|
297
|
+
const taskMap = new Map();
|
|
298
|
+
for (const task of tasks)
|
|
299
|
+
taskMap.set(task.id, task);
|
|
300
|
+
const lines = [];
|
|
301
|
+
const visited = new Set();
|
|
302
|
+
function renderTaskNode(taskId) {
|
|
303
|
+
if (visited.has(taskId)) {
|
|
304
|
+
lines.push(` ↻ back to ${taskId}`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
visited.add(taskId);
|
|
308
|
+
const task = taskMap.get(taskId);
|
|
309
|
+
if (!task) {
|
|
310
|
+
lines.push(` ⚠ ${taskId} (missing task)`);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const nameLC = (task.name || "").toLowerCase();
|
|
314
|
+
const isTerminal = !task.onSuccessTaskId && !task.onFailureTaskId;
|
|
315
|
+
const isFail = isTerminal && (nameLC.includes("fail") || nameLC.includes("recovery") || nameLC.includes("reset"));
|
|
316
|
+
const isIdle = isTerminal && (task.silentChain || nameLC.includes("idle") || nameLC.includes("nothing"));
|
|
317
|
+
const isSuccess = isTerminal && !isFail && !isIdle;
|
|
318
|
+
const lines_content = [task.name];
|
|
319
|
+
lines_content.push(" " + [task.command, ...task.commandArgs].join(" ").slice(0, 60));
|
|
320
|
+
const boxWidth = Math.min(80, Math.max(...lines_content.map((l) => l.length)) + 4);
|
|
321
|
+
if (isFail) {
|
|
322
|
+
lines.push("┏" + "━".repeat(boxWidth - 2) + "┓");
|
|
323
|
+
for (const line of lines_content) {
|
|
324
|
+
lines.push("┃ " + line + " ".repeat(Math.max(0, boxWidth - 4 - line.length)) + " ┃");
|
|
325
|
+
}
|
|
326
|
+
lines.push("┗" + "━".repeat(boxWidth - 2) + "┛");
|
|
327
|
+
}
|
|
328
|
+
else if (isSuccess) {
|
|
329
|
+
lines.push("╔" + "═".repeat(boxWidth - 2) + "╗");
|
|
330
|
+
for (const line of lines_content) {
|
|
331
|
+
lines.push("║ " + line + " ".repeat(Math.max(0, boxWidth - 4 - line.length)) + " ║");
|
|
332
|
+
}
|
|
333
|
+
lines.push("╚" + "═".repeat(boxWidth - 2) + "╝");
|
|
334
|
+
}
|
|
335
|
+
else if (isIdle) {
|
|
336
|
+
lines.push("┌" + "┄".repeat(boxWidth - 2) + "┐");
|
|
337
|
+
for (const line of lines_content) {
|
|
338
|
+
lines.push("┊ " + line + " ".repeat(Math.max(0, boxWidth - 4 - line.length)) + " ┊");
|
|
339
|
+
}
|
|
340
|
+
lines.push("└" + "┄".repeat(boxWidth - 2) + "┘");
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
lines.push("┌" + "─".repeat(boxWidth - 2) + "┐");
|
|
344
|
+
for (const line of lines_content) {
|
|
345
|
+
lines.push("│ " + line + " ".repeat(Math.max(0, boxWidth - 4 - line.length)) + " │");
|
|
346
|
+
}
|
|
347
|
+
lines.push("└" + "─".repeat(boxWidth - 2) + "┘");
|
|
348
|
+
}
|
|
349
|
+
if (task.onSuccessTaskId && task.onFailureTaskId) {
|
|
350
|
+
if (task.onSuccessTaskId === task.onFailureTaskId) {
|
|
351
|
+
lines.push(" ├──── ✓ ──┐");
|
|
352
|
+
lines.push(" └──── ✗ ──┘");
|
|
353
|
+
renderTaskNode(task.onSuccessTaskId);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
lines.push(" ├──── ✓ ──▶");
|
|
357
|
+
renderTaskNode(task.onSuccessTaskId);
|
|
358
|
+
lines.push(" └──── ✗ ──▶");
|
|
359
|
+
renderTaskNode(task.onFailureTaskId);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else if (task.onSuccessTaskId) {
|
|
363
|
+
lines.push(" │ ✓");
|
|
364
|
+
lines.push(" ▼");
|
|
365
|
+
renderTaskNode(task.onSuccessTaskId);
|
|
366
|
+
}
|
|
367
|
+
else if (task.onFailureTaskId) {
|
|
368
|
+
lines.push(" │ ✗");
|
|
369
|
+
lines.push(" ▼");
|
|
370
|
+
renderTaskNode(task.onFailureTaskId);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
renderTaskNode(rootTaskId);
|
|
374
|
+
return lines.join("\n");
|
|
375
|
+
}
|