diff --git a/CHANGELOG.md b/CHANGELOG.md index e034b34f6..b5e6032fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/bun.lock b/bun.lock index 3d7b4ecca..5e5542f03 100644 --- a/bun.lock +++ b/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=="], diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 2dfb2c578..dba6c814e 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -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" diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 8f969b9a4..e15396925 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -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" diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index e643c501f..3d344463d 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -41,6 +41,11 @@ fn take_pending_open(state: tauri::State) -> Vec { .unwrap_or_default() } +#[tauri::command] +fn mcp_executable_available() -> bool { + which::which("openpencil-mcp-http").is_ok() +} + fn file_association_path(path: PathBuf) -> Option { 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, diff --git a/package.json b/package.json index 78f2b88ca..150cd7098 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/automation/mcp/spawn.ts b/src/app/automation/mcp/spawn.ts index c46a785c8..fb953b714 100644 --- a/src/app/automation/mcp/spawn.ts +++ b/src/app/automation/mcp/spawn.ts @@ -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 { }) 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 { 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 { return runtimeAutomationAuthToken } -export async function spawnMCPIfNeeded(): Promise { +async function startMCPIfNeeded(): Promise { + 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 } } + const { invoke } = await import('@tauri-apps/api/core') + const executableAvailable = await invoke('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 // 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 } }) + 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'})`) - if (spawnedToken && runtimeAutomationAuthToken === spawnedToken) { - runtimeAutomationAuthToken = null - } + let child: Awaited> + 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 const token = discovered ?? authToken spawnedToken = token runtimeAutomationAuthToken = token + runtimeAutomationStartupError = null return { disconnect: () => { void child.kill().catch((e) => { @@ -304,11 +351,21 @@ export async function spawnMCPIfNeeded(): Promise } 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 { + try { + return await startMCPIfNeeded() + } catch (error) { + return rememberStartupError(error) + } +} + /** * Returns the user's home directory. Used as the default OPENPENCIL_MCP_ROOT * so file-scoped tools operate on paths inside ~, which is writable and diff --git a/src/app/code/highlight.ts b/src/app/code/highlight.ts new file mode 100644 index 000000000..f6b610056 --- /dev/null +++ b/src/app/code/highlight.ts @@ -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')) +} diff --git a/src/components/CodePanel.vue b/src/components/CodePanel.vue index f2b2b92d4..1b913acb8 100644 --- a/src/components/CodePanel.vue +++ b/src/components/CodePanel.vue @@ -1,6 +1,4 @@