devglide 0.1.1
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 +338 -0
- package/bin/claude-md-template.js +94 -0
- package/bin/devglide.js +387 -0
- package/package.json +85 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/apps/coder/.turbo/turbo-lint.log +5 -0
- package/src/apps/coder/package.json +16 -0
- package/src/apps/coder/public/favicon.svg +7 -0
- package/src/apps/coder/public/page.css +275 -0
- package/src/apps/coder/public/page.js +528 -0
- package/src/apps/coder/server.js +3 -0
- package/src/apps/documentation/public/page.css +597 -0
- package/src/apps/documentation/public/page.js +609 -0
- package/src/apps/kanban/.turbo/turbo-lint.log +97 -0
- package/src/apps/kanban/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/kanban/package.json +32 -0
- package/src/apps/kanban/public/favicon.svg +7 -0
- package/src/apps/kanban/public/page.css +1010 -0
- package/src/apps/kanban/public/page.js +1730 -0
- package/src/apps/kanban/public/vendor/marked.min.js +6 -0
- package/src/apps/kanban/public/vendor/sortable.min.js +2 -0
- package/src/apps/kanban/src/db.ts +319 -0
- package/src/apps/kanban/src/index.ts +14 -0
- package/src/apps/kanban/src/mcp-helpers.test.ts +88 -0
- package/src/apps/kanban/src/mcp-helpers.ts +60 -0
- package/src/apps/kanban/src/mcp.ts +59 -0
- package/src/apps/kanban/src/routes/attachments.ts +161 -0
- package/src/apps/kanban/src/routes/features.ts +233 -0
- package/src/apps/kanban/src/routes/issues.ts +373 -0
- package/src/apps/kanban/src/tools/feature-tools.ts +164 -0
- package/src/apps/kanban/src/tools/item-tools.ts +307 -0
- package/src/apps/kanban/src/tools/versioned-entry-tools.ts +72 -0
- package/src/apps/kanban/tsconfig.check.json +9 -0
- package/src/apps/kanban/tsconfig.json +9 -0
- package/src/apps/keymap/.turbo/turbo-lint.log +5 -0
- package/src/apps/keymap/package.json +16 -0
- package/src/apps/keymap/public/page.css +275 -0
- package/src/apps/keymap/public/page.js +294 -0
- package/src/apps/keymap/server.js +25 -0
- package/src/apps/log/.turbo/turbo-build.log +5 -0
- package/src/apps/log/.turbo/turbo-lint.log +45 -0
- package/src/apps/log/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/log/node_modules/.bin/tsc +21 -0
- package/src/apps/log/node_modules/.bin/tsserver +21 -0
- package/src/apps/log/node_modules/.bin/tsx +21 -0
- package/src/apps/log/package.json +36 -0
- package/src/apps/log/public/console-sniffer.js +221 -0
- package/src/apps/log/public/favicon.svg +7 -0
- package/src/apps/log/public/page.css +322 -0
- package/src/apps/log/public/page.js +463 -0
- package/src/apps/log/src/index.ts +9 -0
- package/src/apps/log/src/mcp.ts +122 -0
- package/src/apps/log/src/routes/log.ts +333 -0
- package/src/apps/log/src/routes/status.ts +25 -0
- package/src/apps/log/src/server-sniffer.ts +118 -0
- package/src/apps/log/src/services/file-patterns.ts +39 -0
- package/src/apps/log/src/services/file-tailer.ts +228 -0
- package/src/apps/log/src/services/line-parser.ts +94 -0
- package/src/apps/log/src/services/log-writer.ts +39 -0
- package/src/apps/log/tsconfig.json +8 -0
- package/src/apps/prompts/.turbo/turbo-build.log +5 -0
- package/src/apps/prompts/.turbo/turbo-lint.log +24 -0
- package/src/apps/prompts/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/prompts/mcp.ts +175 -0
- package/src/apps/prompts/node_modules/.bin/tsc +21 -0
- package/src/apps/prompts/node_modules/.bin/tsserver +21 -0
- package/src/apps/prompts/node_modules/.bin/tsx +21 -0
- package/src/apps/prompts/package.json +25 -0
- package/src/apps/prompts/public/page.css +315 -0
- package/src/apps/prompts/public/page.js +541 -0
- package/src/apps/prompts/services/prompt-store.ts +212 -0
- package/src/apps/prompts/src/index.ts +9 -0
- package/src/apps/prompts/tsconfig.json +8 -0
- package/src/apps/prompts/types.ts +27 -0
- package/src/apps/shell/.turbo/turbo-build.log +5 -0
- package/src/apps/shell/.turbo/turbo-lint.log +34 -0
- package/src/apps/shell/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/shell/package.json +35 -0
- package/src/apps/shell/public/favicon.svg +7 -0
- package/src/apps/shell/public/page.css +407 -0
- package/src/apps/shell/public/page.js +1577 -0
- package/src/apps/shell/src/index.ts +150 -0
- package/src/apps/shell/src/mcp.ts +398 -0
- package/src/apps/shell/src/shell-types.ts +41 -0
- package/src/apps/shell/tsconfig.json +8 -0
- package/src/apps/test/.turbo/turbo-build.log +5 -0
- package/src/apps/test/.turbo/turbo-lint.log +27 -0
- package/src/apps/test/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/test/node_modules/.bin/tsc +21 -0
- package/src/apps/test/node_modules/.bin/tsserver +21 -0
- package/src/apps/test/node_modules/.bin/tsx +21 -0
- package/src/apps/test/node_modules/.bin/uuid +21 -0
- package/src/apps/test/package.json +35 -0
- package/src/apps/test/public/favicon.svg +7 -0
- package/src/apps/test/public/page.css +499 -0
- package/src/apps/test/public/page.js +417 -0
- package/src/apps/test/public/scenario-runner.js +450 -0
- package/src/apps/test/src/index.ts +9 -0
- package/src/apps/test/src/mcp.ts +192 -0
- package/src/apps/test/src/routes/trigger.ts +285 -0
- package/src/apps/test/src/services/scenario-broadcaster.ts +60 -0
- package/src/apps/test/src/services/scenario-manager.ts +361 -0
- package/src/apps/test/src/services/scenario-store.ts +145 -0
- package/src/apps/test/tsconfig.json +8 -0
- package/src/apps/vocabulary/.turbo/turbo-build.log +5 -0
- package/src/apps/vocabulary/.turbo/turbo-lint.log +25 -0
- package/src/apps/vocabulary/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/vocabulary/mcp.ts +173 -0
- package/src/apps/vocabulary/node_modules/.bin/tsc +21 -0
- package/src/apps/vocabulary/node_modules/.bin/tsserver +21 -0
- package/src/apps/vocabulary/node_modules/.bin/tsx +21 -0
- package/src/apps/vocabulary/package.json +25 -0
- package/src/apps/vocabulary/public/page.css +247 -0
- package/src/apps/vocabulary/public/page.js +444 -0
- package/src/apps/vocabulary/services/vocabulary-store.ts +179 -0
- package/src/apps/vocabulary/src/index.ts +10 -0
- package/src/apps/vocabulary/tsconfig.json +8 -0
- package/src/apps/vocabulary/types.ts +22 -0
- package/src/apps/voice/.turbo/turbo-build.log +5 -0
- package/src/apps/voice/.turbo/turbo-lint.log +43 -0
- package/src/apps/voice/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/voice/node_modules/.bin/openai +21 -0
- package/src/apps/voice/node_modules/.bin/tsc +21 -0
- package/src/apps/voice/node_modules/.bin/tsserver +21 -0
- package/src/apps/voice/node_modules/.bin/tsx +21 -0
- package/src/apps/voice/package.json +35 -0
- package/src/apps/voice/public/favicon.svg +7 -0
- package/src/apps/voice/public/page.css +388 -0
- package/src/apps/voice/public/page.js +718 -0
- package/src/apps/voice/src/index.ts +10 -0
- package/src/apps/voice/src/mcp.ts +70 -0
- package/src/apps/voice/src/providers/index.ts +85 -0
- package/src/apps/voice/src/providers/openai-compatible.ts +94 -0
- package/src/apps/voice/src/providers/types.ts +27 -0
- package/src/apps/voice/src/routes/config.ts +118 -0
- package/src/apps/voice/src/routes/transcribe.ts +90 -0
- package/src/apps/voice/src/services/config-store.ts +129 -0
- package/src/apps/voice/src/services/stats.ts +108 -0
- package/src/apps/voice/src/transcribe.ts +11 -0
- package/src/apps/voice/src/utils/mime.ts +16 -0
- package/src/apps/voice/tsconfig.json +8 -0
- package/src/apps/workflow/.turbo/turbo-build.log +5 -0
- package/src/apps/workflow/.turbo/turbo-lint.log +96 -0
- package/src/apps/workflow/.turbo/turbo-typecheck.log +5 -0
- package/src/apps/workflow/engine/executors/decision-executor.ts +87 -0
- package/src/apps/workflow/engine/executors/file-executor.ts +90 -0
- package/src/apps/workflow/engine/executors/git-executor.ts +137 -0
- package/src/apps/workflow/engine/executors/http-executor.ts +65 -0
- package/src/apps/workflow/engine/executors/index.ts +28 -0
- package/src/apps/workflow/engine/executors/kanban-executor.ts +154 -0
- package/src/apps/workflow/engine/executors/llm-executor.ts +46 -0
- package/src/apps/workflow/engine/executors/log-executor.ts +62 -0
- package/src/apps/workflow/engine/executors/loop-executor.ts +14 -0
- package/src/apps/workflow/engine/executors/shell-executor.ts +107 -0
- package/src/apps/workflow/engine/executors/sub-workflow-executor.ts +61 -0
- package/src/apps/workflow/engine/executors/test-executor.ts +73 -0
- package/src/apps/workflow/engine/executors/trigger-executor.ts +39 -0
- package/src/apps/workflow/engine/expression-evaluator.ts +117 -0
- package/src/apps/workflow/engine/graph-runner.ts +438 -0
- package/src/apps/workflow/engine/node-executor.ts +104 -0
- package/src/apps/workflow/engine/node-registry.ts +15 -0
- package/src/apps/workflow/engine/variable-resolver.ts +109 -0
- package/src/apps/workflow/mcp.ts +223 -0
- package/src/apps/workflow/node_modules/.bin/tsc +21 -0
- package/src/apps/workflow/node_modules/.bin/tsserver +21 -0
- package/src/apps/workflow/node_modules/.bin/tsx +21 -0
- package/src/apps/workflow/package.json +25 -0
- package/src/apps/workflow/public/editor/canvas.js +366 -0
- package/src/apps/workflow/public/editor/drag-manager.js +326 -0
- package/src/apps/workflow/public/editor/edge-renderer.js +235 -0
- package/src/apps/workflow/public/editor/history-manager.js +147 -0
- package/src/apps/workflow/public/editor/layout-engine.js +159 -0
- package/src/apps/workflow/public/editor/node-renderer.js +199 -0
- package/src/apps/workflow/public/editor/selection-manager.js +193 -0
- package/src/apps/workflow/public/favicon.svg +7 -0
- package/src/apps/workflow/public/models/node-types.js +300 -0
- package/src/apps/workflow/public/models/workflow-model.js +257 -0
- package/src/apps/workflow/public/page.css +406 -0
- package/src/apps/workflow/public/page.js +658 -0
- package/src/apps/workflow/public/panels/inspector.js +360 -0
- package/src/apps/workflow/public/panels/palette.js +106 -0
- package/src/apps/workflow/public/panels/run-view.js +275 -0
- package/src/apps/workflow/public/panels/toolbar.js +232 -0
- package/src/apps/workflow/public/panels/workflow-list.js +237 -0
- package/src/apps/workflow/public/state/store.js +47 -0
- package/src/apps/workflow/services/custom-node-loader.ts +48 -0
- package/src/apps/workflow/services/legacy-converter.ts +72 -0
- package/src/apps/workflow/services/run-manager.ts +190 -0
- package/src/apps/workflow/services/workflow-store.ts +424 -0
- package/src/apps/workflow/services/workflow-validator.test.ts +103 -0
- package/src/apps/workflow/services/workflow-validator.ts +98 -0
- package/src/apps/workflow/src/index.ts +10 -0
- package/src/apps/workflow/templates/ci-pipeline.json +18 -0
- package/src/apps/workflow/templates/code-review.json +22 -0
- package/src/apps/workflow/templates/kanban-testing.json +24 -0
- package/src/apps/workflow/tsconfig.json +8 -0
- package/src/apps/workflow/types.ts +268 -0
- package/src/packages/auth-middleware.ts +14 -0
- package/src/packages/design-tokens/.turbo/turbo-build.log +10 -0
- package/src/packages/design-tokens/STYLEGUIDE.md +414 -0
- package/src/packages/design-tokens/build.js +413 -0
- package/src/packages/design-tokens/demo/index.html +1367 -0
- package/src/packages/design-tokens/demo/proposition-a.html +717 -0
- package/src/packages/design-tokens/demo/proposition-b.html +1239 -0
- package/src/packages/design-tokens/demo/proposition-c.html +1049 -0
- package/src/packages/design-tokens/dist/tailwind-preset.js +115 -0
- package/src/packages/design-tokens/dist/tokens.css +345 -0
- package/src/packages/design-tokens/dist/tokens.d.ts +229 -0
- package/src/packages/design-tokens/dist/tokens.js +386 -0
- package/src/packages/design-tokens/package.json +25 -0
- package/src/packages/design-tokens/tokens.json +228 -0
- package/src/packages/devtools-middleware.ts +22 -0
- package/src/packages/eslint-config/index.js +63 -0
- package/src/packages/eslint-config/node_modules/.bin/eslint +21 -0
- package/src/packages/eslint-config/package.json +18 -0
- package/src/packages/json-file-store.ts +232 -0
- package/src/packages/mcp-utils/.turbo/turbo-build.log +5 -0
- package/src/packages/mcp-utils/dist/index.d.ts +33 -0
- package/src/packages/mcp-utils/dist/index.d.ts.map +1 -0
- package/src/packages/mcp-utils/dist/index.js +126 -0
- package/src/packages/mcp-utils/dist/index.js.map +1 -0
- package/src/packages/mcp-utils/node_modules/.bin/tsc +21 -0
- package/src/packages/mcp-utils/node_modules/.bin/tsserver +21 -0
- package/src/packages/mcp-utils/package.json +32 -0
- package/src/packages/mcp-utils/src/index.ts +171 -0
- package/src/packages/mcp-utils/tsconfig.json +9 -0
- package/src/packages/paths.ts +18 -0
- package/src/packages/project-context/index.js +55 -0
- package/src/packages/project-context/package.json +13 -0
- package/src/packages/project-store.ts +127 -0
- package/src/packages/server-sniffer.ts +132 -0
- package/src/packages/shared-assets/favicon.svg +7 -0
- package/src/packages/shared-assets/keymap-registry.js +512 -0
- package/src/packages/shared-assets/logo.svg +6 -0
- package/src/packages/shared-assets/package.json +11 -0
- package/src/packages/shared-assets/ui-utils.js +48 -0
- package/src/packages/shared-assets/voice-widget.d.ts +37 -0
- package/src/packages/shared-assets/voice-widget.js +695 -0
- package/src/packages/shared-types/.turbo/turbo-build.log +5 -0
- package/src/packages/shared-types/dist/index.d.ts +39 -0
- package/src/packages/shared-types/dist/index.d.ts.map +1 -0
- package/src/packages/shared-types/node_modules/.bin/tsc +21 -0
- package/src/packages/shared-types/node_modules/.bin/tsserver +21 -0
- package/src/packages/shared-types/package.json +25 -0
- package/src/packages/shared-types/src/index.ts +41 -0
- package/src/packages/shared-types/tsconfig.json +11 -0
- package/src/packages/tsconfig/base.json +15 -0
- package/src/packages/tsconfig/next.json +14 -0
- package/src/packages/tsconfig/node.json +11 -0
- package/src/packages/tsconfig/package.json +10 -0
- package/turbo.json +25 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// ── Workflow Editor — Layout Utilities ──────────────────────────────────
|
|
2
|
+
// Grid snapping, fit-to-view, and automatic layout for imported workflows.
|
|
3
|
+
|
|
4
|
+
const NODE_WIDTH = 220;
|
|
5
|
+
const NODE_HEIGHT = 80;
|
|
6
|
+
const H_SPACING = 100; // Horizontal gap between columns
|
|
7
|
+
const V_SPACING = 40; // Vertical gap between rows
|
|
8
|
+
|
|
9
|
+
export const LayoutEngine = {
|
|
10
|
+
/**
|
|
11
|
+
* Snap coordinates to a grid.
|
|
12
|
+
* @param {number} x
|
|
13
|
+
* @param {number} y
|
|
14
|
+
* @param {number} [gridSize=20]
|
|
15
|
+
* @returns {{ x: number, y: number }}
|
|
16
|
+
*/
|
|
17
|
+
snapToGrid(x, y, gridSize = 20) {
|
|
18
|
+
return {
|
|
19
|
+
x: Math.round(x / gridSize) * gridSize,
|
|
20
|
+
y: Math.round(y / gridSize) * gridSize,
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Calculate zoom and pan to fit all nodes within the canvas viewport.
|
|
26
|
+
* @param {Array<{ position: { x: number, y: number } }>} nodes
|
|
27
|
+
* @param {number} canvasWidth
|
|
28
|
+
* @param {number} canvasHeight
|
|
29
|
+
* @param {number} [padding=50]
|
|
30
|
+
* @returns {{ zoom: number, panX: number, panY: number }}
|
|
31
|
+
*/
|
|
32
|
+
fitToView(nodes, canvasWidth, canvasHeight, padding = 50) {
|
|
33
|
+
if (!nodes.length) {
|
|
34
|
+
return { zoom: 1, panX: 0, panY: 0 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
38
|
+
for (const n of nodes) {
|
|
39
|
+
minX = Math.min(minX, n.position.x);
|
|
40
|
+
minY = Math.min(minY, n.position.y);
|
|
41
|
+
maxX = Math.max(maxX, n.position.x + NODE_WIDTH);
|
|
42
|
+
maxY = Math.max(maxY, n.position.y + NODE_HEIGHT);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const boundsW = maxX - minX + padding * 2;
|
|
46
|
+
const boundsH = maxY - minY + padding * 2;
|
|
47
|
+
|
|
48
|
+
const zoom = Math.max(0.25, Math.min(3, Math.min(canvasWidth / boundsW, canvasHeight / boundsH)));
|
|
49
|
+
const panX = (canvasWidth - boundsW * zoom) / 2 - (minX - padding) * zoom;
|
|
50
|
+
const panY = (canvasHeight - boundsH * zoom) / 2 - (minY - padding) * zoom;
|
|
51
|
+
|
|
52
|
+
return { zoom, panX, panY };
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Automatically layout nodes in a top-to-bottom arrangement.
|
|
57
|
+
* Uses a simple topological sort approach for DAG layout.
|
|
58
|
+
* Useful for imported legacy workflows that have no position data.
|
|
59
|
+
*
|
|
60
|
+
* @param {Array<object>} nodes - Nodes with { id, type, ... }
|
|
61
|
+
* @param {Array<object>} edges - Edges with { source, target, ... }
|
|
62
|
+
* @returns {Array<object>} Nodes with updated positions
|
|
63
|
+
*/
|
|
64
|
+
autoLayout(nodes, edges) {
|
|
65
|
+
if (!nodes.length) return nodes;
|
|
66
|
+
|
|
67
|
+
// Build adjacency map
|
|
68
|
+
const outgoing = new Map(); // nodeId -> [targetNodeId]
|
|
69
|
+
const incoming = new Map(); // nodeId -> [sourceNodeId]
|
|
70
|
+
const nodeMap = new Map();
|
|
71
|
+
|
|
72
|
+
for (const n of nodes) {
|
|
73
|
+
nodeMap.set(n.id, n);
|
|
74
|
+
outgoing.set(n.id, []);
|
|
75
|
+
incoming.set(n.id, []);
|
|
76
|
+
}
|
|
77
|
+
for (const e of edges) {
|
|
78
|
+
if (outgoing.has(e.source) && incoming.has(e.target)) {
|
|
79
|
+
outgoing.get(e.source).push(e.target);
|
|
80
|
+
incoming.get(e.target).push(e.source);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Topological sort (Kahn's algorithm) to determine layers
|
|
85
|
+
const inDegree = new Map();
|
|
86
|
+
for (const n of nodes) {
|
|
87
|
+
inDegree.set(n.id, incoming.get(n.id).length);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const layers = [];
|
|
91
|
+
const visited = new Set();
|
|
92
|
+
let queue = [];
|
|
93
|
+
|
|
94
|
+
// Start with nodes that have no incoming edges (triggers, roots)
|
|
95
|
+
for (const n of nodes) {
|
|
96
|
+
if (inDegree.get(n.id) === 0) {
|
|
97
|
+
queue.push(n.id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
while (queue.length > 0) {
|
|
102
|
+
layers.push([...queue]);
|
|
103
|
+
for (const id of queue) visited.add(id);
|
|
104
|
+
|
|
105
|
+
const nextQueue = [];
|
|
106
|
+
for (const id of queue) {
|
|
107
|
+
for (const targetId of outgoing.get(id)) {
|
|
108
|
+
if (visited.has(targetId)) continue;
|
|
109
|
+
inDegree.set(targetId, inDegree.get(targetId) - 1);
|
|
110
|
+
if (inDegree.get(targetId) === 0) {
|
|
111
|
+
nextQueue.push(targetId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
queue = nextQueue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle any remaining nodes (cycles or disconnected)
|
|
119
|
+
for (const n of nodes) {
|
|
120
|
+
if (!visited.has(n.id)) {
|
|
121
|
+
layers.push([n.id]);
|
|
122
|
+
visited.add(n.id);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Assign positions: each layer is a column (left to right)
|
|
127
|
+
const result = nodes.map(n => ({ ...n, position: { ...n.position } }));
|
|
128
|
+
|
|
129
|
+
for (let col = 0; col < layers.length; col++) {
|
|
130
|
+
const layer = layers[col];
|
|
131
|
+
const totalHeight = layer.length * NODE_HEIGHT + (layer.length - 1) * V_SPACING;
|
|
132
|
+
const startY = -totalHeight / 2;
|
|
133
|
+
|
|
134
|
+
for (let row = 0; row < layer.length; row++) {
|
|
135
|
+
const nodeId = layer[row];
|
|
136
|
+
const node = result.find(n => n.id === nodeId);
|
|
137
|
+
if (node) {
|
|
138
|
+
node.position.x = col * (NODE_WIDTH + H_SPACING);
|
|
139
|
+
node.position.y = startY + row * (NODE_HEIGHT + V_SPACING);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Normalize so that the top-left is at (40, 40)
|
|
145
|
+
let minX = Infinity, minY = Infinity;
|
|
146
|
+
for (const n of result) {
|
|
147
|
+
minX = Math.min(minX, n.position.x);
|
|
148
|
+
minY = Math.min(minY, n.position.y);
|
|
149
|
+
}
|
|
150
|
+
const offsetX = 40 - minX;
|
|
151
|
+
const offsetY = 40 - minY;
|
|
152
|
+
for (const n of result) {
|
|
153
|
+
n.position.x += offsetX;
|
|
154
|
+
n.position.y += offsetY;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// ── Workflow Editor — Node Card DOM Creation ───────────────────────────
|
|
2
|
+
// Creates, updates, and manages runtime status for node DOM elements.
|
|
3
|
+
// Simplified to a single "step" node type for LLM instruction flows.
|
|
4
|
+
|
|
5
|
+
import { NODE_TYPES, resolveOutputPorts, resolveInputPortCount } from '../models/node-types.js';
|
|
6
|
+
|
|
7
|
+
const NODE_WIDTH = 220;
|
|
8
|
+
|
|
9
|
+
function truncate(s, len) {
|
|
10
|
+
return s.length > len ? s.slice(0, len) + '\u2026' : s;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build a compact config preview string for a node.
|
|
15
|
+
*/
|
|
16
|
+
function configPreview(node) {
|
|
17
|
+
const c = node.config ?? {};
|
|
18
|
+
|
|
19
|
+
switch (node.type) {
|
|
20
|
+
case 'trigger':
|
|
21
|
+
return c.triggerType ?? 'manual';
|
|
22
|
+
case 'action:shell':
|
|
23
|
+
return c.command ? truncate(c.command, 40) : 'No command';
|
|
24
|
+
case 'action:kanban':
|
|
25
|
+
return c.operation ?? 'kanban';
|
|
26
|
+
case 'action:git':
|
|
27
|
+
return c.operation ? `git ${c.operation}` : 'git';
|
|
28
|
+
case 'action:llm':
|
|
29
|
+
return c.prompt ? truncate(c.prompt, 40) : (c.model ?? 'LLM');
|
|
30
|
+
case 'action:test':
|
|
31
|
+
return c.operation ?? 'test';
|
|
32
|
+
case 'action:log':
|
|
33
|
+
return c.operation ?? 'log';
|
|
34
|
+
case 'action:file':
|
|
35
|
+
return c.operation ? `${c.operation}: ${truncate(c.path ?? '', 30)}` : 'file';
|
|
36
|
+
case 'action:http':
|
|
37
|
+
return c.method ? `${c.method} ${truncate(c.url ?? '', 30)}` : 'HTTP';
|
|
38
|
+
case 'decision':
|
|
39
|
+
return c.conditionType ?? 'decision';
|
|
40
|
+
case 'loop':
|
|
41
|
+
return c.loopType ?? 'loop';
|
|
42
|
+
case 'sub-workflow':
|
|
43
|
+
return c.workflowId ? truncate(c.workflowId, 30) : 'sub-workflow';
|
|
44
|
+
case 'step':
|
|
45
|
+
if (c.instructions) return truncate(c.instructions, 40);
|
|
46
|
+
if (c.instructionFile) return c.instructionFile;
|
|
47
|
+
return 'Click to add instructions';
|
|
48
|
+
default:
|
|
49
|
+
return node.type;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export const NodeRenderer = {
|
|
56
|
+
/**
|
|
57
|
+
* Create a DOM element for a workflow node.
|
|
58
|
+
* @param {object} node - { id, type, label, position, config }
|
|
59
|
+
* @returns {HTMLElement}
|
|
60
|
+
*/
|
|
61
|
+
createNodeElement(node) {
|
|
62
|
+
const typeDef = NODE_TYPES[node.type];
|
|
63
|
+
const color = typeDef?.color ?? '#64748b';
|
|
64
|
+
|
|
65
|
+
const el = document.createElement('div');
|
|
66
|
+
el.className = 'wfb-node';
|
|
67
|
+
el.dataset.nodeId = node.id;
|
|
68
|
+
el.style.position = 'absolute';
|
|
69
|
+
el.style.left = `${node.position.x}px`;
|
|
70
|
+
el.style.top = `${node.position.y}px`;
|
|
71
|
+
el.style.setProperty('--wfb-type-color', color);
|
|
72
|
+
|
|
73
|
+
// Header
|
|
74
|
+
const header = document.createElement('div');
|
|
75
|
+
header.className = 'wfb-node-header';
|
|
76
|
+
|
|
77
|
+
const labelSpan = document.createElement('span');
|
|
78
|
+
labelSpan.className = 'wfb-node-label';
|
|
79
|
+
labelSpan.textContent = node.label;
|
|
80
|
+
|
|
81
|
+
header.appendChild(labelSpan);
|
|
82
|
+
|
|
83
|
+
// Body (config preview)
|
|
84
|
+
const body = document.createElement('div');
|
|
85
|
+
body.className = 'wfb-node-body';
|
|
86
|
+
body.textContent = configPreview(node);
|
|
87
|
+
|
|
88
|
+
el.appendChild(header);
|
|
89
|
+
el.appendChild(body);
|
|
90
|
+
|
|
91
|
+
// Input ports
|
|
92
|
+
const inCount = resolveInputPortCount(node);
|
|
93
|
+
for (let i = 0; i < inCount; i++) {
|
|
94
|
+
const portIn = document.createElement('div');
|
|
95
|
+
portIn.className = 'wfb-port wfb-port-in';
|
|
96
|
+
portIn.dataset.portId = `in-${i}`;
|
|
97
|
+
portIn.dataset.portType = 'in';
|
|
98
|
+
portIn.dataset.nodeId = node.id;
|
|
99
|
+
if (inCount > 1) {
|
|
100
|
+
portIn.style.top = `${((i + 1) / (inCount + 1)) * 100}%`;
|
|
101
|
+
}
|
|
102
|
+
el.appendChild(portIn);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Output ports
|
|
106
|
+
const outPorts = resolveOutputPorts(node);
|
|
107
|
+
for (let i = 0; i < outPorts.length; i++) {
|
|
108
|
+
const portOut = document.createElement('div');
|
|
109
|
+
portOut.className = 'wfb-port wfb-port-out';
|
|
110
|
+
portOut.dataset.portId = outPorts[i].id;
|
|
111
|
+
portOut.dataset.portType = 'out';
|
|
112
|
+
portOut.dataset.nodeId = node.id;
|
|
113
|
+
if (outPorts.length > 1) {
|
|
114
|
+
portOut.style.top = `${((i + 1) / (outPorts.length + 1)) * 100}%`;
|
|
115
|
+
portOut.title = outPorts[i].label;
|
|
116
|
+
}
|
|
117
|
+
el.appendChild(portOut);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return el;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Update an existing node element to reflect changes.
|
|
125
|
+
* @param {HTMLElement} el
|
|
126
|
+
* @param {object} node
|
|
127
|
+
*/
|
|
128
|
+
updateNodeElement(el, node) {
|
|
129
|
+
const typeDef = NODE_TYPES[node.type];
|
|
130
|
+
const color = typeDef?.color ?? '#64748b';
|
|
131
|
+
|
|
132
|
+
// Update position
|
|
133
|
+
el.style.left = `${node.position.x}px`;
|
|
134
|
+
el.style.top = `${node.position.y}px`;
|
|
135
|
+
|
|
136
|
+
// Update type color
|
|
137
|
+
el.style.setProperty('--wfb-type-color', color);
|
|
138
|
+
|
|
139
|
+
// Update label
|
|
140
|
+
const labelEl = el.querySelector('.wfb-node-label');
|
|
141
|
+
if (labelEl) labelEl.textContent = node.label;
|
|
142
|
+
|
|
143
|
+
// Update config preview
|
|
144
|
+
const bodyEl = el.querySelector('.wfb-node-body');
|
|
145
|
+
if (bodyEl) bodyEl.textContent = configPreview(node);
|
|
146
|
+
|
|
147
|
+
// Sync output ports for dynamic-port nodes (decision)
|
|
148
|
+
if (typeDef?.ports?.out === 'dynamic') {
|
|
149
|
+
const outPorts = resolveOutputPorts(node);
|
|
150
|
+
const existingOuts = el.querySelectorAll('.wfb-port-out');
|
|
151
|
+
// Only rebuild if count changed
|
|
152
|
+
if (existingOuts.length !== outPorts.length) {
|
|
153
|
+
existingOuts.forEach(p => p.remove());
|
|
154
|
+
for (let i = 0; i < outPorts.length; i++) {
|
|
155
|
+
const portOut = document.createElement('div');
|
|
156
|
+
portOut.className = 'wfb-port wfb-port-out';
|
|
157
|
+
portOut.dataset.portId = outPorts[i].id;
|
|
158
|
+
portOut.dataset.portType = 'out';
|
|
159
|
+
portOut.dataset.nodeId = node.id;
|
|
160
|
+
if (outPorts.length > 1) {
|
|
161
|
+
portOut.style.top = `${((i + 1) / (outPorts.length + 1)) * 100}%`;
|
|
162
|
+
portOut.title = outPorts[i].label;
|
|
163
|
+
}
|
|
164
|
+
el.appendChild(portOut);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Set runtime status on a node element.
|
|
172
|
+
* @param {HTMLElement} el
|
|
173
|
+
* @param {'running'|'passed'|'failed'} status
|
|
174
|
+
*/
|
|
175
|
+
setNodeStatus(el, status) {
|
|
176
|
+
el.classList.remove('wfb-status-running', 'wfb-status-passed', 'wfb-status-failed');
|
|
177
|
+
el.classList.add(`wfb-status-${status}`);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Remove runtime status from a node element.
|
|
182
|
+
* @param {HTMLElement} el
|
|
183
|
+
*/
|
|
184
|
+
clearNodeStatus(el) {
|
|
185
|
+
el.classList.remove('wfb-status-running', 'wfb-status-passed', 'wfb-status-failed');
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Mark a node element as selected.
|
|
190
|
+
* @param {HTMLElement} el
|
|
191
|
+
* @param {boolean} selected
|
|
192
|
+
*/
|
|
193
|
+
setSelected(el, selected) {
|
|
194
|
+
el.classList.toggle('selected', selected);
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/** Node width constant for layout calculations. */
|
|
198
|
+
NODE_WIDTH,
|
|
199
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// ── Workflow Editor — Selection Manager ─────────────────────────────────
|
|
2
|
+
// Handles node/edge selection with shift-toggle and keyboard shortcuts.
|
|
3
|
+
|
|
4
|
+
import { store } from '../state/store.js';
|
|
5
|
+
import { WorkflowModel } from '../models/workflow-model.js';
|
|
6
|
+
|
|
7
|
+
let _container = null;
|
|
8
|
+
let _onContainerClick = null;
|
|
9
|
+
let _onKeyDown = null;
|
|
10
|
+
let _unsubs = [];
|
|
11
|
+
|
|
12
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export const SelectionManager = {
|
|
15
|
+
/**
|
|
16
|
+
* Initialize selection handling on the canvas container.
|
|
17
|
+
* @param {HTMLElement} container - The canvas root element
|
|
18
|
+
*/
|
|
19
|
+
init(container) {
|
|
20
|
+
_container = container;
|
|
21
|
+
|
|
22
|
+
_onContainerClick = (e) => {
|
|
23
|
+
// Edge click — check if clicked on an edge hit area
|
|
24
|
+
const edgeHit = e.target.closest?.('.wfb-edge-hit');
|
|
25
|
+
if (edgeHit) {
|
|
26
|
+
const edgeGroup = edgeHit.closest('.wfb-edge');
|
|
27
|
+
const edgeId = edgeGroup?.dataset?.edgeId;
|
|
28
|
+
if (edgeId) {
|
|
29
|
+
this.selectEdge(edgeId, e.shiftKey);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Node click
|
|
35
|
+
const nodeEl = e.target.closest?.('.wfb-node');
|
|
36
|
+
if (nodeEl) {
|
|
37
|
+
const nodeId = nodeEl.dataset.nodeId;
|
|
38
|
+
if (nodeId) {
|
|
39
|
+
this.selectNode(nodeId, e.shiftKey);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Port click — don't deselect when clicking ports
|
|
45
|
+
const portEl = e.target.closest?.('.wfb-port');
|
|
46
|
+
if (portEl) return;
|
|
47
|
+
|
|
48
|
+
// Canvas background click — deselect all
|
|
49
|
+
if (e.target.closest?.('.wfb-canvas') && !e.target.closest?.('.wfb-node') && !e.target.closest?.('.wfb-edge')) {
|
|
50
|
+
this.deselectAll();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
_onKeyDown = (e) => {
|
|
55
|
+
// Escape — deselect all
|
|
56
|
+
if (e.key === 'Escape') {
|
|
57
|
+
this.deselectAll();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Delete or Backspace — delete selected (only when not in an input)
|
|
62
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && !isInputFocused()) {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
this.deleteSelected();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Ctrl/Cmd+A — select all nodes
|
|
69
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'a' && !isInputFocused()) {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
const wf = store.get('workflow');
|
|
72
|
+
if (wf?.nodes.length) {
|
|
73
|
+
store.set('selectedNodeIds', new Set(wf.nodes.map(n => n.id)));
|
|
74
|
+
store.set('selectedEdgeIds', new Set());
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
_container.addEventListener('click', _onContainerClick);
|
|
80
|
+
document.addEventListener('keydown', _onKeyDown);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove all event listeners.
|
|
85
|
+
*/
|
|
86
|
+
destroy() {
|
|
87
|
+
if (_container && _onContainerClick) {
|
|
88
|
+
_container.removeEventListener('click', _onContainerClick);
|
|
89
|
+
}
|
|
90
|
+
if (_onKeyDown) {
|
|
91
|
+
document.removeEventListener('keydown', _onKeyDown);
|
|
92
|
+
}
|
|
93
|
+
for (const unsub of _unsubs) unsub();
|
|
94
|
+
_unsubs = [];
|
|
95
|
+
_container = null;
|
|
96
|
+
_onContainerClick = null;
|
|
97
|
+
_onKeyDown = null;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Select a node. If additive (shift), toggle it in/out of the selection.
|
|
102
|
+
* @param {string} id
|
|
103
|
+
* @param {boolean} [additive=false]
|
|
104
|
+
*/
|
|
105
|
+
selectNode(id, additive = false) {
|
|
106
|
+
const sel = store.get('selectedNodeIds');
|
|
107
|
+
if (additive) {
|
|
108
|
+
const next = new Set(sel);
|
|
109
|
+
if (next.has(id)) {
|
|
110
|
+
next.delete(id);
|
|
111
|
+
} else {
|
|
112
|
+
next.add(id);
|
|
113
|
+
}
|
|
114
|
+
store.set('selectedNodeIds', next);
|
|
115
|
+
} else {
|
|
116
|
+
store.set('selectedNodeIds', new Set([id]));
|
|
117
|
+
store.set('selectedEdgeIds', new Set());
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Select an edge. If additive (shift), toggle it in/out of the selection.
|
|
123
|
+
* @param {string} id
|
|
124
|
+
* @param {boolean} [additive=false]
|
|
125
|
+
*/
|
|
126
|
+
selectEdge(id, additive = false) {
|
|
127
|
+
const sel = store.get('selectedEdgeIds');
|
|
128
|
+
if (additive) {
|
|
129
|
+
const next = new Set(sel);
|
|
130
|
+
if (next.has(id)) {
|
|
131
|
+
next.delete(id);
|
|
132
|
+
} else {
|
|
133
|
+
next.add(id);
|
|
134
|
+
}
|
|
135
|
+
store.set('selectedEdgeIds', next);
|
|
136
|
+
} else {
|
|
137
|
+
store.set('selectedEdgeIds', new Set([id]));
|
|
138
|
+
store.set('selectedNodeIds', new Set());
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Clear all selection.
|
|
144
|
+
*/
|
|
145
|
+
deselectAll() {
|
|
146
|
+
store.set('selectedNodeIds', new Set());
|
|
147
|
+
store.set('selectedEdgeIds', new Set());
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get the current set of selected node IDs.
|
|
152
|
+
* @returns {Set<string>}
|
|
153
|
+
*/
|
|
154
|
+
getSelectedNodes() {
|
|
155
|
+
return store.get('selectedNodeIds');
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get the current set of selected edge IDs.
|
|
160
|
+
* @returns {Set<string>}
|
|
161
|
+
*/
|
|
162
|
+
getSelectedEdges() {
|
|
163
|
+
return store.get('selectedEdgeIds');
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Delete all selected nodes and edges.
|
|
168
|
+
*/
|
|
169
|
+
deleteSelected() {
|
|
170
|
+
const nodeIds = store.get('selectedNodeIds');
|
|
171
|
+
const edgeIds = store.get('selectedEdgeIds');
|
|
172
|
+
|
|
173
|
+
// Delete edges first (removing nodes also removes their edges)
|
|
174
|
+
for (const edgeId of edgeIds) {
|
|
175
|
+
WorkflowModel.removeEdge(edgeId);
|
|
176
|
+
}
|
|
177
|
+
for (const nodeId of nodeIds) {
|
|
178
|
+
WorkflowModel.removeNode(nodeId);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.deselectAll();
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if an input/textarea/contenteditable is focused.
|
|
187
|
+
*/
|
|
188
|
+
function isInputFocused() {
|
|
189
|
+
const el = document.activeElement;
|
|
190
|
+
if (!el) return false;
|
|
191
|
+
const tag = el.tagName.toLowerCase();
|
|
192
|
+
return tag === 'input' || tag === 'textarea' || tag === 'select' || el.isContentEditable;
|
|
193
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<rect width="32" height="32" fill="#1c2128"/>
|
|
3
|
+
<rect width="32" height="32" fill="none" stroke="#7ee787" stroke-width="1.5"/>
|
|
4
|
+
<path d="M8 10h6a6 6 0 0 1 0 12H8V10z" fill="#7ee787"/>
|
|
5
|
+
<rect x="18" y="10" width="6" height="4" fill="#7ee787" opacity="0.6"/>
|
|
6
|
+
<rect x="18" y="18" width="6" height="4" fill="#7ee787" opacity="0.6"/>
|
|
7
|
+
</svg>
|