mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 12:09:53 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d1a020b35 | ||
|
|
8d690503d4 | ||
|
|
4b4f36e374 | ||
|
|
a9091e96f1 | ||
|
|
213b672e92 |
@@ -0,0 +1,55 @@
|
|||||||
|
# Issues labelled "wait for confrim" are waiting for the reporter. After 15
|
||||||
|
# days without activity the issue gets a reminder and the "no response"
|
||||||
|
# label; after 15 more days it is closed. A reply from the reporter removes
|
||||||
|
# both labels, so the issue shows up as needing the maintainer again.
|
||||||
|
name: Wait for confirmation
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "17 3 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
remind-and-close:
|
||||||
|
if: github.event_name != 'issue_comment'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
only-issue-labels: "wait for confrim"
|
||||||
|
days-before-issue-stale: 15
|
||||||
|
days-before-issue-close: 15
|
||||||
|
days-before-pr-stale: -1
|
||||||
|
days-before-pr-close: -1
|
||||||
|
stale-issue-label: "no response"
|
||||||
|
stale-issue-message: >-
|
||||||
|
Still there? We asked for more information 15 days ago. A reply
|
||||||
|
would help; if we hear nothing in another 15 days this issue will
|
||||||
|
be closed. It can be reopened any time.
|
||||||
|
close-issue-message: >-
|
||||||
|
No reply in 30 days, so this issue is closed for now. If the
|
||||||
|
problem is still there, reply or reopen it.
|
||||||
|
close-issue-reason: not_planned
|
||||||
|
remove-stale-when-updated: true
|
||||||
|
|
||||||
|
reporter-replied:
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'issue_comment' &&
|
||||||
|
contains(github.event.issue.labels.*.name, 'wait for confrim') &&
|
||||||
|
github.event.comment.user.type != 'Bot' &&
|
||||||
|
!contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: The reporter answered, so the issue is the maintainer's again
|
||||||
|
run: >-
|
||||||
|
gh issue edit "$NUMBER" --repo "$REPO"
|
||||||
|
--remove-label "wait for confrim" --remove-label "no response"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
Generated
+36
-36
@@ -75,7 +75,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||||
"@biomejs/biome": "2.5.7",
|
"@biomejs/biome": "2.5.15",
|
||||||
"@playwright/test": "^1.57.0",
|
"@playwright/test": "^1.57.0",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
@@ -2093,9 +2093,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/biome": {
|
"node_modules/@biomejs/biome": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.15.tgz",
|
||||||
"integrity": "sha512-zr8K/DcY5tYsQOQwqMJ0AWElo6QgmgNI7idXgXLhevVszlt8RGVpesEJPqx3ThazLaOwjJ5Y8fz3BtH5fGZNsw==",
|
"integrity": "sha512-WZTW4slm/pdkh92K6t/3aEN++44JD1PQ7squ7RCMLI1flHGl43DVOIGXvCjHiZgBMO1V4uYxoFNtqrIA4qGuIQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -2109,20 +2109,20 @@
|
|||||||
"url": "https://opencollective.com/biome"
|
"url": "https://opencollective.com/biome"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@biomejs/cli-darwin-arm64": "2.5.7",
|
"@biomejs/cli-darwin-arm64": "2.5.15",
|
||||||
"@biomejs/cli-darwin-x64": "2.5.7",
|
"@biomejs/cli-darwin-x64": "2.5.15",
|
||||||
"@biomejs/cli-linux-arm64": "2.5.7",
|
"@biomejs/cli-linux-arm64": "2.5.15",
|
||||||
"@biomejs/cli-linux-arm64-musl": "2.5.7",
|
"@biomejs/cli-linux-arm64-musl": "2.5.15",
|
||||||
"@biomejs/cli-linux-x64": "2.5.7",
|
"@biomejs/cli-linux-x64": "2.5.15",
|
||||||
"@biomejs/cli-linux-x64-musl": "2.5.7",
|
"@biomejs/cli-linux-x64-musl": "2.5.15",
|
||||||
"@biomejs/cli-win32-arm64": "2.5.7",
|
"@biomejs/cli-win32-arm64": "2.5.15",
|
||||||
"@biomejs/cli-win32-x64": "2.5.7"
|
"@biomejs/cli-win32-x64": "2.5.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-arm64": {
|
"node_modules/@biomejs/cli-darwin-arm64": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.15.tgz",
|
||||||
"integrity": "sha512-vxo/Ls3/PYdQWyLhYYcgMOCzQypAjcY+iihS8M0wW03l16TCLW4zqZzGo75gm1VdCMj38hTVZ31KBWrZ4G9dJw==",
|
"integrity": "sha512-BZVzFhJ/mUvTLMYbc9x6el0o2Uv5zP7bACYyhFb6fSc1giroXm7PL0a5KqMJ9F+/BnkYRjagD/ROSDVOZ6eapg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2137,9 +2137,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-x64": {
|
"node_modules/@biomejs/cli-darwin-x64": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.15.tgz",
|
||||||
"integrity": "sha512-Cd3Ga61amT/Yl/0x8elP5hhGYaFy4bw6WuysTgf7oo8TA5tJ5A1k+DkVoJ2BHbTVil51gTX9VPzArnrlLJ3Kyg==",
|
"integrity": "sha512-V5Kw63V+fVGNFhFrizDxMbGXAzZCh8kYzJpy3GI6gTapWDg7bZK9oYC9CbVgQSEdTnupz0U5Efz8Ev+5vdBBXw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2154,9 +2154,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64": {
|
"node_modules/@biomejs/cli-linux-arm64": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.15.tgz",
|
||||||
"integrity": "sha512-rR2QE0yF2GYSuYuKIa7pKvODGJqnOH+2eDREAM8wV+mWKSkMQKdAp4zXEZfTaxY8PMoNONnpgSWcBCyLDPDOKg==",
|
"integrity": "sha512-XaG7P7eeSLYETD3K9grfB0mQmpLQZjygdbsxekWBJAg4am79fGAtRpfRyNnTvXB2fZFNnJZG3nXm7HBat0+VUw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2174,9 +2174,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.15.tgz",
|
||||||
"integrity": "sha512-xPI5yB6XlpDbNkS+bm1t42olw5c4l3UrlOmLg7KtLJvjvkNF/1V4tnUgfkylGIeb3u/T+BzMGYqgQhzjAoJzuQ==",
|
"integrity": "sha512-tGzZUTcJCV7tj5Adh/Gn6nR4MycqA3iohq2LEcrarAgRBkU0h0OKd2UzCRkOsOYOuUGaUJb4YmqRqyy2feGGSw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2194,9 +2194,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64": {
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.15.tgz",
|
||||||
"integrity": "sha512-FQgqJhscrqJUFptGaRSUJWlXAExwWcDwLuK49dvKfkQ1bB5SEEyFssnsxQY83Xm6jR0EbbX3+8+D5bfvYqUG2Q==",
|
"integrity": "sha512-xE4iEW/3LqlYj9GFgGrFsFSH51dEEpUbYWBfeOv0q87WkUDxK2o/HhreSb7qMJqck70RVM6Lg96hgedcAYcOkA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2214,9 +2214,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
"node_modules/@biomejs/cli-linux-x64-musl": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.15.tgz",
|
||||||
"integrity": "sha512-rE5VZi+qtmPgQH+l7jVxYoZ18b/TiHEhulhMpjmCZH1PltSbjRcxNWywC3HZ9tYottG7ORkeTtoscBilKSBm0g==",
|
"integrity": "sha512-IlxUcyxilVGPsE008x13pWdkTbU3nQpP2i9b5UrbOYj6goBkKsRX1XB7yJLcV+O1H9LBchIIm4adOaVLZBX0ZQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2234,9 +2234,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-arm64": {
|
"node_modules/@biomejs/cli-win32-arm64": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.15.tgz",
|
||||||
"integrity": "sha512-Oq4x0CCwP4jirrcTywXs5kOGZ4v5vuEP+gWrbtjApOA2CL9F3F9GlIdQIci8AKSCa/zURanMRpX/4wQ7Am6hHg==",
|
"integrity": "sha512-2kPKzhNlm8C+Ru3GcO0Me2ODkCBLrVOUNuyi84RJyWDVKAA4+Kjj3jMjL938lF6BRfvltC4vB2nPY9SvN51Ovg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2251,9 +2251,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-win32-x64": {
|
"node_modules/@biomejs/cli-win32-x64": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.15.tgz",
|
||||||
"integrity": "sha512-V+0wu/nrj2S+MhP4EQ0uHNolP0IALEsz45pg0WoKkHfDeh0+ItHwP/p7bX5RPoMOl9NkpHYWdYPhIcy2mACHvQ==",
|
"integrity": "sha512-yAzh4UqEImV6Hcy0zjUqNfJAsUhoD64FBQCdTG8Md/Z2wQeVP/ZmJ3XRYAs1g8J9fjTFyl2fCtc5FB5til47+g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|||||||
+1
-1
@@ -110,7 +110,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||||
"@biomejs/biome": "2.5.7",
|
"@biomejs/biome": "2.5.15",
|
||||||
"@playwright/test": "^1.57.0",
|
"@playwright/test": "^1.57.0",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ Use the standard MCP configuration with:
|
|||||||
| `get_drawing_guide` | Return the drawing rules again, for example after a long conversation was compacted |
|
| `get_drawing_guide` | Return the drawing rules again, for example after a long conversation was compacted |
|
||||||
| `get_shape_library` | Return the shapes and icon styles of a library such as `aws4`, `azure2`, or `kubernetes` |
|
| `get_shape_library` | Return the shapes and icon styles of a library such as `aws4`, `azure2`, or `kubernetes` |
|
||||||
| `create_new_diagram` | Create a new diagram from XML; a plain list of `mxCell` elements is enough |
|
| `create_new_diagram` | Create a new diagram from XML; a plain list of `mxCell` elements is enough |
|
||||||
| `load_diagram` | Load a `.drawio` file from disk into the session (handles compressed files) |
|
| `load_diagram` | Load a `.drawio` file into the session, from a `path` on disk or from its `xml` content (handles compressed files) |
|
||||||
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells); all or nothing |
|
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells); all or nothing |
|
||||||
| `get_diagram` | Get the current diagram XML, including your edits in the browser |
|
| `get_diagram` | Get the current diagram XML, including your edits in the browser |
|
||||||
| `screenshot_diagram` | Return a PNG of a page so the AI can check the rendered diagram |
|
| `screenshot_diagram` | Return a PNG of a page so the AI can check the rendered diagram |
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
export const DRAWING_GUIDE = `# Draw.io drawing guide
|
export const DRAWING_GUIDE = `# Draw.io drawing guide
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
- create_new_diagram draws a new diagram and REPLACES the whole document. add_page adds another tab. edit_diagram changes cells of an existing page. load_diagram opens a .drawio file (the server reads the file itself). get_diagram returns the current XML, including the user's manual edits. export_diagram saves to a file.
|
- create_new_diagram draws a new diagram and REPLACES the whole document. add_page adds another tab. edit_diagram changes cells of an existing page. load_diagram opens a .drawio file (the server reads the file itself, or takes the file's content as its 'xml' argument when you already have it in hand). get_diagram returns the current XML, including the user's manual edits. export_diagram saves to a file.
|
||||||
- Before drawing, describe your layout plan in 2-3 sentences, so shapes do not overlap and edges do not cross shapes.
|
- Before drawing, describe your layout plan in 2-3 sentences, so shapes do not overlap and edges do not cross shapes.
|
||||||
- Send XML only through tool calls, never in chat text. Never draw a box just to send the user a message.
|
- Send XML only through tool calls, never in chat text. Never draw a box just to send the user a message.
|
||||||
- Before using any icon library (AWS, Azure, GCP, Kubernetes, Cisco, BPMN, Material Design, web icons...), call get_shape_library and use the exact style names it returns. NEVER guess icon style names. For AWS, use the AWS 2025 icons (library aws4).
|
- Before using any icon library (AWS, Azure, GCP, Kubernetes, Cisco, BPMN, Material Design, web icons...), call get_shape_library and use the exact style names it returns. NEVER guess icon style names. For AWS, use the AWS 2025 icons (library aws4).
|
||||||
|
|||||||
@@ -455,21 +455,54 @@ registerWriteTool(
|
|||||||
{
|
{
|
||||||
title: "Load .drawio file",
|
title: "Load .drawio file",
|
||||||
description:
|
description:
|
||||||
"Load a .drawio file from disk into the current session, REPLACING the entire diagram (all pages). " +
|
"Load a .drawio diagram into the current session, REPLACING the entire diagram (all pages). " +
|
||||||
"The server reads the file directly — you do NOT need to read the file yourself or pass its XML through create_new_diagram. " +
|
"Provide ONE of two mutually exclusive sources: 'path' (the server reads the file from disk — you do NOT need to read the file yourself or pass its XML through create_new_diagram) " +
|
||||||
"Handles both plain-XML and draw.io's compressed save format.\n\n" +
|
"or 'xml' (the raw file content you already have — from another tool, a repository read, or an API response — so no temporary file needs to be written first). " +
|
||||||
"After loading, call get_diagram before edit_diagram — you haven't seen the file's cell IDs or structure yet.",
|
"Both accept plain XML and draw.io's compressed save format.\n\n" +
|
||||||
|
"After loading from 'path' (or from compressed 'xml'), call get_diagram before edit_diagram — you haven't seen the file's cell IDs yet. " +
|
||||||
|
"Plain-XML 'xml' content you supplied yourself is already known and can be edited immediately.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
path: z
|
path: z
|
||||||
.string()
|
.string()
|
||||||
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Absolute path to the .drawio file to load (e.g. /Users/me/diagram.drawio or ~/diagram.drawio). Relative paths resolve against the MCP server's working directory, which is often not your project.",
|
"Path to the .drawio file to load (e.g. /Users/me/diagram.drawio or ~/diagram.drawio). Relative paths resolve against the MCP server's working directory, which is often not your project. Mutually exclusive with 'xml'.",
|
||||||
|
),
|
||||||
|
xml: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(
|
||||||
|
"Raw .drawio file content: a plain <mxfile>/<mxGraphModel>, or draw.io's compressed save format. Use when the content is already in hand (another tool's output, a repository read, an API response). Mutually exclusive with 'path'.",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
annotations: { openWorldHint: false },
|
annotations: { openWorldHint: false },
|
||||||
},
|
},
|
||||||
async ({ path }) => {
|
async ({ path, xml: inlineXml }) => {
|
||||||
try {
|
try {
|
||||||
|
// Argument validation comes before the session check: a bad
|
||||||
|
// argument is a caller error and should be reported as such.
|
||||||
|
if (path !== undefined && inlineXml !== undefined) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: Provide either 'path' or 'xml', not both.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (path === undefined && inlineXml === undefined) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Error: Provide either 'path' (a .drawio file to read) or 'xml' (the file's content).",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!currentSession) {
|
if (!currentSession) {
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
@@ -482,11 +515,17 @@ registerWriteTool(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Exactly one of path/xml is present (validated above).
|
||||||
|
let content = ""
|
||||||
|
let sourceLabel = ""
|
||||||
|
if (inlineXml !== undefined) {
|
||||||
|
content = inlineXml
|
||||||
|
sourceLabel = "inline XML"
|
||||||
|
} else if (path !== undefined) {
|
||||||
const fs = await import("node:fs/promises")
|
const fs = await import("node:fs/promises")
|
||||||
const nodePath = await import("node:path")
|
const nodePath = await import("node:path")
|
||||||
const absolutePath = nodePath.resolve(expandHome(path))
|
const absolutePath = nodePath.resolve(expandHome(path))
|
||||||
|
|
||||||
let content: string
|
|
||||||
try {
|
try {
|
||||||
// A pipe or device could be read forever, and the other
|
// A pipe or device could be read forever, and the other
|
||||||
// write tools wait for this one
|
// write tools wait for this one
|
||||||
@@ -506,6 +545,8 @@ registerWriteTool(
|
|||||||
isError: true,
|
isError: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sourceLabel = absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
const loaded = parseDrawioFileContent(content)
|
const loaded = parseDrawioFileContent(content)
|
||||||
if (!loaded.ok) {
|
if (!loaded.ok) {
|
||||||
@@ -517,7 +558,7 @@ registerWriteTool(
|
|||||||
const xml = loaded.xml
|
const xml = loaded.xml
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`Loading diagram from ${absolutePath} (${xml.length} chars)`,
|
`Loading diagram from ${sourceLabel} (${xml.length} chars)`,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Save the user's current state before replacing (same flow as
|
// Save the user's current state before replacing (same flow as
|
||||||
@@ -537,10 +578,17 @@ registerWriteTool(
|
|||||||
currentSession.xml = xml
|
currentSession.xml = xml
|
||||||
currentSession.version++
|
currentSession.version++
|
||||||
setState(currentSession.id, xml)
|
setState(currentSession.id, xml)
|
||||||
// Deliberately NOT marking the loaded XML as seen: the model only
|
// Edit-gate semantics by source:
|
||||||
// supplied a path, so it doesn't know the file's cell IDs. The
|
// - 'path': the model only supplied a path, so it doesn't know
|
||||||
// edit gate will require one get_diagram before edits.
|
// the file's cell IDs — keep the gate (one get_diagram first).
|
||||||
currentSession.lastSeenXml = ""
|
// - plain 'xml': the model supplied the exact content, same
|
||||||
|
// rationale as create_new_diagram — record it as seen.
|
||||||
|
// - compressed 'xml': the session now holds the decompressed
|
||||||
|
// form, which the model cannot derive from the compressed
|
||||||
|
// input — keep the gate.
|
||||||
|
const markSeen =
|
||||||
|
inlineXml !== undefined && !loaded.hadCompressedPages
|
||||||
|
currentSession.lastSeenXml = markSeen ? xml : ""
|
||||||
|
|
||||||
addHistory(currentSession.id, xml, "")
|
addHistory(currentSession.id, xml, "")
|
||||||
|
|
||||||
@@ -551,13 +599,17 @@ registerWriteTool(
|
|||||||
? `Pages (${pages.length}): ${pages.map((p) => `[${p.index}] id=${p.id} name="${p.name}" cells=${p.cellCount}`).join(" | ")}`
|
? `Pages (${pages.length}): ${pages.map((p) => `[${p.index}] id=${p.id} name="${p.name}" cells=${p.cellCount}`).join(" | ")}`
|
||||||
: "no pages parsed"
|
: "no pages parsed"
|
||||||
|
|
||||||
log.info(`Diagram loaded from file (${pageSummary})`)
|
log.info(`Diagram loaded (${pageSummary})`)
|
||||||
|
|
||||||
|
const gateHint = markSeen
|
||||||
|
? ""
|
||||||
|
: "\n\nCall get_diagram before edit_diagram — you haven't seen this file's cell IDs yet."
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `Diagram loaded from ${absolutePath}!\n\nThe diagram is now visible in your browser.\n\n${pageSummary}\n\nCall get_diagram before edit_diagram — you haven't seen this file's cell IDs yet.`,
|
text: `Diagram loaded from ${sourceLabel}!\n\nThe diagram is now visible in your browser.\n\n${pageSummary}${gateHint}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -572,7 +624,6 @@ registerWriteTool(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Tool: edit_diagram
|
// Tool: edit_diagram
|
||||||
registerWriteTool(
|
registerWriteTool(
|
||||||
"edit_diagram",
|
"edit_diagram",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
import { getXmlSyntaxError } from "./xml-syntax.ts"
|
import { getXmlSyntaxError } from "./xml-syntax.ts"
|
||||||
|
|
||||||
export type LoadResult =
|
export type LoadResult =
|
||||||
| { ok: true; xml: string }
|
| { ok: true; xml: string; hadCompressedPages: boolean }
|
||||||
| { ok: false; error: string }
|
| { ok: false; error: string }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,5 +101,9 @@ export function parseDrawioFileContent(content: string): LoadResult {
|
|||||||
decompressedAny = true
|
decompressedAny = true
|
||||||
}
|
}
|
||||||
// Nothing changed — keep the file's own serialisation.
|
// Nothing changed — keep the file's own serialisation.
|
||||||
return { ok: true, xml: decompressedAny ? serializeMxfile(doc) : trimmed }
|
return {
|
||||||
|
ok: true,
|
||||||
|
xml: decompressedAny ? serializeMxfile(doc) : trimmed,
|
||||||
|
hadCompressedPages: decompressedAny,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,11 @@ describe("decompressPageContent", () => {
|
|||||||
describe("parseDrawioFileContent", () => {
|
describe("parseDrawioFileContent", () => {
|
||||||
it("passes a plain-XML mxfile through unchanged", () => {
|
it("passes a plain-XML mxfile through unchanged", () => {
|
||||||
const r = parseDrawioFileContent(PLAIN_MXFILE)
|
const r = parseDrawioFileContent(PLAIN_MXFILE)
|
||||||
expect(r).toEqual({ ok: true, xml: PLAIN_MXFILE })
|
expect(r).toEqual({
|
||||||
|
ok: true,
|
||||||
|
xml: PLAIN_MXFILE,
|
||||||
|
hadCompressedPages: false,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("wraps a bare mxGraphModel into a one-page mxfile", () => {
|
it("wraps a bare mxGraphModel into a one-page mxfile", () => {
|
||||||
@@ -103,7 +107,11 @@ describe("parseDrawioFileContent", () => {
|
|||||||
it("keeps empty pages as-is", () => {
|
it("keeps empty pages as-is", () => {
|
||||||
const withEmpty = `<mxfile><diagram id="a" name="Page-1">${MODEL_XML}</diagram><diagram id="b" name="Empty"></diagram></mxfile>`
|
const withEmpty = `<mxfile><diagram id="a" name="Page-1">${MODEL_XML}</diagram><diagram id="b" name="Empty"></diagram></mxfile>`
|
||||||
const r = parseDrawioFileContent(withEmpty)
|
const r = parseDrawioFileContent(withEmpty)
|
||||||
expect(r).toEqual({ ok: true, xml: withEmpty })
|
expect(r).toEqual({
|
||||||
|
ok: true,
|
||||||
|
xml: withEmpty,
|
||||||
|
hadCompressedPages: false,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("rejects empty files", () => {
|
it("rejects empty files", () => {
|
||||||
@@ -124,3 +132,30 @@ describe("parseDrawioFileContent", () => {
|
|||||||
if (!r.ok) expect(r.error).toContain('"Broken"')
|
if (!r.ok) expect(r.error).toContain('"Broken"')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("hadCompressedPages", () => {
|
||||||
|
it("is false for a plain-XML mxfile", () => {
|
||||||
|
const r = parseDrawioFileContent(PLAIN_MXFILE)
|
||||||
|
expect(r.ok).toBe(true)
|
||||||
|
if (r.ok) expect(r.hadCompressedPages).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("is false for a bare mxGraphModel", () => {
|
||||||
|
const r = parseDrawioFileContent(MODEL_XML)
|
||||||
|
expect(r.ok).toBe(true)
|
||||||
|
if (r.ok) expect(r.hadCompressedPages).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("is true for a fully compressed mxfile", () => {
|
||||||
|
const r = parseDrawioFileContent(COMPRESSED_MXFILE)
|
||||||
|
expect(r.ok).toBe(true)
|
||||||
|
if (r.ok) expect(r.hadCompressedPages).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("is true for a mixed plain/compressed file", () => {
|
||||||
|
const mixed = `<mxfile><diagram id="a" name="Plain">${MODEL_XML}</diagram><diagram id="b" name="Squeezed">${drawioCompress(MODEL_XML)}</diagram></mxfile>`
|
||||||
|
const r = parseDrawioFileContent(mixed)
|
||||||
|
expect(r.ok).toBe(true)
|
||||||
|
if (r.ok) expect(r.hadCompressedPages).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -188,3 +188,51 @@ describe("MCP server wiring", () => {
|
|||||||
expect(text.length).toBeLessThanOrEqual(15000)
|
expect(text.length).toBeLessThanOrEqual(15000)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("load_diagram dual-source arguments", () => {
|
||||||
|
it("advertises both optional 'path' and 'xml' sources", async () => {
|
||||||
|
const resp = await send("tools/list", {})
|
||||||
|
const load = resp.result.tools.find(
|
||||||
|
(t: { name: string }) => t.name === "load_diagram",
|
||||||
|
)
|
||||||
|
const props = load?.inputSchema?.properties ?? {}
|
||||||
|
expect(props.path).toBeTruthy()
|
||||||
|
expect(props.xml).toBeTruthy()
|
||||||
|
const required: string[] = load?.inputSchema?.required ?? []
|
||||||
|
expect(required).not.toContain("path")
|
||||||
|
expect(required).not.toContain("xml")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects passing both 'path' and 'xml'", async () => {
|
||||||
|
// Argument validation fires before the session check: no session
|
||||||
|
// exists in this harness, so a both-args call must report the
|
||||||
|
// mutual-exclusion error, not "No active session".
|
||||||
|
const resp = await send("tools/call", {
|
||||||
|
name: "load_diagram",
|
||||||
|
arguments: { path: "/tmp/x.drawio", xml: "<mxfile/>" },
|
||||||
|
})
|
||||||
|
expect(resp.error, JSON.stringify(resp.error)).toBeUndefined()
|
||||||
|
expect(resp.result?.isError).toBe(true)
|
||||||
|
expect(resp.result?.content?.[0]?.text).toContain("not both")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rejects passing neither 'path' nor 'xml'", async () => {
|
||||||
|
const resp = await send("tools/call", {
|
||||||
|
name: "load_diagram",
|
||||||
|
arguments: {},
|
||||||
|
})
|
||||||
|
expect(resp.result?.isError).toBe(true)
|
||||||
|
expect(resp.result?.content?.[0]?.text).toContain("either 'path'")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("accepts 'xml' alone as a source (fails only on the missing session)", async () => {
|
||||||
|
const resp = await send("tools/call", {
|
||||||
|
name: "load_diagram",
|
||||||
|
arguments: {
|
||||||
|
xml: '<mxfile><diagram id="p" name="P"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(resp.result?.isError).toBe(true)
|
||||||
|
expect(resp.result?.content?.[0]?.text).toContain("No active session")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -108,6 +108,25 @@ async function boxOf(locator: Locator) {
|
|||||||
return box
|
return box
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click the canvas where no shape is, to deselect: the corner of the
|
||||||
|
* container farthest from the shape. A fixed spot near the top-left corner
|
||||||
|
* is not safe, as a click on a shape may scroll it into that corner.
|
||||||
|
*/
|
||||||
|
async function clickEmptyCanvas(page: Page, shape: Locator) {
|
||||||
|
const container = getIframeContent(page).locator(".geDiagramContainer")
|
||||||
|
const box = await boxOf(container)
|
||||||
|
const shapeBox = await boxOf(shape)
|
||||||
|
const shapeX = shapeBox.x + shapeBox.width / 2
|
||||||
|
const shapeY = shapeBox.y + shapeBox.height / 2
|
||||||
|
await container.click({
|
||||||
|
position: {
|
||||||
|
x: shapeX < box.x + box.width / 2 ? box.width - 20 : 20,
|
||||||
|
y: shapeY < box.y + box.height / 2 ? box.height - 20 : 20,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** draw.io's own Undo button: draw.io sets "disabled" with nothing to undo */
|
/** draw.io's own Undo button: draw.io sets "disabled" with nothing to undo */
|
||||||
function drawioUndo(page: Page) {
|
function drawioUndo(page: Page) {
|
||||||
return getIframeContent(page).locator('.geSimpleMainMenu a[title="Undo"]')
|
return getIframeContent(page).locator('.geSimpleMainMenu a[title="Undo"]')
|
||||||
@@ -4512,9 +4531,7 @@ test.describe("Edge cases", () => {
|
|||||||
})
|
})
|
||||||
await sendMessage(page, "Make this red")
|
await sendMessage(page, "Make this red")
|
||||||
// Deselected while the diagram exports
|
// Deselected while the diagram exports
|
||||||
await canvas
|
await clickEmptyCanvas(page, shape)
|
||||||
.locator(".geDiagramContainer")
|
|
||||||
.click({ position: { x: 10, y: 10 } })
|
|
||||||
await expect(chip).toHaveCount(0)
|
await expect(chip).toHaveCount(0)
|
||||||
await page.evaluate(() => (window as any).__releaseExports())
|
await page.evaluate(() => (window as any).__releaseExports())
|
||||||
await waitForCompleteCount(page, 2)
|
await waitForCompleteCount(page, 2)
|
||||||
|
|||||||
Reference in New Issue
Block a user