Merge pull request #481 from open-pencil/release-v014-smoke
fix(app): keep desktop startup console clean
This commit is contained in:
commit
c29654cd07
|
|
@ -53,6 +53,8 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- Keep desktop startup clean when optional MCP automation is unavailable, retain startup diagnostics for MCP-dependent features, and load JSX syntax highlighting without browser globals.
|
||||
|
||||
- Preserve Figma’s imported glyph outlines through layout and appearance updates so text keeps its intended weight and shape.
|
||||
- Keep swapped image avatars and thin stepper dividers at their effective imported size and position.
|
||||
- Scale proportion-constrained `.fig` instance geometry through fixed wrapper layers so imported logos and icons retain their intended size.
|
||||
|
|
|
|||
44
bun.lock
44
bun.lock
|
|
@ -43,12 +43,13 @@
|
|||
"dedent": "^1.7.1",
|
||||
"fflate": "^0.8.2",
|
||||
"fzstd": "^0.1.1",
|
||||
"hast-util-to-html": "^9.0.5",
|
||||
"idb": "^8.0.3",
|
||||
"jspdf": "^4.2.1",
|
||||
"lib0": "^0.2.117",
|
||||
"motion-v": "^2.0.0",
|
||||
"opentype.js": "^2.0.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"refractor": "^5.0.0",
|
||||
"reka-ui": "^2.9.0",
|
||||
"svg2pdf.js": "^2.7.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
|
|
@ -84,7 +85,6 @@
|
|||
"@types/culori": "^4.0.1",
|
||||
"@types/node": "^25.3.2",
|
||||
"@types/opentype.js": "^1.3.9",
|
||||
"@types/prismjs": "^1.26.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260228.1",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
},
|
||||
"packages/cli": {
|
||||
"name": "@open-pencil/cli",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"bin": {
|
||||
"openpencil": "./bin/openpencil.js",
|
||||
},
|
||||
|
|
@ -136,7 +136,7 @@
|
|||
},
|
||||
"packages/core": {
|
||||
"name": "@open-pencil/core",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"dependencies": {
|
||||
"@chenglou/pretext": "^0.0.7",
|
||||
"@iconify/utils": "^3.1.0",
|
||||
|
|
@ -193,7 +193,7 @@
|
|||
},
|
||||
"packages/dom-css": {
|
||||
"name": "@open-pencil/dom-css",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.31",
|
||||
"parse5": "^8.0.1",
|
||||
|
|
@ -213,7 +213,7 @@
|
|||
},
|
||||
"packages/fig": {
|
||||
"name": "@open-pencil/fig",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"dependencies": {
|
||||
"es-toolkit": "^1.46.1",
|
||||
"fflate": "^0.8.2",
|
||||
|
|
@ -230,7 +230,7 @@
|
|||
},
|
||||
"packages/kiwi": {
|
||||
"name": "@open-pencil/kiwi",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"dependencies": {
|
||||
"fflate": "^0.8.2",
|
||||
"fzstd": "^0.1.1",
|
||||
|
|
@ -243,7 +243,7 @@
|
|||
},
|
||||
"packages/mcp": {
|
||||
"name": "@open-pencil/mcp",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"bin": {
|
||||
"openpencil-mcp": "./dist/stdio.mjs",
|
||||
"openpencil-mcp-http": "./dist/index.mjs",
|
||||
|
|
@ -264,7 +264,7 @@
|
|||
},
|
||||
"packages/pen": {
|
||||
"name": "@open-pencil/pen",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"dependencies": {
|
||||
"culori": "^4.0.2",
|
||||
},
|
||||
|
|
@ -280,7 +280,7 @@
|
|||
},
|
||||
"packages/scene-graph": {
|
||||
"name": "@open-pencil/scene-graph",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"dependencies": {
|
||||
"es-toolkit": "^1.46.1",
|
||||
"nanoevents": "^9.1.0",
|
||||
|
|
@ -294,7 +294,7 @@
|
|||
},
|
||||
"packages/vue": {
|
||||
"name": "@open-pencil/vue",
|
||||
"version": "0.13.2",
|
||||
"version": "0.14.0",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.9",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
||||
|
|
@ -1589,6 +1589,8 @@
|
|||
|
||||
"character-parser": ["character-parser@2.2.0", "", { "dependencies": { "is-regex": "^1.0.3" } }, "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw=="],
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
|
||||
"check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
|
@ -1961,10 +1963,14 @@
|
|||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||
|
||||
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
|
||||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
|
||||
|
||||
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
|
||||
|
||||
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
|
||||
|
|
@ -2025,6 +2031,10 @@
|
|||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
|
||||
|
||||
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
|
||||
|
|
@ -2043,6 +2053,8 @@
|
|||
|
||||
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
|
||||
|
||||
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-electron": ["is-electron@2.2.2", "", {}, "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg=="],
|
||||
|
|
@ -2061,6 +2073,8 @@
|
|||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||
|
||||
"is-loopback-addr": ["is-loopback-addr@2.0.2", "", {}, "sha512-26POf2KRCno/KTNL5Q0b/9TYnL00xEsSaLfiFRmjM7m7Lw7ZMmFybzzuX4CcsLAluZGd+niLUiMRxEooVE3aqg=="],
|
||||
|
|
@ -2527,6 +2541,8 @@
|
|||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
|
||||
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
|
||||
|
||||
"parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
|
||||
|
|
@ -2591,8 +2607,6 @@
|
|||
|
||||
"prexit": ["prexit@2.3.0", "", {}, "sha512-mX+LIbtS0anKtl2PykYabxninwloblUQMRO6CubeSmjxb+kKlATuJoH9UeN8NLE4TgIEFWfBXw7V3GkWbBrSmg=="],
|
||||
|
||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||
|
||||
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||
|
||||
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
|
||||
|
|
@ -2685,6 +2699,8 @@
|
|||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"refractor": ["refractor@5.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="],
|
||||
|
||||
"regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="],
|
||||
|
||||
"regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="],
|
||||
|
|
@ -3485,6 +3501,8 @@
|
|||
|
||||
"p-queue/p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="],
|
||||
|
||||
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
|
|
|||
31
desktop/Cargo.lock
generated
31
desktop/Cargo.lock
generated
|
|
@ -967,6 +967,12 @@ version = "1.0.20"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "3.0.6"
|
||||
|
|
@ -1023,6 +1029,12 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_home"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
|
|
@ -2957,6 +2969,7 @@ dependencies = [
|
|||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
"which",
|
||||
"zip 2.4.2",
|
||||
"zstd",
|
||||
]
|
||||
|
|
@ -5897,6 +5910,18 @@ version = "0.1.12"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "7.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762"
|
||||
dependencies = [
|
||||
"either",
|
||||
"env_home",
|
||||
"rustix",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
|
@ -6372,6 +6397,12 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
||||
[[package]]
|
||||
name = "wio"
|
||||
version = "0.2.2"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ tauri-plugin-fs = { version = "2.4.5", features = ["watch"] }
|
|||
tauri-plugin-shell = "2"
|
||||
zstd = "0.13"
|
||||
zip = { version = "2", default-features = false }
|
||||
which = "7.0.3"
|
||||
font-kit = "0.14.3"
|
||||
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", branch = "dev" }
|
||||
tauri-plugin-updater = "2"
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ fn take_pending_open(state: tauri::State<PendingOpen>) -> Vec<PendingOpenFile> {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn mcp_executable_available() -> bool {
|
||||
which::which("openpencil-mcp-http").is_ok()
|
||||
}
|
||||
|
||||
fn file_association_path(path: PathBuf) -> Option<PathBuf> {
|
||||
let path = path.canonicalize().ok()?;
|
||||
if !path.is_file() {
|
||||
|
|
@ -128,6 +133,7 @@ pub fn run() {
|
|||
credential_status,
|
||||
credential_store_availability,
|
||||
credential_write,
|
||||
mcp_executable_available,
|
||||
list_system_fonts,
|
||||
load_system_font,
|
||||
proxy_http_request,
|
||||
|
|
|
|||
|
|
@ -96,12 +96,13 @@
|
|||
"dedent": "^1.7.1",
|
||||
"fflate": "^0.8.2",
|
||||
"fzstd": "^0.1.1",
|
||||
"hast-util-to-html": "^9.0.5",
|
||||
"idb": "^8.0.3",
|
||||
"jspdf": "^4.2.1",
|
||||
"lib0": "^0.2.117",
|
||||
"motion-v": "^2.0.0",
|
||||
"opentype.js": "^2.0.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"refractor": "^5.0.0",
|
||||
"reka-ui": "^2.9.0",
|
||||
"svg2pdf.js": "^2.7.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
|
|
@ -137,7 +138,6 @@
|
|||
"@types/culori": "^4.0.1",
|
||||
"@types/node": "^25.3.2",
|
||||
"@types/opentype.js": "^1.3.9",
|
||||
"@types/prismjs": "^1.26.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260228.1",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
|
|
|
|||
|
|
@ -28,8 +28,26 @@ const DEV_AUTOMATION_AUTH_TOKEN =
|
|||
const APP_VERSION =
|
||||
typeof __OPENPENCIL_APP_VERSION__ === 'string' ? __OPENPENCIL_APP_VERSION__ : '0.0.0-test'
|
||||
const noop = () => undefined
|
||||
const MAX_STARTUP_STDERR_LENGTH = 8_192
|
||||
const MCP_EXECUTABLE = 'openpencil-mcp-http'
|
||||
|
||||
let runtimeAutomationAuthToken: string | null = DEV_AUTOMATION_AUTH_TOKEN
|
||||
let runtimeAutomationStartupError: Error | null = null
|
||||
|
||||
function toError(error: unknown): Error {
|
||||
return error instanceof Error ? error : new Error(String(error))
|
||||
}
|
||||
|
||||
function missingMCPError(): Error {
|
||||
return new Error(
|
||||
`MCP automation is not installed. Install @open-pencil/mcp@${APP_VERSION} globally with your package manager, then restart OpenPencil.`
|
||||
)
|
||||
}
|
||||
|
||||
function rememberStartupError(error: unknown): null {
|
||||
runtimeAutomationStartupError = toError(error)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the auth token from the MCP discovery file via Tauri's FS plugin.
|
||||
|
|
@ -138,8 +156,7 @@ async function readHealth(): Promise<AutomationHealth | null> {
|
|||
})
|
||||
if (!res.ok) return null
|
||||
return (await res.json()) as AutomationHealth
|
||||
} catch (e) {
|
||||
console.error('[MCP] health check failed:', e instanceof Error ? e.message : e)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +173,7 @@ function parseMajorMinor(version: string): string | null {
|
|||
function assertCompatibleMCPVersion(health: AutomationHealth): void {
|
||||
const runningMajorMinor = health.version ? parseMajorMinor(health.version) : null
|
||||
const oursMajorMinor = parseMajorMinor(APP_VERSION)
|
||||
if (!runningMajorMinor || !oursMajorMinor) return // unparseable — don't block
|
||||
if (!oursMajorMinor) return
|
||||
if (runningMajorMinor === oursMajorMinor) return
|
||||
const runningVersion = health.version ? `v${health.version}` : 'an older version'
|
||||
const updateHint = health.installCommand
|
||||
|
|
@ -181,6 +198,7 @@ export async function getAutomationAuthToken(): Promise<string | null> {
|
|||
if (runtimeAutomationAuthToken) return runtimeAutomationAuthToken
|
||||
const health = await readHealth()
|
||||
if (!health) {
|
||||
if (runtimeAutomationStartupError) throw runtimeAutomationStartupError
|
||||
throw new Error(
|
||||
'MCP server is not reachable. Ensure the desktop app is running and the MCP server has started.'
|
||||
)
|
||||
|
|
@ -212,14 +230,17 @@ export async function getAutomationAuthToken(): Promise<string | null> {
|
|||
return runtimeAutomationAuthToken
|
||||
}
|
||||
|
||||
export async function spawnMCPIfNeeded(): Promise<AutomationServerHandle | null> {
|
||||
async function startMCPIfNeeded(): Promise<AutomationServerHandle | null> {
|
||||
runtimeAutomationStartupError = null
|
||||
if (import.meta.env.DEV || !isTauri()) {
|
||||
return DEV_AUTOMATION_AUTH_TOKEN
|
||||
? { disconnect: noop, authToken: DEV_AUTOMATION_AUTH_TOKEN }
|
||||
: null
|
||||
}
|
||||
|
||||
const existing = await readHealth()
|
||||
const expectedDiscoveryPath = await computeExpectedDiscoveryPath()
|
||||
const hasDiscovery = await discoveryFileExists(expectedDiscoveryPath)
|
||||
const existing = hasDiscovery ? await readHealth() : null
|
||||
if (existing) {
|
||||
assertCompatibleMCPVersion(existing)
|
||||
const discoveryPath = await resolveDiscoveryPath(existing.discoveryPath)
|
||||
|
|
@ -237,6 +258,10 @@ export async function spawnMCPIfNeeded(): Promise<AutomationServerHandle | null>
|
|||
}
|
||||
}
|
||||
|
||||
const { invoke } = await import('@tauri-apps/api/core')
|
||||
const executableAvailable = await invoke<boolean>('mcp_executable_available')
|
||||
if (!executableAvailable) return rememberStartupError(missingMCPError())
|
||||
|
||||
const authToken = randomHex(32)
|
||||
// Cache only after MCP startup is confirmed healthy.
|
||||
|
||||
|
|
@ -247,7 +272,7 @@ export async function spawnMCPIfNeeded(): Promise<AutomationServerHandle | null>
|
|||
// The app bundle directory (Tauri executableDir) is read-only and would
|
||||
// cause EACCES errors on every file write.
|
||||
const mcpRoot = await resolveTauriHomeDir()
|
||||
const resolved = resolvePlatformCommand('openpencil-mcp-http')
|
||||
const resolved = resolvePlatformCommand(MCP_EXECUTABLE)
|
||||
const command = Command.create(resolved.command, resolved.args, {
|
||||
env: {
|
||||
PORT: String(AUTOMATION_HTTP_PORT),
|
||||
|
|
@ -258,19 +283,40 @@ export async function spawnMCPIfNeeded(): Promise<AutomationServerHandle | null>
|
|||
}
|
||||
})
|
||||
|
||||
let startupStderr = ''
|
||||
command.stderr.on('data', (raw: Uint8Array | number[] | string) => {
|
||||
console.error('[MCP]', decodeTauriStderr(raw))
|
||||
if (startupStderr.length >= MAX_STARTUP_STDERR_LENGTH) return
|
||||
startupStderr += decodeTauriStderr(raw).slice(
|
||||
0,
|
||||
MAX_STARTUP_STDERR_LENGTH - startupStderr.length
|
||||
)
|
||||
})
|
||||
|
||||
let spawnedToken: string | null = null
|
||||
command.on('close', (data: { code: number | null }) => {
|
||||
console.error(`[MCP] Server exited (code ${data.code ?? 'null'})`)
|
||||
let child: Awaited<ReturnType<typeof command.spawn>>
|
||||
const childClosed = new Promise<{ code: number | null; signal: number | null }>((resolve) => {
|
||||
command.on('close', (event) => {
|
||||
resolve(event)
|
||||
if (spawnedToken && runtimeAutomationAuthToken === spawnedToken) {
|
||||
runtimeAutomationAuthToken = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const child = await command.spawn()
|
||||
try {
|
||||
child = await command.spawn()
|
||||
} catch (error) {
|
||||
return rememberStartupError(error)
|
||||
}
|
||||
const earlyExit = await Promise.race([childClosed, promiseTimeout(250).then(() => null)])
|
||||
if (earlyExit) {
|
||||
const details = startupStderr.trim()
|
||||
return rememberStartupError(
|
||||
new Error(
|
||||
`MCP server exited before startup completed (code ${earlyExit.code ?? 'null'}, signal ${earlyExit.signal ?? 'null'})${details ? `: ${details}` : '.'}`
|
||||
)
|
||||
)
|
||||
}
|
||||
const health = await pollHealth(5, 1000)
|
||||
|
||||
if (health) {
|
||||
|
|
@ -281,6 +327,7 @@ export async function spawnMCPIfNeeded(): Promise<AutomationServerHandle | null>
|
|||
const token = discovered ?? authToken
|
||||
spawnedToken = token
|
||||
runtimeAutomationAuthToken = token
|
||||
runtimeAutomationStartupError = null
|
||||
return {
|
||||
disconnect: () => {
|
||||
void child.kill().catch((e) => {
|
||||
|
|
@ -304,9 +351,19 @@ export async function spawnMCPIfNeeded(): Promise<AutomationServerHandle | null>
|
|||
} finally {
|
||||
runtimeAutomationAuthToken = null
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to start MCP server. Install @open-pencil/mcp@${APP_VERSION} globally with your package manager, then restart OpenPencil.`
|
||||
return rememberStartupError(
|
||||
new Error(
|
||||
`MCP server did not become healthy within the startup timeout${startupStderr.trim() ? `: ${startupStderr.trim()}` : '.'}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export async function spawnMCPIfNeeded(): Promise<AutomationServerHandle | null> {
|
||||
try {
|
||||
return await startMCPIfNeeded()
|
||||
} catch (error) {
|
||||
return rememberStartupError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
9
src/app/code/highlight.ts
Normal file
9
src/app/code/highlight.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { toHtml as toHTML } from 'hast-util-to-html'
|
||||
import { refractor } from 'refractor/core'
|
||||
import jsx from 'refractor/jsx'
|
||||
|
||||
refractor.register(jsx)
|
||||
|
||||
export function highlightJSX(code: string): string {
|
||||
return toHTML(refractor.highlight(code, 'jsx'))
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import Prism from 'prismjs'
|
||||
import 'prismjs/components/prism-jsx'
|
||||
import { ScrollAreaRoot, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport } from 'reka-ui'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
|
@ -8,6 +6,7 @@ import { computed, ref, watch } from 'vue'
|
|||
import { JSX_REFERENCE, selectionToJSX } from '@open-pencil/core/design-jsx'
|
||||
import { useI18n, useSceneComputed } from '@open-pencil/vue'
|
||||
|
||||
import { highlightJSX } from '@/app/code/highlight'
|
||||
import { useEditorStore } from '@/app/editor/active-store'
|
||||
import AppPlaceholder from '@/components/ui/AppPlaceholder.vue'
|
||||
import AppTextButton from '@/components/ui/AppTextButton.vue'
|
||||
|
|
@ -38,8 +37,7 @@ const jsxCode = useSceneComputed(() => {
|
|||
|
||||
const highlightedLines = computed(() => {
|
||||
if (!jsxCode.value) return []
|
||||
const grammar = Prism.languages.jsx ?? Prism.languages.javascript
|
||||
return jsxCode.value.split('\n').map((line) => Prism.highlight(line, grammar, 'jsx'))
|
||||
return jsxCode.value.split('\n').map(highlightJSX)
|
||||
})
|
||||
|
||||
const { copy: copyRef, copied: copiedRef } = useClipboard({ copiedDuring: 2000 })
|
||||
|
|
|
|||
|
|
@ -87,16 +87,12 @@ async function bindAssociatedFileOpen() {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const mcp = await spawnMCPIfNeeded()
|
||||
mcpCleanup.value = mcp?.disconnect ?? null
|
||||
const tauri = isTauri()
|
||||
if (import.meta.env.DEV || tauri) {
|
||||
if (import.meta.env.DEV || (tauri && mcp)) {
|
||||
automationCleanup.value = connectAutomation(getActiveStore, mcp?.authToken ?? null).disconnect
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[MCP]', e)
|
||||
}
|
||||
|
||||
try {
|
||||
await bindAssociatedFileOpen()
|
||||
|
|
|
|||
15
tests/engine/app/code/highlight.test.ts
Normal file
15
tests/engine/app/code/highlight.test.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { highlightJSX } from '@/app/code/highlight'
|
||||
|
||||
describe('code highlighting', () => {
|
||||
test('highlights JavaScript and JSX syntax without a Prism global', () => {
|
||||
const existingPrism = Reflect.get(globalThis, 'Prism')
|
||||
const html = highlightJSX('const view = <Button disabled>{label}</Button>')
|
||||
|
||||
expect(html).toContain('<span class="token keyword">const</span>')
|
||||
expect(html).toContain('<span class="token class-name">Button</span>')
|
||||
expect(html).toContain('<span class="token attr-name">disabled</span>')
|
||||
expect(Reflect.get(globalThis, 'Prism')).toBe(existingPrism)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { afterEach, describe, expect, test, vi } from 'bun:test'
|
||||
|
||||
import { spawnMCPIfNeeded } from '@/app/automation/mcp/spawn'
|
||||
import { getAutomationAuthToken, spawnMCPIfNeeded } from '@/app/automation/mcp/spawn'
|
||||
|
||||
import { clearTauriMocks, installTauriMockWindow, mockTauriIPC } from '#tests/helpers/tauri/mocks'
|
||||
|
||||
|
|
@ -24,6 +24,81 @@ afterEach(async () => {
|
|||
})
|
||||
|
||||
describe('Tauri MCP spawning', () => {
|
||||
test('retains an early startup failure for MCP-dependent features', async () => {
|
||||
installTauriMockWindow()
|
||||
Object.assign(globalThis.window, { location: { origin: 'tauri://localhost' } })
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
configurable: true,
|
||||
value: { platform: 'Win32', userAgent: 'Windows' }
|
||||
})
|
||||
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('', { status: 404 }))
|
||||
await mockTauriIPC((cmd, args) => {
|
||||
if (cmd === 'mcp_executable_available') return true
|
||||
if (cmd === 'plugin:path|resolve_directory') return '/mock/home'
|
||||
if (cmd === 'plugin:fs|exists') return false
|
||||
if (cmd === 'plugin:shell|spawn') {
|
||||
const onEvent = (args as { onEvent: { onmessage: (event: unknown) => void } }).onEvent
|
||||
.onmessage
|
||||
queueMicrotask(() => {
|
||||
onEvent({
|
||||
event: 'Stderr',
|
||||
payload: Array.from(
|
||||
new TextEncoder().encode('El sistema no puede encontrar el archivo especificado.')
|
||||
)
|
||||
})
|
||||
onEvent({ event: 'Terminated', payload: { code: 1, signal: null } })
|
||||
})
|
||||
return 88
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
expect(await spawnMCPIfNeeded()).toBeNull()
|
||||
expect(getAutomationAuthToken()).rejects.toThrow('MCP server exited before startup completed')
|
||||
})
|
||||
|
||||
test('retains unexpected spawn errors for MCP-dependent features', async () => {
|
||||
installTauriMockWindow()
|
||||
Object.assign(globalThis.window, { location: { origin: 'tauri://localhost' } })
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
configurable: true,
|
||||
value: { platform: 'MacIntel', userAgent: 'Macintosh' }
|
||||
})
|
||||
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('', { status: 404 }))
|
||||
await mockTauriIPC((cmd) => {
|
||||
if (cmd === 'mcp_executable_available') return true
|
||||
if (cmd === 'plugin:path|resolve_directory') return '/mock/home'
|
||||
if (cmd === 'plugin:fs|exists') return false
|
||||
if (cmd === 'plugin:shell|spawn') throw new Error('shell permission denied')
|
||||
return null
|
||||
})
|
||||
|
||||
expect(await spawnMCPIfNeeded()).toBeNull()
|
||||
expect(getAutomationAuthToken()).rejects.toThrow('shell permission denied')
|
||||
})
|
||||
|
||||
test('retains a language-independent missing executable diagnostic', async () => {
|
||||
installTauriMockWindow()
|
||||
Object.assign(globalThis.window, { location: { origin: 'tauri://localhost' } })
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
configurable: true,
|
||||
value: { platform: 'MacIntel', userAgent: 'Macintosh' }
|
||||
})
|
||||
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('', { status: 404 }))
|
||||
await mockTauriIPC((cmd) => {
|
||||
if (cmd === 'plugin:path|resolve_directory') return '/mock/home'
|
||||
if (cmd === 'plugin:fs|exists') return false
|
||||
if (cmd === 'mcp_executable_available') return false
|
||||
return null
|
||||
})
|
||||
|
||||
expect(await spawnMCPIfNeeded()).toBeNull()
|
||||
expect(getAutomationAuthToken()).rejects.toThrow('MCP automation is not installed')
|
||||
})
|
||||
|
||||
test('spawns MCP server with shell plugin when health check is missing', async () => {
|
||||
installTauriMockWindow()
|
||||
Object.assign(globalThis.window, { location: { origin: 'tauri://localhost' } })
|
||||
|
|
@ -51,6 +126,7 @@ describe('Tauri MCP spawning', () => {
|
|||
const calls: Array<{ cmd: string; args: unknown }> = []
|
||||
await mockTauriIPC((cmd, args) => {
|
||||
calls.push({ cmd, args })
|
||||
if (cmd === 'mcp_executable_available') return true
|
||||
if (cmd === 'plugin:shell|spawn') {
|
||||
expect(args).toMatchObject({
|
||||
program: 'openpencil-mcp-http',
|
||||
|
|
@ -68,9 +144,12 @@ describe('Tauri MCP spawning', () => {
|
|||
onEvent = (args as { onEvent: { onmessage: (event: unknown) => void } }).onEvent.onmessage
|
||||
return 77
|
||||
}
|
||||
const pathArg = (args as { path?: string })?.path
|
||||
if (cmd === 'plugin:fs|exists') {
|
||||
return pathArg === DISCOVERY_PATH
|
||||
}
|
||||
if (cmd === 'plugin:fs|read_text_file') {
|
||||
// Only return discovery data when reading the expected discovery path
|
||||
const pathArg = (args as { path?: string })?.path
|
||||
if (pathArg === DISCOVERY_PATH) return new TextEncoder().encode(DISCOVERY_JSON)
|
||||
return null
|
||||
}
|
||||
|
|
@ -82,6 +161,7 @@ describe('Tauri MCP spawning', () => {
|
|||
})
|
||||
|
||||
const handle = await spawnMCPIfNeeded()
|
||||
await expect(getAutomationAuthToken()).resolves.toBe('discovery-token')
|
||||
onEvent?.({ event: 'Stderr', payload: [119, 97, 114, 110] })
|
||||
handle?.disconnect()
|
||||
await Promise.resolve()
|
||||
|
|
|
|||
Loading…
Reference in a new issue