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,223 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { WorkflowStore } from './services/workflow-store.js';
|
|
4
|
+
import { getActiveProject } from '../../project-context.js';
|
|
5
|
+
import type { Workflow, WorkflowNode, WorkflowEdge, VariableDefinition } from './types.js';
|
|
6
|
+
import { jsonResult, errorResult, createDevglideMcpServer } from '../../packages/mcp-utils/src/index.js';
|
|
7
|
+
|
|
8
|
+
// ── Zod schemas for validating MCP input ─────────────────────────────────────
|
|
9
|
+
const nodeInputSchema = z.array(z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
type: z.string(),
|
|
12
|
+
label: z.string(),
|
|
13
|
+
config: z.record(z.unknown()),
|
|
14
|
+
position: z.object({ x: z.number(), y: z.number() }),
|
|
15
|
+
}).passthrough());
|
|
16
|
+
|
|
17
|
+
const edgeInputSchema = z.array(z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
source: z.string(),
|
|
20
|
+
target: z.string(),
|
|
21
|
+
}).passthrough());
|
|
22
|
+
|
|
23
|
+
const variableInputSchema = z.array(z.object({
|
|
24
|
+
name: z.string(),
|
|
25
|
+
type: z.enum(['string', 'number', 'boolean', 'json']),
|
|
26
|
+
}).passthrough());
|
|
27
|
+
|
|
28
|
+
export function createWorkflowMcpServer(): McpServer {
|
|
29
|
+
const server = createDevglideMcpServer(
|
|
30
|
+
'devglide-workflow',
|
|
31
|
+
'0.1.0',
|
|
32
|
+
'Workflow engine for creating and managing passive instruction templates for LLM integration',
|
|
33
|
+
{
|
|
34
|
+
instructions: [
|
|
35
|
+
'## Workflow — Usage Conventions',
|
|
36
|
+
'',
|
|
37
|
+
'### Workflows',
|
|
38
|
+
'- Workflows are passive instruction templates that LLMs pick up automatically.',
|
|
39
|
+
'- They are directed acyclic graphs (DAGs) of nodes connected by edges.',
|
|
40
|
+
'- Node types include triggers, shell commands, kanban ops, git ops, tests, log, file I/O, LLM, HTTP, decisions, loops, and sub-workflows.',
|
|
41
|
+
'',
|
|
42
|
+
'### Matching workflows to prompts',
|
|
43
|
+
'- **Always** call `workflow_match` with the user prompt before responding to check if a workflow applies.',
|
|
44
|
+
'- If a match is found, follow the returned workflow instructions exactly.',
|
|
45
|
+
'- If no match is found, proceed normally.',
|
|
46
|
+
'',
|
|
47
|
+
'### Instructions',
|
|
48
|
+
'- Use `workflow_get_instructions` to get compiled instructions from all enabled workflows.',
|
|
49
|
+
'- Use `workflow_toggle` to enable or disable a workflow.',
|
|
50
|
+
'',
|
|
51
|
+
'### Managing workflows',
|
|
52
|
+
'- Use `workflow_list` to see all workflows.',
|
|
53
|
+
'- Use `workflow_get` to get a full workflow by ID.',
|
|
54
|
+
'- Use `workflow_create` to create a new workflow.',
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const store = WorkflowStore.getInstance();
|
|
60
|
+
|
|
61
|
+
// ── 1. workflow_list ──────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
server.tool(
|
|
64
|
+
'workflow_list',
|
|
65
|
+
'List workflows visible to the current project context (project-scoped + global). Returns summaries with id, name, tags, node/edge counts. Pass projectId to scope to a specific project.',
|
|
66
|
+
{
|
|
67
|
+
projectId: z.string().optional().describe('Optional project ID to scope results. Defaults to the active project.'),
|
|
68
|
+
},
|
|
69
|
+
async ({ projectId }) => {
|
|
70
|
+
const scopeId = projectId ?? getActiveProject()?.id;
|
|
71
|
+
const workflows = await store.list(scopeId);
|
|
72
|
+
return jsonResult(workflows);
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// ── 2. workflow_get ───────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
server.tool(
|
|
79
|
+
'workflow_get',
|
|
80
|
+
'Get a workflow by ID. Returns the full workflow graph with nodes, edges, and variables.',
|
|
81
|
+
{ id: z.string().describe('Workflow ID') },
|
|
82
|
+
async ({ id }) => {
|
|
83
|
+
const workflow = await store.get(id);
|
|
84
|
+
if (!workflow) return errorResult('Workflow not found');
|
|
85
|
+
return jsonResult(workflow);
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// ── 3. workflow_create ────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
server.tool(
|
|
92
|
+
'workflow_create',
|
|
93
|
+
'Create a new workflow. Nodes and edges are passed as JSON strings.',
|
|
94
|
+
{
|
|
95
|
+
name: z.string().describe('Workflow name'),
|
|
96
|
+
description: z.string().optional().describe('Workflow description'),
|
|
97
|
+
nodes: z.string().describe('JSON array of WorkflowNode objects'),
|
|
98
|
+
edges: z.string().describe('JSON array of WorkflowEdge objects'),
|
|
99
|
+
variables: z.string().optional().describe('JSON array of VariableDefinition objects'),
|
|
100
|
+
tags: z.string().optional().describe('JSON array of tag strings'),
|
|
101
|
+
},
|
|
102
|
+
async ({ name, description, nodes, edges, variables, tags }) => {
|
|
103
|
+
let parsedNodes: WorkflowNode[];
|
|
104
|
+
let parsedEdges: WorkflowEdge[];
|
|
105
|
+
let parsedVariables: VariableDefinition[] = [];
|
|
106
|
+
let parsedTags: string[] = [];
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const raw = JSON.parse(nodes);
|
|
110
|
+
const result = nodeInputSchema.safeParse(raw);
|
|
111
|
+
if (!result.success) return errorResult(`Invalid nodes: ${result.error.issues[0]?.message}`);
|
|
112
|
+
parsedNodes = result.data as unknown as WorkflowNode[];
|
|
113
|
+
} catch {
|
|
114
|
+
return errorResult('Invalid JSON for nodes');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const raw = JSON.parse(edges);
|
|
119
|
+
const result = edgeInputSchema.safeParse(raw);
|
|
120
|
+
if (!result.success) return errorResult(`Invalid edges: ${result.error.issues[0]?.message}`);
|
|
121
|
+
parsedEdges = result.data as WorkflowEdge[];
|
|
122
|
+
} catch {
|
|
123
|
+
return errorResult('Invalid JSON for edges');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (variables) {
|
|
127
|
+
try {
|
|
128
|
+
const raw = JSON.parse(variables);
|
|
129
|
+
const result = variableInputSchema.safeParse(raw);
|
|
130
|
+
if (!result.success) return errorResult(`Invalid variables: ${result.error.issues[0]?.message}`);
|
|
131
|
+
parsedVariables = result.data as VariableDefinition[];
|
|
132
|
+
} catch {
|
|
133
|
+
return errorResult('Invalid JSON for variables');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (tags) {
|
|
138
|
+
try {
|
|
139
|
+
const raw = JSON.parse(tags);
|
|
140
|
+
const result = z.array(z.string()).safeParse(raw);
|
|
141
|
+
if (!result.success) return errorResult(`Invalid tags: ${result.error.issues[0]?.message}`);
|
|
142
|
+
parsedTags = result.data;
|
|
143
|
+
} catch {
|
|
144
|
+
return errorResult('Invalid JSON for tags');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const workflow = await store.save({
|
|
149
|
+
name,
|
|
150
|
+
description,
|
|
151
|
+
version: 1,
|
|
152
|
+
nodes: parsedNodes,
|
|
153
|
+
edges: parsedEdges,
|
|
154
|
+
variables: parsedVariables,
|
|
155
|
+
tags: parsedTags,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return jsonResult(workflow);
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// ── 4. workflow_get_instructions ──────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
server.tool(
|
|
165
|
+
'workflow_get_instructions',
|
|
166
|
+
'Get compiled instructions from all enabled workflows as markdown. Optionally filter by projectId.',
|
|
167
|
+
{
|
|
168
|
+
projectId: z.string().optional().describe('Optional project ID to filter workflows'),
|
|
169
|
+
},
|
|
170
|
+
async ({ projectId }) => {
|
|
171
|
+
const scopeId = projectId ?? getActiveProject()?.id;
|
|
172
|
+
const markdown = await store.getCompiledInstructions(scopeId);
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: 'text' as const, text: markdown }],
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// ── 5. workflow_match ───────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
server.tool(
|
|
182
|
+
'workflow_match',
|
|
183
|
+
'Match a user prompt against all enabled workflows. Returns only workflows whose name, description, tags, or node content match the prompt, ranked by relevance. Use this before responding to check if a workflow applies.',
|
|
184
|
+
{
|
|
185
|
+
prompt: z.string().describe('The user prompt to match against workflows'),
|
|
186
|
+
projectId: z.string().optional().describe('Optional project ID to scope matching'),
|
|
187
|
+
},
|
|
188
|
+
async ({ prompt, projectId }) => {
|
|
189
|
+
const scopeId = projectId ?? getActiveProject()?.id;
|
|
190
|
+
const result = await store.match(prompt, scopeId);
|
|
191
|
+
if (result.matches.length === 0) {
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: 'text' as const, text: 'No matching workflows found.' }],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return jsonResult(result);
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// ── 6. workflow_toggle ──────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
server.tool(
|
|
203
|
+
'workflow_toggle',
|
|
204
|
+
'Toggle a workflow enabled/disabled. If currently enabled (or undefined), disables it. If disabled, enables it.',
|
|
205
|
+
{
|
|
206
|
+
id: z.string().describe('Workflow ID'),
|
|
207
|
+
},
|
|
208
|
+
async ({ id }) => {
|
|
209
|
+
const workflow = await store.get(id);
|
|
210
|
+
if (!workflow) return errorResult('Workflow not found');
|
|
211
|
+
|
|
212
|
+
const newEnabled = workflow.enabled === false ? true : false;
|
|
213
|
+
const updated = await store.save({
|
|
214
|
+
...workflow,
|
|
215
|
+
enabled: newEnabled,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return jsonResult(updated);
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return server;
|
|
223
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../typescript/bin/tsc" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../typescript/bin/tsserver" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
|
|
4
|
+
case `uname` in
|
|
5
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
6
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
7
|
+
basedir=`cygpath -w "$basedir"`
|
|
8
|
+
fi
|
|
9
|
+
;;
|
|
10
|
+
esac
|
|
11
|
+
|
|
12
|
+
if [ -z "$NODE_PATH" ]; then
|
|
13
|
+
export NODE_PATH="/home/runner/_work/devglide/devglide/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/tsx@4.21.0/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/node_modules"
|
|
14
|
+
else
|
|
15
|
+
export NODE_PATH="/home/runner/_work/devglide/devglide/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/tsx@4.21.0/node_modules:/home/runner/_work/devglide/devglide/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
16
|
+
fi
|
|
17
|
+
if [ -x "$basedir/node" ]; then
|
|
18
|
+
exec "$basedir/node" "$basedir/../tsx/dist/cli.mjs" "$@"
|
|
19
|
+
else
|
|
20
|
+
exec node "$basedir/../tsx/dist/cli.mjs" "$@"
|
|
21
|
+
fi
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devglide/workflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Visual workflow builder with DAG execution engine",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/src/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsx watch src/index.ts",
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"start": "node dist/src/index.js",
|
|
11
|
+
"clean": "rm -rf dist",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"lint": "eslint ."
|
|
14
|
+
},
|
|
15
|
+
"private": true,
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@devglide/mcp-utils": "workspace:*",
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
19
|
+
"zod": "^3.25.49"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"tsx": "^4.19.4",
|
|
23
|
+
"typescript": "^5.8.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// ── Workflow Editor — Zoomable/Pannable Canvas ─────────────────────────
|
|
2
|
+
// Creates a transformable world element with SVG edge overlay and dot-grid background.
|
|
3
|
+
|
|
4
|
+
import { store } from '../state/store.js';
|
|
5
|
+
|
|
6
|
+
const MIN_ZOOM = 0.25;
|
|
7
|
+
const MAX_ZOOM = 3.0;
|
|
8
|
+
const ZOOM_STEP = 0.1;
|
|
9
|
+
|
|
10
|
+
let _container = null;
|
|
11
|
+
let _root = null;
|
|
12
|
+
let _world = null;
|
|
13
|
+
let _svg = null;
|
|
14
|
+
let _unsubs = [];
|
|
15
|
+
let _isPanning = false;
|
|
16
|
+
let _panStart = { x: 0, y: 0 };
|
|
17
|
+
let _spaceHeld = false;
|
|
18
|
+
|
|
19
|
+
function buildDOM() {
|
|
20
|
+
_root = document.createElement('div');
|
|
21
|
+
_root.className = 'wfb-canvas';
|
|
22
|
+
_root.style.cssText = `
|
|
23
|
+
position: relative;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: 100%;
|
|
27
|
+
background-color: var(--df-color-bg-base);
|
|
28
|
+
cursor: default;
|
|
29
|
+
`;
|
|
30
|
+
updateGridBackground();
|
|
31
|
+
|
|
32
|
+
// SVG overlay for edges
|
|
33
|
+
_svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
34
|
+
_svg.setAttribute('class', 'wfb-svg-layer');
|
|
35
|
+
_svg.style.cssText = `
|
|
36
|
+
position: absolute;
|
|
37
|
+
top: 0;
|
|
38
|
+
left: 0;
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 100%;
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
overflow: visible;
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
// Arrowhead marker definition
|
|
46
|
+
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
47
|
+
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
|
48
|
+
marker.setAttribute('id', 'wfb-arrowhead');
|
|
49
|
+
marker.setAttribute('viewBox', '0 0 10 7');
|
|
50
|
+
marker.setAttribute('refX', '10');
|
|
51
|
+
marker.setAttribute('refY', '3.5');
|
|
52
|
+
marker.setAttribute('markerWidth', '8');
|
|
53
|
+
marker.setAttribute('markerHeight', '6');
|
|
54
|
+
marker.setAttribute('orient', 'auto-start-reverse');
|
|
55
|
+
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
|
56
|
+
arrow.setAttribute('points', '0 0, 10 3.5, 0 7');
|
|
57
|
+
arrow.setAttribute('fill', 'var(--df-color-border-default)');
|
|
58
|
+
marker.appendChild(arrow);
|
|
59
|
+
defs.appendChild(marker);
|
|
60
|
+
|
|
61
|
+
// Selected arrowhead
|
|
62
|
+
const markerSel = marker.cloneNode(true);
|
|
63
|
+
markerSel.setAttribute('id', 'wfb-arrowhead-selected');
|
|
64
|
+
markerSel.querySelector('polygon').setAttribute('fill', 'var(--df-color-accent-default)');
|
|
65
|
+
defs.appendChild(markerSel);
|
|
66
|
+
|
|
67
|
+
_svg.appendChild(defs);
|
|
68
|
+
|
|
69
|
+
// SVG group that transforms with the world
|
|
70
|
+
const svgWorld = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
71
|
+
svgWorld.setAttribute('class', 'wfb-svg-world');
|
|
72
|
+
_svg.appendChild(svgWorld);
|
|
73
|
+
|
|
74
|
+
// World element (nodes go here, transforms for pan/zoom)
|
|
75
|
+
_world = document.createElement('div');
|
|
76
|
+
_world.className = 'wfb-world';
|
|
77
|
+
_world.style.cssText = `
|
|
78
|
+
position: absolute;
|
|
79
|
+
top: 0;
|
|
80
|
+
left: 0;
|
|
81
|
+
transform-origin: 0 0;
|
|
82
|
+
will-change: transform;
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
_root.appendChild(_svg);
|
|
86
|
+
_root.appendChild(_world);
|
|
87
|
+
return _root;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function updateGridBackground() {
|
|
91
|
+
if (!_root) return;
|
|
92
|
+
const zoom = store.get('zoom');
|
|
93
|
+
const panX = store.get('panX');
|
|
94
|
+
const panY = store.get('panY');
|
|
95
|
+
const size = 20 * zoom;
|
|
96
|
+
const ox = (panX % (20)) * zoom;
|
|
97
|
+
const oy = (panY % (20)) * zoom;
|
|
98
|
+
_root.style.backgroundImage = `radial-gradient(circle, var(--df-color-border-default) 1px, transparent 1px)`;
|
|
99
|
+
_root.style.backgroundSize = `${size}px ${size}px`;
|
|
100
|
+
_root.style.backgroundPosition = `${ox}px ${oy}px`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function updateTransform() {
|
|
104
|
+
if (!_world || !_svg) return;
|
|
105
|
+
const zoom = store.get('zoom');
|
|
106
|
+
const panX = store.get('panX');
|
|
107
|
+
const panY = store.get('panY');
|
|
108
|
+
_world.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`;
|
|
109
|
+
|
|
110
|
+
// Update SVG world group to match
|
|
111
|
+
const svgWorld = _svg.querySelector('.wfb-svg-world');
|
|
112
|
+
if (svgWorld) {
|
|
113
|
+
svgWorld.setAttribute('transform', `translate(${panX}, ${panY}) scale(${zoom})`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
updateGridBackground();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Event handlers ──────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
function onWheel(e) {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
const zoom = store.get('zoom');
|
|
124
|
+
const delta = -Math.sign(e.deltaY) * ZOOM_STEP;
|
|
125
|
+
const newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom + delta));
|
|
126
|
+
if (newZoom === zoom) return;
|
|
127
|
+
|
|
128
|
+
// Zoom toward cursor
|
|
129
|
+
const rect = _root.getBoundingClientRect();
|
|
130
|
+
const cx = e.clientX - rect.left;
|
|
131
|
+
const cy = e.clientY - rect.top;
|
|
132
|
+
const panX = store.get('panX');
|
|
133
|
+
const panY = store.get('panY');
|
|
134
|
+
|
|
135
|
+
const scale = newZoom / zoom;
|
|
136
|
+
const newPanX = cx - (cx - panX) * scale;
|
|
137
|
+
const newPanY = cy - (cy - panY) * scale;
|
|
138
|
+
|
|
139
|
+
store.set('zoom', newZoom);
|
|
140
|
+
store.set('panX', newPanX);
|
|
141
|
+
store.set('panY', newPanY);
|
|
142
|
+
updateTransform();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function onPointerDown(e) {
|
|
146
|
+
// Middle mouse button or space+left for panning
|
|
147
|
+
if (e.button === 1 || (_spaceHeld && e.button === 0)) {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
_isPanning = true;
|
|
150
|
+
_panStart = { x: e.clientX - store.get('panX'), y: e.clientY - store.get('panY') };
|
|
151
|
+
_root.style.cursor = 'grabbing';
|
|
152
|
+
_root.setPointerCapture(e.pointerId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function onPointerMove(e) {
|
|
157
|
+
if (!_isPanning) return;
|
|
158
|
+
const panX = e.clientX - _panStart.x;
|
|
159
|
+
const panY = e.clientY - _panStart.y;
|
|
160
|
+
store.set('panX', panX);
|
|
161
|
+
store.set('panY', panY);
|
|
162
|
+
updateTransform();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function onPointerUp(e) {
|
|
166
|
+
if (_isPanning) {
|
|
167
|
+
_isPanning = false;
|
|
168
|
+
_root.style.cursor = _spaceHeld ? 'grab' : 'default';
|
|
169
|
+
_root.releasePointerCapture(e.pointerId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function onKeyDown(e) {
|
|
174
|
+
if (e.code === 'Space' && !e.repeat && document.activeElement === document.body) {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
_spaceHeld = true;
|
|
177
|
+
if (_root) _root.style.cursor = 'grab';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function onKeyUp(e) {
|
|
182
|
+
if (e.code === 'Space') {
|
|
183
|
+
_spaceHeld = false;
|
|
184
|
+
if (_root && !_isPanning) _root.style.cursor = 'default';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export const Canvas = {
|
|
191
|
+
/**
|
|
192
|
+
* Mount the canvas into a container.
|
|
193
|
+
* @param {HTMLElement} container
|
|
194
|
+
*/
|
|
195
|
+
mount(container) {
|
|
196
|
+
_container = container;
|
|
197
|
+
const root = buildDOM();
|
|
198
|
+
container.appendChild(root);
|
|
199
|
+
|
|
200
|
+
// Attach event listeners
|
|
201
|
+
_root.addEventListener('wheel', onWheel, { passive: false });
|
|
202
|
+
_root.addEventListener('pointerdown', onPointerDown);
|
|
203
|
+
_root.addEventListener('pointermove', onPointerMove);
|
|
204
|
+
_root.addEventListener('pointerup', onPointerUp);
|
|
205
|
+
_root.addEventListener('pointercancel', onPointerUp);
|
|
206
|
+
document.addEventListener('keydown', onKeyDown);
|
|
207
|
+
document.addEventListener('keyup', onKeyUp);
|
|
208
|
+
|
|
209
|
+
// Subscribe to store changes
|
|
210
|
+
_unsubs.push(store.on('zoom', () => updateTransform()));
|
|
211
|
+
_unsubs.push(store.on('panX', () => updateTransform()));
|
|
212
|
+
_unsubs.push(store.on('panY', () => updateTransform()));
|
|
213
|
+
|
|
214
|
+
updateTransform();
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Remove listeners and clear DOM.
|
|
219
|
+
*/
|
|
220
|
+
unmount() {
|
|
221
|
+
if (_root) {
|
|
222
|
+
_root.removeEventListener('wheel', onWheel);
|
|
223
|
+
_root.removeEventListener('pointerdown', onPointerDown);
|
|
224
|
+
_root.removeEventListener('pointermove', onPointerMove);
|
|
225
|
+
_root.removeEventListener('pointerup', onPointerUp);
|
|
226
|
+
_root.removeEventListener('pointercancel', onPointerUp);
|
|
227
|
+
}
|
|
228
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
229
|
+
document.removeEventListener('keyup', onKeyUp);
|
|
230
|
+
|
|
231
|
+
for (const unsub of _unsubs) unsub();
|
|
232
|
+
_unsubs = [];
|
|
233
|
+
|
|
234
|
+
if (_root && _container) {
|
|
235
|
+
_container.removeChild(_root);
|
|
236
|
+
}
|
|
237
|
+
_root = null;
|
|
238
|
+
_world = null;
|
|
239
|
+
_svg = null;
|
|
240
|
+
_container = null;
|
|
241
|
+
_isPanning = false;
|
|
242
|
+
_spaceHeld = false;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Convert world coordinates to screen coordinates.
|
|
247
|
+
* @param {number} x
|
|
248
|
+
* @param {number} y
|
|
249
|
+
* @returns {{ x: number, y: number }}
|
|
250
|
+
*/
|
|
251
|
+
worldToScreen(x, y) {
|
|
252
|
+
const zoom = store.get('zoom');
|
|
253
|
+
const panX = store.get('panX');
|
|
254
|
+
const panY = store.get('panY');
|
|
255
|
+
return {
|
|
256
|
+
x: x * zoom + panX,
|
|
257
|
+
y: y * zoom + panY,
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Convert screen coordinates to world coordinates (for drops).
|
|
263
|
+
* @param {number} sx
|
|
264
|
+
* @param {number} sy
|
|
265
|
+
* @returns {{ x: number, y: number }}
|
|
266
|
+
*/
|
|
267
|
+
screenToWorld(sx, sy) {
|
|
268
|
+
const zoom = store.get('zoom');
|
|
269
|
+
const panX = store.get('panX');
|
|
270
|
+
const panY = store.get('panY');
|
|
271
|
+
const rect = _root?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
|
272
|
+
return {
|
|
273
|
+
x: (sx - rect.left - panX) / zoom,
|
|
274
|
+
y: (sy - rect.top - panY) / zoom,
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Returns the world element where nodes are placed.
|
|
280
|
+
* @returns {HTMLElement|null}
|
|
281
|
+
*/
|
|
282
|
+
getWorldElement() {
|
|
283
|
+
return _world;
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Returns the SVG world group where edges are drawn.
|
|
288
|
+
* @returns {SVGGElement|null}
|
|
289
|
+
*/
|
|
290
|
+
getSvgElement() {
|
|
291
|
+
return _svg?.querySelector('.wfb-svg-world') ?? null;
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Returns the root canvas element.
|
|
296
|
+
* @returns {HTMLElement|null}
|
|
297
|
+
*/
|
|
298
|
+
getRootElement() {
|
|
299
|
+
return _root;
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Set the zoom level, optionally zooming toward a screen-space point.
|
|
304
|
+
* @param {number} zoom
|
|
305
|
+
* @param {number} [cx] - Screen X to zoom toward
|
|
306
|
+
* @param {number} [cy] - Screen Y to zoom toward
|
|
307
|
+
*/
|
|
308
|
+
setZoom(zoom, cx, cy) {
|
|
309
|
+
const clamped = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
|
|
310
|
+
if (cx != null && cy != null) {
|
|
311
|
+
const oldZoom = store.get('zoom');
|
|
312
|
+
const panX = store.get('panX');
|
|
313
|
+
const panY = store.get('panY');
|
|
314
|
+
const rect = _root?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
|
315
|
+
const px = cx - rect.left;
|
|
316
|
+
const py = cy - rect.top;
|
|
317
|
+
const scale = clamped / oldZoom;
|
|
318
|
+
store.set('panX', px - (px - panX) * scale);
|
|
319
|
+
store.set('panY', py - (py - panY) * scale);
|
|
320
|
+
}
|
|
321
|
+
store.set('zoom', clamped);
|
|
322
|
+
updateTransform();
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Calculate bounds and set zoom/pan to fit all given nodes with padding.
|
|
327
|
+
* @param {Array<{ position: { x: number, y: number } }>} nodes
|
|
328
|
+
*/
|
|
329
|
+
fitToView(nodes) {
|
|
330
|
+
if (!_root || !nodes.length) return;
|
|
331
|
+
|
|
332
|
+
const NODE_WIDTH = 220;
|
|
333
|
+
const NODE_HEIGHT = 80;
|
|
334
|
+
const PADDING = 50;
|
|
335
|
+
|
|
336
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
337
|
+
for (const n of nodes) {
|
|
338
|
+
minX = Math.min(minX, n.position.x);
|
|
339
|
+
minY = Math.min(minY, n.position.y);
|
|
340
|
+
maxX = Math.max(maxX, n.position.x + NODE_WIDTH);
|
|
341
|
+
maxY = Math.max(maxY, n.position.y + NODE_HEIGHT);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const boundsW = maxX - minX + PADDING * 2;
|
|
345
|
+
const boundsH = maxY - minY + PADDING * 2;
|
|
346
|
+
const rect = _root.getBoundingClientRect();
|
|
347
|
+
const canvasW = rect.width || 800;
|
|
348
|
+
const canvasH = rect.height || 600;
|
|
349
|
+
|
|
350
|
+
const zoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, Math.min(canvasW / boundsW, canvasH / boundsH)));
|
|
351
|
+
const panX = (canvasW - boundsW * zoom) / 2 - (minX - PADDING) * zoom;
|
|
352
|
+
const panY = (canvasH - boundsH * zoom) / 2 - (minY - PADDING) * zoom;
|
|
353
|
+
|
|
354
|
+
store.set('zoom', zoom);
|
|
355
|
+
store.set('panX', panX);
|
|
356
|
+
store.set('panY', panY);
|
|
357
|
+
updateTransform();
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Force-refresh the canvas transform.
|
|
362
|
+
*/
|
|
363
|
+
refresh() {
|
|
364
|
+
updateTransform();
|
|
365
|
+
},
|
|
366
|
+
};
|