diff --git a/clients/web/package-lock.json b/clients/web/package-lock.json index 498b20391..f3903443c 100644 --- a/clients/web/package-lock.json +++ b/clients/web/package-lock.json @@ -24,6 +24,8 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-icons": "^5.6.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", "zod": "^4.3.6", "zustand": "^5.0.13" }, @@ -2245,6 +2247,15 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2263,9 +2274,17 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -2291,6 +2310,15 @@ "@types/send": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -2305,6 +2333,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", @@ -2312,6 +2349,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.12.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", @@ -2346,7 +2389,6 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2390,6 +2432,12 @@ "@types/node": "*" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", @@ -2702,6 +2750,12 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -3188,6 +3242,16 @@ "npm": ">=6" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3360,6 +3424,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -3394,6 +3468,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -3457,6 +3571,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3597,6 +3721,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -3667,7 +3804,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3689,6 +3825,19 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -4121,6 +4270,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -4239,6 +4398,12 @@ "express": ">= 4.11" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4634,6 +4799,46 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -4676,6 +4881,16 @@ "dev": true, "license": "MIT" }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -4764,6 +4979,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/ip-address": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", @@ -4782,6 +5003,30 @@ "node": ">= 0.10" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -4803,6 +5048,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-docker": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", @@ -4841,6 +5096,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -4859,6 +5124,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -5358,6 +5635,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -5449,6 +5736,16 @@ "node": ">=10" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5458,91 +5755,936 @@ "node": ">= 0.4" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://opencollective.com/unified" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" }, - "engines": { - "node": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -5747,6 +6889,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -6104,6 +7271,16 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -6240,6 +7417,33 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-number-format": { "version": "5.4.4", "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz", @@ -6415,6 +7619,72 @@ "node": ">=8" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -6747,6 +8017,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -6841,6 +8121,20 @@ "node": ">=10" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -6921,6 +8215,24 @@ "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", "license": "MIT" }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -7047,6 +8359,26 @@ "node": ">=6" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -7175,6 +8507,93 @@ "dev": true, "license": "MIT" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -7358,6 +8777,34 @@ "node": ">= 0.8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", @@ -7750,6 +9197,16 @@ "optional": true } } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/clients/web/package.json b/clients/web/package.json index 1501f1604..401d4f434 100644 --- a/clients/web/package.json +++ b/clients/web/package.json @@ -39,6 +39,8 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-icons": "^5.6.0", + "react-markdown": "^10.1.0", + "remark-gfm": "^4.0.1", "zod": "^4.3.6", "zustand": "^5.0.13" }, diff --git a/clients/web/src/App.css b/clients/web/src/App.css index 0c064dd19..703644ef3 100644 --- a/clients/web/src/App.css +++ b/clients/web/src/App.css @@ -147,3 +147,38 @@ .grid-align-start { align-items: start; } + +/* ── Markdown content (third-party HTML from react-markdown) ───── */ + +.markdown-content { + max-width: 100%; + overflow-wrap: anywhere; +} + +.markdown-content > :first-child { + margin-top: 0; +} + +.markdown-content > :last-child { + margin-bottom: 0; +} + +.markdown-content pre { + max-width: 100%; + overflow-x: auto; +} + +.markdown-content code { + word-break: break-word; +} + +.markdown-content table { + display: block; + max-width: 100%; + overflow-x: auto; +} + +.markdown-content img { + max-width: 100%; + height: auto; +} diff --git a/clients/web/src/App.tsx b/clients/web/src/App.tsx index 0187f7582..8b9784d92 100644 --- a/clients/web/src/App.tsx +++ b/clients/web/src/App.tsx @@ -16,6 +16,7 @@ import { ManagedPromptsState } from "@inspector/core/mcp/state/managedPromptsSta import { ManagedResourcesState } from "@inspector/core/mcp/state/managedResourcesState.js"; import { ManagedResourceTemplatesState } from "@inspector/core/mcp/state/managedResourceTemplatesState.js"; import { ManagedRequestorTasksState } from "@inspector/core/mcp/state/managedRequestorTasksState.js"; +import { ResourceSubscriptionsState } from "@inspector/core/mcp/state/resourceSubscriptionsState.js"; import { MessageLogState } from "@inspector/core/mcp/state/messageLogState.js"; import { FetchRequestLogState } from "@inspector/core/mcp/state/fetchRequestLogState.js"; import { StderrLogState } from "@inspector/core/mcp/state/stderrLogState.js"; @@ -26,6 +27,7 @@ import { useManagedPrompts } from "@inspector/core/react/useManagedPrompts.js"; import { useManagedResources } from "@inspector/core/react/useManagedResources.js"; import { useManagedResourceTemplates } from "@inspector/core/react/useManagedResourceTemplates.js"; import { useManagedRequestorTasks } from "@inspector/core/react/useManagedRequestorTasks.js"; +import { useResourceSubscriptions } from "@inspector/core/react/useResourceSubscriptions.js"; import { useMessageLog } from "@inspector/core/react/useMessageLog.js"; import { InspectorView } from "./components/views/InspectorView/InspectorView"; import type { ToolCallState } from "./components/screens/ToolsScreen/ToolsScreen"; @@ -173,6 +175,8 @@ function App() { useState(null); const [managedRequestorTasksState, setManagedRequestorTasksState] = useState(null); + const [resourceSubscriptionsState, setResourceSubscriptionsState] = + useState(null); const [messageLogState, setMessageLogState] = useState(null); const [fetchRequestLogState, setFetchRequestLogState] = @@ -237,6 +241,9 @@ function App() { inspectorClient, managedRequestorTasksState, ); + const { subscriptions } = useResourceSubscriptions( + resourceSubscriptionsState, + ); const { messages } = useMessageLog(messageLogState); // Capture observed handshake latency at the connecting → connected edge. @@ -304,6 +311,7 @@ function App() { managedResourcesState?.destroy(); managedResourceTemplatesState?.destroy(); managedRequestorTasksState?.destroy(); + resourceSubscriptionsState?.destroy(); messageLogState?.destroy(); fetchRequestLogState?.destroy(); stderrLogState?.destroy(); @@ -325,11 +333,19 @@ function App() { setInspectorClient(client); setManagedToolsState(new ManagedToolsState(client)); setManagedPromptsState(new ManagedPromptsState(client)); - setManagedResourcesState(new ManagedResourcesState(client)); + const nextResourcesState = new ManagedResourcesState(client); + setManagedResourcesState(nextResourcesState); setManagedResourceTemplatesState( new ManagedResourceTemplatesState(client), ); setManagedRequestorTasksState(new ManagedRequestorTasksState(client)); + // ResourceSubscriptionsState consults the managed resources list to + // resolve subscribed URIs to full Resource objects (so the subscription + // tile shows the server-supplied name/title). Pass the freshly created + // state to avoid the React update lag from setManagedResourcesState. + setResourceSubscriptionsState( + new ResourceSubscriptionsState(client, nextResourcesState), + ); setMessageLogState(new MessageLogState(client)); setFetchRequestLogState(new FetchRequestLogState(client)); setStderrLogState(new StderrLogState(client)); @@ -342,6 +358,7 @@ function App() { managedResourcesState, managedResourceTemplatesState, managedRequestorTasksState, + resourceSubscriptionsState, messageLogState, fetchRequestLogState, stderrLogState, @@ -491,6 +508,27 @@ function App() { [inspectorClient], ); + const onCompleteArgument = useCallback( + async ( + ref: + | { type: "ref/resource"; uri: string } + | { type: "ref/prompt"; name: string }, + argumentName: string, + argumentValue: string, + context: Record, + ): Promise => { + if (!inspectorClient) return []; + const result = await inspectorClient.getCompletions( + ref, + argumentName, + argumentValue, + context, + ); + return result.values; + }, + [inspectorClient], + ); + const onCancelTask = useCallback( (taskId: string) => { if (!inspectorClient) return; @@ -545,6 +583,22 @@ function App() { /* TODO: not wired yet */ }, []); + // The Resources screen needs `isSubscribed` to flip the Subscribe button + // label to "Unsubscribe". Derive it from the live subscriptions list rather + // than threading it through every setReadResourceState site — that way the + // button reflects state changes from any source (preview panel, subscribed + // tile, or future server-initiated subscribe notifications). + const effectiveReadResourceState = useMemo< + ReadResourceState | undefined + >(() => { + if (!readResourceState) return undefined; + if (!readResourceState.uri) return readResourceState; + const isSubscribed = subscriptions.some( + (s) => s.resource.uri === readResourceState.uri, + ); + return { ...readResourceState, isSubscribed }; + }, [readResourceState, subscriptions]); + return ( { expect(screen.queryByRole("img")).not.toBeInTheDocument(); expect(screen.queryByRole("button")).not.toBeInTheDocument(); }); + + it("renders text as markdown when mimeType is text/markdown", () => { + const block: ContentBlock = { + type: "text", + text: "# Title\n\nSome **bold** text.", + }; + renderWithMantine(); + expect( + screen.getByRole("heading", { level: 1, name: "Title" }), + ).toBeInTheDocument(); + expect(screen.getByText("bold")).toBeInTheDocument(); + }); + + it("accepts mimeType with parameters (e.g. text/markdown; charset=utf-8)", () => { + const block: ContentBlock = { type: "text", text: "# Heading" }; + renderWithMantine( + , + ); + expect( + screen.getByRole("heading", { level: 1, name: "Heading" }), + ).toBeInTheDocument(); + }); + + it("falls back to code rendering for non-markdown mime types", () => { + const block: ContentBlock = { type: "text", text: "# not markdown" }; + renderWithMantine(); + expect(screen.getByText("# not markdown")).toBeInTheDocument(); + // No

generated by react-markdown + expect(screen.queryByRole("heading", { level: 1 })).not.toBeInTheDocument(); + }); + + it("renders a copy overlay for markdown content when copyable", () => { + const block: ContentBlock = { type: "text", text: "# hi" }; + renderWithMantine( + , + ); + expect(screen.getByRole("button")).toBeInTheDocument(); + }); }); diff --git a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx index a5710e32b..00d4a68c8 100644 --- a/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx +++ b/clients/web/src/components/elements/ContentViewer/ContentViewer.tsx @@ -1,10 +1,18 @@ import { Code, Flex, Image, Stack, Text } from "@mantine/core"; import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; import { CopyButton } from "../CopyButton/CopyButton"; export interface ContentViewerProps { block: ContentBlock; copyable?: boolean; + /** + * Optional MIME type for the block. When `text/markdown` (or + * `text/x-markdown`), text content is rendered via react-markdown + * instead of as preformatted code. + */ + mimeType?: string; } function formatJson(content: string): string { @@ -21,6 +29,12 @@ function isJsonText(block: ContentBlock): boolean { return trimmed.startsWith("{") || trimmed.startsWith("["); } +function isMarkdownMime(mimeType: string | undefined): boolean { + if (!mimeType) return false; + const base = mimeType.split(";")[0].trim().toLowerCase(); + return base === "text/markdown" || base === "text/x-markdown"; +} + function buildDataUri(mimeType: string, data: string): string { return `data:${mimeType};base64,${data}`; } @@ -36,21 +50,49 @@ const CopyOverlay = Flex.withProps({ right: 4, }); +const MarkdownWrapper = Flex.withProps({ + className: "markdown-content", + direction: "column", +}); + const PreviewImage = Image.withProps({ alt: "Content preview", maw: 400, radius: "md", }); -export function ContentViewer({ block, copyable = false }: ContentViewerProps) { +export function ContentViewer({ + block, + copyable = false, + mimeType, +}: ContentViewerProps) { switch (block.type) { case "text": { + const renderAsMarkdown = isMarkdownMime(mimeType); + if (renderAsMarkdown) { + return ( + + + + + {block.text} + + + {copyable && ( + + + + )} + + + ); + } const isJson = isJsonText(block); const displayText = isJson ? formatJson(block.text) : block.text; return ( - + {displayText} {copyable && ( diff --git a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.test.tsx b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.test.tsx index c36666062..d6db9ae6c 100644 --- a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.test.tsx +++ b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.test.tsx @@ -199,4 +199,59 @@ describe("ResourcePreviewPanel", () => { ); expect(screen.getByText("text/markdown")).toBeInTheDocument(); }); + + it("renders text/markdown content as markdown", () => { + renderWithMantine( + , + ); + expect( + screen.getByRole("heading", { level: 1, name: "Hello" }), + ).toBeInTheDocument(); + }); + + it("infers markdown from a .md URI when mimeType is missing", () => { + renderWithMantine( + , + ); + expect( + screen.getByRole("heading", { level: 2, name: "From URI" }), + ).toBeInTheDocument(); + }); + + it("does not render plain-text content as markdown even with markdown-looking text", () => { + renderWithMantine( + , + ); + expect(screen.queryByRole("heading", { level: 1 })).not.toBeInTheDocument(); + expect(screen.getByText("# not a heading")).toBeInTheDocument(); + }); }); diff --git a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx index 8a2a0b975..d92cdf7f6 100644 --- a/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx +++ b/clients/web/src/components/groups/ResourcePreviewPanel/ResourcePreviewPanel.tsx @@ -1,4 +1,12 @@ -import { Button, Flex, Group, Stack, Text, Title } from "@mantine/core"; +import { + Button, + Flex, + Group, + ScrollArea, + Stack, + Text, + Title, +} from "@mantine/core"; import type { BlobResourceContents, ContentBlock, @@ -46,6 +54,7 @@ function formatLastUpdated(date: Date): string { const HeaderRow = Group.withProps({ justify: "space-between", wrap: "nowrap", + flex: "0 0 auto", }); const UriGroup = Group.withProps({ @@ -62,6 +71,7 @@ const UriText = Text.withProps({ const MetaRow = Group.withProps({ justify: "space-between", wrap: "nowrap", + flex: "0 0 auto", }); const TimestampText = Text.withProps({ @@ -76,6 +86,7 @@ const MimeText = Text.withProps({ const FooterRow = Group.withProps({ justify: "space-between", + flex: "0 0 auto", }); const AnnotationGroup = Group.withProps({ @@ -88,6 +99,58 @@ const ActionGroup = Group.withProps({ const Spacer = Flex.withProps({}); +// The panel sizes to its content: when the resource body is short the +// Card hugs it; when the body would overflow the Card's `mah`, the +// browser shrinks shrinkable flex items (only ContentScroll, since the +// header / meta / footer rows opt out with `flex: 0 0 auto`) and the +// inner ScrollArea takes over scrolling — keeping the subscribe button +// pinned at the bottom edge of the cap. +const PanelStack = Stack.withProps({ + gap: "md", + miw: 0, + mih: 0, +}); + +// Middle scroll region: basis sized to its own content, can shrink to +// fit the available space when content overflows, never grows past its +// content (so a short resource body doesn't push the footer down). +const ContentScroll = ScrollArea.withProps({ + flex: "0 1 auto", + miw: 0, + mih: 0, + type: "auto", + scrollbars: "y", + offsetScrollbars: true, +}); + +const ContentStack = Stack.withProps({ + gap: "md", +}); + +// Infer a markdown MIME from the URI when the server didn't supply one. +// MCP servers often return `text/plain` (or omit mimeType entirely) for +// `.md` resources; the file extension is the most reliable fallback signal. +function inferMimeFromUri(uri: string): string | undefined { + const path = uri.split("?")[0].split("#")[0]; + const lower = path.toLowerCase(); + if (lower.endsWith(".md") || lower.endsWith(".markdown")) { + return "text/markdown"; + } + return undefined; +} + +function effectiveMime( + itemMime: string | undefined, + resource: Resource, +): string { + return ( + itemMime ?? + resource.mimeType ?? + inferMimeFromUri(resource.uri) ?? + "application/octet-stream" + ); +} + export function ResourcePreviewPanel({ resource, contents, @@ -98,11 +161,10 @@ export function ResourcePreviewPanel({ onUnsubscribe, }: ResourcePreviewPanelProps) { const { uri, annotations } = resource; - const mimeType = - contents[0]?.mimeType ?? resource.mimeType ?? "application/octet-stream"; + const mimeType = effectiveMime(contents[0]?.mimeType, resource); return ( - + Resource @@ -110,9 +172,18 @@ export function ResourcePreviewPanel({ - {contents.map((item, index) => ( - - ))} + + + {contents.map((item, index) => ( + + ))} + + {lastUpdated ? ( {formatLastUpdated(lastUpdated)} @@ -140,6 +211,6 @@ export function ResourcePreviewPanel({ /> - + ); } diff --git a/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.test.tsx b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.test.tsx index d87d01d27..8a144935b 100644 --- a/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.test.tsx +++ b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.test.tsx @@ -5,31 +5,61 @@ import { renderWithMantine, screen } from "../../../test/renderWithMantine"; import { ResourceSubscribedItem } from "./ResourceSubscribedItem"; const subscription: InspectorResourceSubscription = { - resource: { uri: "file:///x", name: "x" }, + resource: { uri: "file:///foo/bar/config.json", name: "config.json" }, lastUpdated: new Date("2024-01-01T12:00:00Z"), }; describe("ResourceSubscribedItem", () => { - it("renders the resource name", () => { + it("renders the last URI path segment, not the name or title", () => { renderWithMantine( {}} />, ); - expect(screen.getByText("x")).toBeInTheDocument(); + expect(screen.getByText("config.json")).toBeInTheDocument(); + expect(screen.queryByText("ignored-name")).not.toBeInTheDocument(); + expect(screen.queryByText("Ignored Title")).not.toBeInTheDocument(); }); - it("prefers the resource title over the name", () => { + it("falls back to the URI itself when it has no slash-separated segments", () => { renderWithMantine( {}} + />, + ); + expect(screen.getByText("opaque")).toBeInTheDocument(); + }); + + it("ignores trailing slashes when picking the last segment", () => { + renderWithMantine( + {}} />, ); - expect(screen.getByText("Display X")).toBeInTheDocument(); + expect(screen.getByText("bar")).toBeInTheDocument(); + }); + + it("shows the full URI in a tooltip on hover", async () => { + const user = userEvent.setup(); + renderWithMantine( + {}} + />, + ); + await user.hover(screen.getByText("config.json")); + expect( + await screen.findByText("file:///foo/bar/config.json"), + ).toBeInTheDocument(); }); it("renders the last updated timestamp when present", () => { diff --git a/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx index 84b4d0c8f..1b96579fb 100644 --- a/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx +++ b/clients/web/src/components/groups/ResourceSubscribedItem/ResourceSubscribedItem.tsx @@ -1,4 +1,4 @@ -import { Button, Group, Stack, Text } from "@mantine/core"; +import { Button, Group, Stack, Text, Tooltip } from "@mantine/core"; import type { InspectorResourceSubscription } from "../../../../../../core/mcp/types.js"; export interface ResourceSubscribedItemProps { @@ -9,6 +9,7 @@ export interface ResourceSubscribedItemProps { const NameText = Text.withProps({ size: "sm", fw: 500, + truncate: "end", }); const TimestampText = Text.withProps({ @@ -24,12 +25,27 @@ const SubtleButton = Button.withProps({ const ItemRow = Group.withProps({ justify: "space-between", wrap: "nowrap", + gap: "xs", +}); + +const NameStack = Stack.withProps({ + gap: 2, + flex: 1, + miw: 0, }); function formatLastUpdated(date: Date): string { return date.toLocaleString(); } +// Strip the URI down to its last non-empty path segment so the tile shows +// a compact label (e.g. `file:///foo/bar/config.json` → `config.json`). +// The full URI is restored via a tooltip on hover. +function lastUriSegment(uri: string): string { + const segments = uri.split("/").filter(Boolean); + return segments[segments.length - 1] ?? uri; +} + export function ResourceSubscribedItem({ subscription, onUnsubscribe, @@ -37,12 +53,14 @@ export function ResourceSubscribedItem({ const { resource, lastUpdated } = subscription; return ( - - {resource.title ?? resource.name} + + + {lastUriSegment(resource.uri)} + {lastUpdated && ( {formatLastUpdated(lastUpdated)} )} - + Unsubscribe ); diff --git a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.test.tsx b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.test.tsx index 3e1871162..c9a7ef842 100644 --- a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.test.tsx +++ b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.test.tsx @@ -137,4 +137,95 @@ describe("ResourceTemplatePanel", () => { screen.getByRole("button", { name: "Read Resource" }), ).not.toBeDisabled(); }); + + describe("completions", () => { + it("calls onCompleteArgument (debounced) and surfaces values when supported", async () => { + const user = userEvent.setup(); + const onCompleteArgument = vi + .fn< + ( + argName: string, + value: string, + context: Record, + ) => Promise + >() + .mockResolvedValue(["alpha", "alphabet"]); + + renderWithMantine( + , + ); + + await user.type(screen.getByRole("textbox", { name: "userId" }), "al"); + // Wait past the 300ms debounce. + await new Promise((r) => setTimeout(r, 400)); + expect(onCompleteArgument).toHaveBeenCalledTimes(1); + expect(onCompleteArgument).toHaveBeenCalledWith("userId", "al", {}); + + // Server-returned values surface in the Autocomplete dropdown. + expect(await screen.findByText("alpha")).toBeInTheDocument(); + expect(screen.getByText("alphabet")).toBeInTheDocument(); + }); + + it("passes sibling variables as completion context", async () => { + const user = userEvent.setup(); + const onCompleteArgument = vi + .fn< + ( + argName: string, + value: string, + context: Record, + ) => Promise + >() + .mockResolvedValue([]); + + renderWithMantine( + , + ); + + await user.type( + screen.getByRole("textbox", { name: "tableName" }), + "users", + ); + await new Promise((r) => setTimeout(r, 400)); + // The completing arg ("tableName") is excluded from context; only + // the other variables ride along. + expect(onCompleteArgument).toHaveBeenLastCalledWith( + "tableName", + "users", + { rowId: "" }, + ); + + await user.type(screen.getByRole("textbox", { name: "rowId" }), "42"); + await new Promise((r) => setTimeout(r, 400)); + expect(onCompleteArgument).toHaveBeenLastCalledWith("rowId", "42", { + tableName: "users", + }); + }); + + it("does not call onCompleteArgument when completions are unsupported", async () => { + const user = userEvent.setup(); + const onCompleteArgument = vi.fn(); + renderWithMantine( + , + ); + await user.type(screen.getByLabelText("userId"), "ab"); + await new Promise((r) => setTimeout(r, 400)); + expect(onCompleteArgument).not.toHaveBeenCalled(); + }); + }); }); diff --git a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx index b7e76e300..1368fa9bc 100644 --- a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx +++ b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx @@ -1,5 +1,13 @@ -import { useState, useMemo } from "react"; -import { Button, Group, Stack, Text, TextInput, Title } from "@mantine/core"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + Autocomplete, + Button, + Group, + Stack, + Text, + TextInput, + Title, +} from "@mantine/core"; import type { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js"; import { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge"; import { CopyButton } from "../../elements/CopyButton/CopyButton"; @@ -7,8 +15,27 @@ import { CopyButton } from "../../elements/CopyButton/CopyButton"; export interface ResourceTemplatePanelProps { template: ResourceTemplate; onReadResource: (uri: string) => void; + /** + * When provided, each keystroke in a variable input dispatches a + * (debounced) `completion/complete` request to the server. The + * resolved values are surfaced as a dropdown via Mantine `Autocomplete`. + * Wire to `InspectorClient.getCompletions` in the host App. + */ + onCompleteArgument?: ( + argumentName: string, + argumentValue: string, + context: Record, + ) => Promise; + /** + * Gates whether to render Autocomplete (with live completions) vs the + * plain TextInput. Typically derived from the server's + * `completions` capability. + */ + completionsSupported?: boolean; } +const COMPLETION_DEBOUNCE_MS = 300; + function parseVariableNames(uriTemplate: string): string[] { const names: string[] = []; const regex = /\{(\w+)\}/g; @@ -69,6 +96,8 @@ const AnnotationGroup = Group.withProps({ export function ResourceTemplatePanel({ template, onReadResource, + onCompleteArgument, + completionsSupported = false, }: ResourceTemplatePanelProps) { const { name, title, uriTemplate, description, annotations } = template; @@ -80,9 +109,79 @@ export function ResourceTemplatePanel({ const [variables, setVariables] = useState>(() => Object.fromEntries(variableNames.map((n) => [n, ""])), ); + const [completions, setCompletions] = useState>({}); + + // Reset state when the user switches to a different template. + useEffect(() => { + setVariables(Object.fromEntries(variableNames.map((n) => [n, ""]))); + setCompletions({}); + }, [uriTemplate, variableNames]); + + // Latest in-flight controller per argument, so a faster keystroke can + // abort an outstanding completion request and the late response can't + // overwrite the fresh one. + const requestsRef = useRef>(new Map()); + // Debounce timer per argument so we don't spam the server on every key. + const timersRef = useRef>>( + new Map(), + ); + + // Drop pending timers / abort in-flight requests on unmount. + useEffect(() => { + const timers = timersRef.current; + const requests = requestsRef.current; + return () => { + for (const t of timers.values()) clearTimeout(t); + timers.clear(); + for (const c of requests.values()) c.abort(); + requests.clear(); + }; + }, []); + + const useAutocomplete = completionsSupported && !!onCompleteArgument; + + const runCompletion = useCallback( + async (varName: string, value: string, context: Record) => { + if (!onCompleteArgument) return; + requestsRef.current.get(varName)?.abort(); + const controller = new AbortController(); + requestsRef.current.set(varName, controller); + try { + const values = await onCompleteArgument(varName, value, context); + if (controller.signal.aborted) return; + setCompletions((prev) => ({ ...prev, [varName]: values })); + } catch { + if (!controller.signal.aborted) { + setCompletions((prev) => ({ ...prev, [varName]: [] })); + } + } finally { + if (requestsRef.current.get(varName) === controller) { + requestsRef.current.delete(varName); + } + } + }, + [onCompleteArgument], + ); function handleVariableChange(varName: string, value: string) { - setVariables((prev) => ({ ...prev, [varName]: value })); + setVariables((prev) => { + const next = { ...prev, [varName]: value }; + if (useAutocomplete) { + // Schedule a debounced completion call. The `context` carries the + // other variables' current values so the server can disambiguate + // when one variable depends on another. + const context: Record = { ...next }; + delete context[varName]; + const existing = timersRef.current.get(varName); + if (existing) clearTimeout(existing); + const timer = setTimeout(() => { + timersRef.current.delete(varName); + void runCompletion(varName, value, context); + }, COMPLETION_DEBOUNCE_MS); + timersRef.current.set(varName, timer); + } + return next; + }); } const canSubmit = variableNames.every((n) => variables[n]?.length > 0); @@ -104,17 +203,33 @@ export function ResourceTemplatePanel({ {description && {description}} - {variableNames.map((varName) => ( - - handleVariableChange(varName, e.currentTarget.value) - } - /> - ))} + {variableNames.map((varName) => + useAutocomplete ? ( + options} + onChange={(value) => handleVariableChange(varName, value)} + /> + ) : ( + + handleVariableChange(varName, e.currentTarget.value) + } + /> + ), + )} diff --git a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.test.tsx b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.test.tsx index ddae8039e..8991754a8 100644 --- a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.test.tsx +++ b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.test.tsx @@ -110,15 +110,35 @@ describe("ResourcesScreen", () => { await user.click(screen.getByText("Templates (1)")); await user.click(screen.getByText("files")); expect( - screen.getByText("Enter a URI and click Read to preview"), + screen.getByRole("button", { name: "Read Resource" }), ).toBeInTheDocument(); }); - it("renders empty state when a resource is selected but no readState", async () => { + it("hides the template panel once the user reads the resource", async () => { const user = userEvent.setup(); - renderWithMantine(); + const onReadResource = vi.fn(); + renderWithMantine( + , + ); + await user.click(screen.getByText("Templates (1)")); + await user.click(screen.getByText("files")); + await user.type(screen.getByLabelText("path"), "alpha"); + await user.click(screen.getByRole("button", { name: "Read Resource" })); + expect(onReadResource).toHaveBeenCalledWith("file:///alpha"); + // After read, the template form is gone and the preview branch is active. + expect( + screen.queryByRole("button", { name: "Read Resource" }), + ).not.toBeInTheDocument(); + }); + + it("auto-reads when a resource is clicked in the sidebar", async () => { + const user = userEvent.setup(); + const onReadResource = vi.fn(); + renderWithMantine( + , + ); await user.click(screen.getByText("x.txt")); - expect(screen.getByText("Click to read this resource")).toBeInTheDocument(); + expect(onReadResource).toHaveBeenCalledWith("file:///x"); }); it("forwards refresh and subscribe events from the preview panel", async () => { diff --git a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx index 08860cff6..b22806e83 100644 --- a/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx +++ b/clients/web/src/components/screens/ResourcesScreen/ResourcesScreen.tsx @@ -1,14 +1,5 @@ import { useState } from "react"; -import { - Alert, - Card, - Flex, - Group, - Loader, - ScrollArea, - Stack, - Text, -} from "@mantine/core"; +import { Alert, Card, Flex, Loader, Stack, Text } from "@mantine/core"; import type { ReadResourceResult, Resource, @@ -34,10 +25,19 @@ export interface ResourcesScreenProps { subscriptions: InspectorResourceSubscription[]; readState?: ReadResourceState; listChanged: boolean; + completionsSupported?: boolean; onRefreshList: () => void; onReadResource: (uri: string) => void; onSubscribeResource: (uri: string) => void; onUnsubscribeResource: (uri: string) => void; + onCompleteArgument?: ( + ref: + | { type: "ref/resource"; uri: string } + | { type: "ref/prompt"; name: string }, + argumentName: string, + argumentValue: string, + context: Record, + ) => Promise; } const ScreenLayout = Flex.withProps({ @@ -62,6 +62,26 @@ const DetailCard = Card.withProps({ padding: "lg", }); +// Card that sizes to its content but caps at the screen's available +// height. When content fits, the card stays compact (footer sits right +// under the body); when content would overflow, the inner ScrollArea +// inside ResourcePreviewPanel shrinks and scrolls. +const PreviewCard = Card.withProps({ + withBorder: true, + padding: "lg", + variant: "preview", +}); + +// Column that pins the preview card to the top of the available space +// and bounds its growth via the consumer-set `mah`. The card inside +// keeps its natural height up to that cap. +const PreviewPane = Flex.withProps({ + flex: 1, + miw: 0, + direction: "column", + align: "stretch", +}); + const EmptyState = Text.withProps({ c: "dimmed", ta: "center", @@ -77,10 +97,12 @@ export function ResourcesScreen({ subscriptions, readState, listChanged, + completionsSupported, onRefreshList, onReadResource, onSubscribeResource, onUnsubscribeResource, + onCompleteArgument, }: ResourcesScreenProps) { const [selectedResourceUri, setSelectedResourceUri] = useState< string | undefined @@ -107,6 +129,7 @@ export function ResourcesScreen({ function handleSelectResource(uri: string) { setSelectedTemplateUri(undefined); setSelectedResourceUri(uri); + onReadResource(uri); } function handleSelectTemplate(uriTemplate: string) { @@ -115,6 +138,11 @@ export function ResourcesScreen({ } function handleReadResource(uri: string) { + // Once the user reads (either from the template form or a refresh + // inside the preview panel), hand the screen over to the preview: + // clearing the template selection hides the template form so only + // the rendered resource is shown. + setSelectedTemplateUri(undefined); setSelectedResourceUri(uri); onReadResource(uri); } @@ -124,28 +152,28 @@ export function ResourcesScreen({ if (readState.status === "pending") { return ( - + Reading resource... - + ); } if (readState.status === "error") { return ( - + {readState.error ?? "Failed to read resource"} - + ); } if (readState.result && readResource) { return ( - + onSubscribeResource(readResource.uri)} onUnsubscribe={() => onUnsubscribeResource(readResource.uri)} /> - + ); } @@ -182,31 +210,44 @@ export function ResourcesScreen({ {selectedTemplate ? ( - - - - - - - - {renderReadState() ?? ( - - Enter a URI and click Read to preview - - )} - - - ) : selectedResource ? ( - - {renderReadState() ?? ( - - Click to read this resource - - )} - + // Template form only — once the user clicks Read Resource, + // handleReadResource clears the template selection so the + // resource branch takes over and the preview is shown alone. + // maw=40% keeps the form from stretching across the whole + // main area; an unconstrained text input + Read button at + // viewport width looks weird, especially on wide displays. + + + + onCompleteArgument( + { + type: "ref/resource", + uri: selectedTemplate.uriTemplate, + }, + argName, + value, + context, + ) + : undefined + } + /> + + + ) : readResource ? ( + // Sized-to-content preview pane, capped at the screen's available + // height. When the resource body fits, the card hugs its content + // and the subscribe/refresh row sits right under it. When the body + // would overflow, the inner ScrollArea inside ResourcePreviewPanel + // shrinks and scrolls, keeping the footer pinned at the cap. + // miw=0 prevents wide content (long unbroken lines, tables) from + // pushing the pane past the viewport's right edge. + {renderReadState()} ) : ( Select a resource to preview diff --git a/clients/web/src/components/views/InspectorView/InspectorView.tsx b/clients/web/src/components/views/InspectorView/InspectorView.tsx index c8e940512..ef13e1416 100644 --- a/clients/web/src/components/views/InspectorView/InspectorView.tsx +++ b/clients/web/src/components/views/InspectorView/InspectorView.tsx @@ -174,6 +174,15 @@ export interface InspectorViewProps { onSubscribeResource: (uri: string) => void; onUnsubscribeResource: (uri: string) => void; onRefreshResources: () => void; + onCompleteArgument?: ( + ref: + | { type: "ref/resource"; uri: string } + | { type: "ref/prompt"; name: string }, + argumentName: string, + argumentValue: string, + context: Record, + ) => Promise; + completionsSupported?: boolean; onCancelTask: (taskId: string) => void; onClearCompletedTasks: () => void; @@ -240,6 +249,8 @@ export function InspectorView({ onSubscribeResource, onUnsubscribeResource, onRefreshResources, + onCompleteArgument, + completionsSupported, onCancelTask, onClearCompletedTasks, onRefreshTasks, @@ -394,10 +405,12 @@ export function InspectorView({ subscriptions={subscriptions} readState={readResourceState} listChanged={false} + completionsSupported={completionsSupported} onRefreshList={onRefreshResources} onReadResource={onReadResource} onSubscribeResource={onSubscribeResource} onUnsubscribeResource={onUnsubscribeResource} + onCompleteArgument={onCompleteArgument} /> diff --git a/clients/web/src/test/core/mcp/state/resourceSubscriptionsState.test.ts b/clients/web/src/test/core/mcp/state/resourceSubscriptionsState.test.ts new file mode 100644 index 000000000..ac21c06d6 --- /dev/null +++ b/clients/web/src/test/core/mcp/state/resourceSubscriptionsState.test.ts @@ -0,0 +1,222 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; +import type { Resource } from "@modelcontextprotocol/sdk/types.js"; +import { ResourceSubscriptionsState } from "@inspector/core/mcp/state/resourceSubscriptionsState"; +import { ManagedResourcesState } from "@inspector/core/mcp/state/managedResourcesState"; +import { FakeInspectorClient } from "@inspector/core/mcp/__tests__/fakeInspectorClient"; +import type { InspectorResourceSubscription } from "@inspector/core/mcp/types"; + +function resource(uri: string, extras: Partial = {}): Resource { + return { uri, name: uri, ...extras }; +} + +function waitForSubscriptionsChange( + state: ResourceSubscriptionsState, +): Promise { + return new Promise((resolve) => { + state.addEventListener("subscriptionsChange", (e) => resolve(e.detail), { + once: true, + }); + }); +} + +describe("ResourceSubscriptionsState", () => { + let client: FakeInspectorClient; + + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-05-19T10:00:00Z")); + client = new FakeInspectorClient({ status: "connected" }); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("starts empty and getSubscriptions returns a defensive copy", () => { + const state = new ResourceSubscriptionsState(client); + expect(state.getSubscriptions()).toEqual([]); + const a = state.getSubscriptions(); + const b = state.getSubscriptions(); + expect(a).not.toBe(b); + }); + + it("rebuilds subscriptions from resourceSubscriptionsChange events", async () => { + const state = new ResourceSubscriptionsState(client); + const changePromise = waitForSubscriptionsChange(state); + client.dispatchTypedEvent("resourceSubscriptionsChange", [ + "file:///a", + "file:///b", + ]); + const next = await changePromise; + expect(next).toEqual([ + { resource: { uri: "file:///a", name: "file:///a" } }, + { resource: { uri: "file:///b", name: "file:///b" } }, + ]); + }); + + it("resolves Resource references via ManagedResourcesState when provided", async () => { + const resourcesState = new ManagedResourcesState(client); + client.queueResourcePages({ + resources: [resource("file:///a", { name: "Alpha", title: "Title A" })], + }); + await resourcesState.refresh(); + + const state = new ResourceSubscriptionsState(client, resourcesState); + const changePromise = waitForSubscriptionsChange(state); + client.dispatchTypedEvent("resourceSubscriptionsChange", [ + "file:///a", + "file:///unknown", + ]); + const next = await changePromise; + expect(next[0].resource).toEqual({ + uri: "file:///a", + name: "Alpha", + title: "Title A", + }); + // Unknown URI falls back to a synthetic Resource + expect(next[1].resource).toEqual({ + uri: "file:///unknown", + name: "file:///unknown", + }); + }); + + it("stamps lastUpdated on resourceUpdated for a tracked URI", async () => { + const state = new ResourceSubscriptionsState(client); + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + expect(state.getSubscriptions()[0].lastUpdated).toBeUndefined(); + + const changePromise = waitForSubscriptionsChange(state); + client.dispatchTypedEvent("resourceUpdated", { uri: "file:///a" }); + const next = await changePromise; + expect(next[0].lastUpdated).toEqual(new Date("2026-05-19T10:00:00Z")); + }); + + it("ignores resourceUpdated for URIs that are not subscribed", () => { + const state = new ResourceSubscriptionsState(client); + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + const handler = vi.fn(); + state.addEventListener("subscriptionsChange", handler); + client.dispatchTypedEvent("resourceUpdated", { + uri: "file:///not-tracked", + }); + expect(handler).not.toHaveBeenCalled(); + expect(state.getSubscriptions()[0].lastUpdated).toBeUndefined(); + }); + + it("preserves lastUpdated across re-subscribes and drops it on unsubscribe", async () => { + const state = new ResourceSubscriptionsState(client); + client.dispatchTypedEvent("resourceSubscriptionsChange", [ + "file:///a", + "file:///b", + ]); + client.dispatchTypedEvent("resourceUpdated", { uri: "file:///a" }); + expect(state.getSubscriptions()[0].lastUpdated).toBeInstanceOf(Date); + + // Unsubscribe from "a", subscribe to "c". lastUpdated for "a" is dropped. + client.dispatchTypedEvent("resourceSubscriptionsChange", [ + "file:///b", + "file:///c", + ]); + expect(state.getSubscriptions().map((s) => s.resource.uri)).toEqual([ + "file:///b", + "file:///c", + ]); + expect(state.getSubscriptions().every((s) => !s.lastUpdated)).toBe(true); + + // Re-subscribe to "a" — no lastUpdated since the prior entry was dropped. + client.dispatchTypedEvent("resourceSubscriptionsChange", [ + "file:///a", + "file:///b", + "file:///c", + ]); + expect(state.getSubscriptions()[0].lastUpdated).toBeUndefined(); + }); + + it("preserves lastUpdated when an unrelated URI is added", () => { + const state = new ResourceSubscriptionsState(client); + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + client.dispatchTypedEvent("resourceUpdated", { uri: "file:///a" }); + const stampedAt = state.getSubscriptions()[0].lastUpdated; + expect(stampedAt).toBeInstanceOf(Date); + + client.dispatchTypedEvent("resourceSubscriptionsChange", [ + "file:///a", + "file:///b", + ]); + const subs = state.getSubscriptions(); + expect(subs[0].lastUpdated).toEqual(stampedAt); + expect(subs[1].lastUpdated).toBeUndefined(); + }); + + it("re-resolves Resource references when ManagedResourcesState refreshes", async () => { + const resourcesState = new ManagedResourcesState(client); + const state = new ResourceSubscriptionsState(client, resourcesState); + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + expect(state.getSubscriptions()[0].resource.name).toBe("file:///a"); + + client.queueResourcePages({ + resources: [resource("file:///a", { name: "Resolved Name" })], + }); + const changePromise = waitForSubscriptionsChange(state); + await resourcesState.refresh(); + const next = await changePromise; + expect(next[0].resource.name).toBe("Resolved Name"); + }); + + it("does not re-emit on resourcesChange when no URIs are subscribed", async () => { + const resourcesState = new ManagedResourcesState(client); + const state = new ResourceSubscriptionsState(client, resourcesState); + const handler = vi.fn(); + state.addEventListener("subscriptionsChange", handler); + + client.queueResourcePages({ resources: [resource("file:///a")] }); + await resourcesState.refresh(); + expect(handler).not.toHaveBeenCalled(); + }); + + it("clears subscriptions on statusChange to disconnected", async () => { + const state = new ResourceSubscriptionsState(client); + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + expect(state.getSubscriptions()).toHaveLength(1); + + const changePromise = waitForSubscriptionsChange(state); + client.setStatus("disconnected"); + const next = await changePromise; + expect(next).toEqual([]); + expect(state.getSubscriptions()).toEqual([]); + }); + + it("does not clear subscriptions on non-disconnected status changes", () => { + const state = new ResourceSubscriptionsState(client); + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + client.setStatus("error"); + expect(state.getSubscriptions()).toHaveLength(1); + }); + + it("destroy unsubscribes from client and resources state events", () => { + const resourcesState = new ManagedResourcesState(client); + const state = new ResourceSubscriptionsState(client, resourcesState); + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + expect(state.getSubscriptions()).toHaveLength(1); + + state.destroy(); + expect(state.getSubscriptions()).toEqual([]); + + // Further events from the client must not affect the destroyed state. + const handler = vi.fn(); + state.addEventListener("subscriptionsChange", handler); + client.dispatchTypedEvent("resourceSubscriptionsChange", [ + "file:///a", + "file:///b", + ]); + client.dispatchTypedEvent("resourceUpdated", { uri: "file:///a" }); + expect(handler).not.toHaveBeenCalled(); + expect(state.getSubscriptions()).toEqual([]); + }); + + it("destroy is idempotent", () => { + const state = new ResourceSubscriptionsState(client); + state.destroy(); + expect(() => state.destroy()).not.toThrow(); + }); +}); diff --git a/clients/web/src/test/core/react/useResourceSubscriptions.test.tsx b/clients/web/src/test/core/react/useResourceSubscriptions.test.tsx new file mode 100644 index 000000000..164444fc5 --- /dev/null +++ b/clients/web/src/test/core/react/useResourceSubscriptions.test.tsx @@ -0,0 +1,69 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { act, renderHook, waitFor } from "@testing-library/react"; +import { FakeInspectorClient } from "@inspector/core/mcp/__tests__/fakeInspectorClient"; +import { ResourceSubscriptionsState } from "@inspector/core/mcp/state/resourceSubscriptionsState"; +import { useResourceSubscriptions } from "@inspector/core/react/useResourceSubscriptions"; + +describe("useResourceSubscriptions", () => { + let client: FakeInspectorClient; + let state: ResourceSubscriptionsState; + + beforeEach(() => { + client = new FakeInspectorClient({ status: "connected" }); + state = new ResourceSubscriptionsState(client); + }); + + it("returns the initial snapshot from the state", () => { + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + const { result } = renderHook(() => useResourceSubscriptions(state)); + expect(result.current.subscriptions.map((s) => s.resource.uri)).toEqual([ + "file:///a", + ]); + }); + + it("returns empty subscriptions when state is null", () => { + const { result } = renderHook(() => useResourceSubscriptions(null)); + expect(result.current.subscriptions).toEqual([]); + }); + + it("updates when state dispatches subscriptionsChange", async () => { + const { result } = renderHook(() => useResourceSubscriptions(state)); + expect(result.current.subscriptions).toEqual([]); + + act(() => { + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + }); + + await waitFor(() => { + expect(result.current.subscriptions.map((s) => s.resource.uri)).toEqual([ + "file:///a", + ]); + }); + }); + + it("resets to empty when the state prop becomes null", async () => { + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + const { result, rerender } = renderHook( + ({ s }: { s: ResourceSubscriptionsState | null }) => + useResourceSubscriptions(s), + { initialProps: { s: state as ResourceSubscriptionsState | null } }, + ); + await waitFor(() => { + expect(result.current.subscriptions).toHaveLength(1); + }); + + rerender({ s: null }); + await waitFor(() => { + expect(result.current.subscriptions).toEqual([]); + }); + }); + + it("unsubscribes from the state on unmount", () => { + const { result, unmount } = renderHook(() => + useResourceSubscriptions(state), + ); + unmount(); + client.dispatchTypedEvent("resourceSubscriptionsChange", ["file:///a"]); + expect(result.current.subscriptions).toEqual([]); + }); +}); diff --git a/clients/web/src/theme/Card.ts b/clients/web/src/theme/Card.ts index 14d75f038..e00765c5f 100644 --- a/clients/web/src/theme/Card.ts +++ b/clients/web/src/theme/Card.ts @@ -20,6 +20,19 @@ export const ThemeCard = Card.extend({ }, }; } + if (props.variant === "preview") { + // Container for the resource preview / template form panels: sizes to + // content (no forced height) but caps at the screen's available area + // via consumer-set `mah`. `overflow: hidden` lets a flex-shrunk inner + // ScrollArea take over scrolling when content exceeds the cap, instead + // of the whole card bleeding past the viewport. + return { + root: { + backgroundColor: "var(--inspector-surface-card)", + overflow: "hidden", + }, + }; + } return { root: { backgroundColor: "var(--inspector-surface-card)" }, }; diff --git a/core/mcp/__tests__/fakeInspectorClient.ts b/core/mcp/__tests__/fakeInspectorClient.ts index 9881a1d06..c0ccd1859 100644 --- a/core/mcp/__tests__/fakeInspectorClient.ts +++ b/core/mcp/__tests__/fakeInspectorClient.ts @@ -120,6 +120,20 @@ export class FakeInspectorClient setLoggingLevel = vi.fn(async (_level: LoggingLevel) => {}); + getCompletions = vi.fn( + async ( + _ref: + | { type: "ref/resource"; uri: string } + | { type: "ref/prompt"; name: string }, + _argumentName: string, + _argumentValue: string, + _context?: Record, + _metadata?: Record, + ): Promise<{ values: string[]; total?: number; hasMore?: boolean }> => ({ + values: [], + }), + ); + constructor(options: FakeInspectorClientOptions = {}) { super(); this.status = options.status ?? "disconnected"; diff --git a/core/mcp/inspectorClientProtocol.ts b/core/mcp/inspectorClientProtocol.ts index 1a64f6a15..612010d14 100644 --- a/core/mcp/inspectorClientProtocol.ts +++ b/core/mcp/inspectorClientProtocol.ts @@ -102,4 +102,15 @@ export interface InspectorClientProtocol extends InspectorClientEventTarget { // Misc surface required by hooks/state setLoggingLevel(level: LoggingLevel): Promise; getSessionId(): string | undefined; + + // Completions (resource templates / prompt arguments) + getCompletions( + ref: + | { type: "ref/resource"; uri: string } + | { type: "ref/prompt"; name: string }, + argumentName: string, + argumentValue: string, + context?: Record, + metadata?: Record, + ): Promise<{ values: string[]; total?: number; hasMore?: boolean }>; } diff --git a/core/mcp/state/index.ts b/core/mcp/state/index.ts index 7f1cb2a37..af1e9f65e 100644 --- a/core/mcp/state/index.ts +++ b/core/mcp/state/index.ts @@ -48,3 +48,5 @@ export type { PagedRequestorTasksStateEventMap, LoadPageResult as PagedRequestorTasksLoadPageResult, } from "./pagedRequestorTasksState.js"; +export { ResourceSubscriptionsState } from "./resourceSubscriptionsState.js"; +export type { ResourceSubscriptionsStateEventMap } from "./resourceSubscriptionsState.js"; diff --git a/core/mcp/state/resourceSubscriptionsState.ts b/core/mcp/state/resourceSubscriptionsState.ts new file mode 100644 index 000000000..419ed86f3 --- /dev/null +++ b/core/mcp/state/resourceSubscriptionsState.ts @@ -0,0 +1,149 @@ +/** + * ResourceSubscriptionsState: tracks the live resource-subscription list the + * Resources screen renders. Subscribes to the InspectorClient's + * `resourceSubscriptionsChange` (URI list) and `resourceUpdated` events, + * resolves each URI against the optional `ManagedResourcesState` so subscriptions + * carry the server-supplied Resource (name/title), and stamps `lastUpdated` + * when a `notifications/resources/updated` arrives for a tracked URI. + * + * When no resource is found in the managed list (e.g. a template-expanded URI + * the user subscribed to before the resources list refreshed), a synthetic + * Resource `{ uri, name: uri }` is used — mirroring the fallback pattern in + * ResourcesScreen. If the server later removes a previously-listed resource + * while the user is still subscribed, the tile regresses to that synthetic + * form: the managed list is the source of truth, so displaying a stale name + * for a server-removed resource is intentionally avoided. + */ + +import type { InspectorClientProtocol } from "../inspectorClientProtocol.js"; +import type { InspectorClientEventMap } from "../inspectorClientEventTarget.js"; +import type { + ManagedResourcesState, + ManagedResourcesStateEventMap, +} from "./managedResourcesState.js"; +import type { InspectorResourceSubscription } from "../types.js"; +import type { Resource } from "@modelcontextprotocol/sdk/types.js"; +import { + TypedEventTarget, + type TypedEventGeneric, +} from "../typedEventTarget.js"; + +export interface ResourceSubscriptionsStateEventMap { + subscriptionsChange: InspectorResourceSubscription[]; +} + +/** + * State manager that mirrors `InspectorClient.subscribedResources` as a list of + * `InspectorResourceSubscription` objects keyed by URI, preserving each + * subscription's `lastUpdated` across re-derivations. + */ +export class ResourceSubscriptionsState extends TypedEventTarget { + private subscribedUris: string[] = []; + private lastUpdatedByUri: Map = new Map(); + private subscriptions: InspectorResourceSubscription[] = []; + private client: InspectorClientProtocol | null = null; + private resourcesState: ManagedResourcesState | null = null; + private unsubscribe: (() => void) | null = null; + + constructor( + client: InspectorClientProtocol, + resourcesState: ManagedResourcesState | null = null, + ) { + super(); + this.client = client; + this.resourcesState = resourcesState; + + const onSubscriptionsChange = ( + event: TypedEventGeneric< + InspectorClientEventMap, + "resourceSubscriptionsChange" + >, + ): void => { + this.subscribedUris = event.detail; + // Drop lastUpdated entries for URIs no longer subscribed + const active = new Set(event.detail); + for (const uri of this.lastUpdatedByUri.keys()) { + if (!active.has(uri)) this.lastUpdatedByUri.delete(uri); + } + this.rebuild(); + }; + + const onResourceUpdated = ( + event: TypedEventGeneric, + ): void => { + const { uri } = event.detail; + // Belt-and-braces: the client's dispatch site is already guarded by + // subscribedResources.has(uri), so this re-check should be redundant. + // It stays correct if a future change ever decouples dispatch from + // subscription state. + if (!this.subscribedUris.includes(uri)) return; + this.lastUpdatedByUri.set(uri, new Date()); + this.rebuild(); + }; + + const onStatusChange = (): void => { + if (this.client?.getStatus() === "disconnected") { + this.subscribedUris = []; + this.lastUpdatedByUri.clear(); + this.subscriptions = []; + this.dispatchTypedEvent("subscriptionsChange", this.getSubscriptions()); + } + }; + + const onResourcesChange = ( + _event: TypedEventGeneric< + ManagedResourcesStateEventMap, + "resourcesChange" + >, + ): void => { + // Re-resolve Resource references in case names/titles changed server-side. + if (this.subscribedUris.length > 0) this.rebuild(); + }; + + client.addEventListener( + "resourceSubscriptionsChange", + onSubscriptionsChange, + ); + client.addEventListener("resourceUpdated", onResourceUpdated); + client.addEventListener("statusChange", onStatusChange); + resourcesState?.addEventListener("resourcesChange", onResourcesChange); + + this.unsubscribe = () => { + this.client?.removeEventListener( + "resourceSubscriptionsChange", + onSubscriptionsChange, + ); + this.client?.removeEventListener("resourceUpdated", onResourceUpdated); + this.client?.removeEventListener("statusChange", onStatusChange); + this.resourcesState?.removeEventListener( + "resourcesChange", + onResourcesChange, + ); + this.client = null; + this.resourcesState = null; + }; + } + + getSubscriptions(): InspectorResourceSubscription[] { + return [...this.subscriptions]; + } + + private rebuild(): void { + const resources = this.resourcesState?.getResources() ?? []; + const byUri = new Map(resources.map((r) => [r.uri, r])); + this.subscriptions = this.subscribedUris.map((uri) => { + const resource: Resource = byUri.get(uri) ?? { uri, name: uri }; + const lastUpdated = this.lastUpdatedByUri.get(uri); + return lastUpdated ? { resource, lastUpdated } : { resource }; + }); + this.dispatchTypedEvent("subscriptionsChange", this.getSubscriptions()); + } + + destroy(): void { + this.unsubscribe?.(); + this.unsubscribe = null; + this.subscribedUris = []; + this.lastUpdatedByUri.clear(); + this.subscriptions = []; + } +} diff --git a/core/react/useResourceSubscriptions.ts b/core/react/useResourceSubscriptions.ts new file mode 100644 index 000000000..579ab5aa7 --- /dev/null +++ b/core/react/useResourceSubscriptions.ts @@ -0,0 +1,46 @@ +import { useState, useEffect } from "react"; +import type { + ResourceSubscriptionsState, + ResourceSubscriptionsStateEventMap, +} from "../mcp/state/resourceSubscriptionsState.js"; +import type { InspectorResourceSubscription } from "../mcp/types.js"; +import type { TypedEventGeneric } from "../mcp/typedEventTarget.js"; + +export interface UseResourceSubscriptionsResult { + subscriptions: InspectorResourceSubscription[]; +} + +/** + * React hook that subscribes to ResourceSubscriptionsState and returns the + * current InspectorResourceSubscription[]. When the state is null (no active + * server), returns an empty array. + */ +export function useResourceSubscriptions( + state: ResourceSubscriptionsState | null, +): UseResourceSubscriptionsResult { + const [subscriptions, setSubscriptions] = useState< + InspectorResourceSubscription[] + >(state?.getSubscriptions() ?? []); + + useEffect(() => { + if (!state) { + setSubscriptions([]); + return; + } + setSubscriptions(state.getSubscriptions()); + const onSubscriptionsChange = ( + event: TypedEventGeneric< + ResourceSubscriptionsStateEventMap, + "subscriptionsChange" + >, + ) => { + setSubscriptions(event.detail); + }; + state.addEventListener("subscriptionsChange", onSubscriptionsChange); + return () => { + state.removeEventListener("subscriptionsChange", onSubscriptionsChange); + }; + }, [state]); + + return { subscriptions }; +}