diff --git a/extensions/markdown-basics/cgmanifest.json b/extensions/markdown-basics/cgmanifest.json index 82e47b637e14a..91686a02c68d8 100644 --- a/extensions/markdown-basics/cgmanifest.json +++ b/extensions/markdown-basics/cgmanifest.json @@ -33,7 +33,7 @@ "git": { "name": "microsoft/vscode-markdown-tm-grammar", "repositoryUrl": "https://github.com/microsoft/vscode-markdown-tm-grammar", - "commitHash": "548ccb91ef58ba40ac745b400d889933ccd5eb4d" + "commitHash": "0812fc4b190efc17bfed0d5b4ff918eff8e4e377" } }, "license": "MIT", diff --git a/extensions/markdown-basics/package.json b/extensions/markdown-basics/package.json index 6eadec35592d7..baf50c80bb281 100644 --- a/extensions/markdown-basics/package.json +++ b/extensions/markdown-basics/package.json @@ -65,9 +65,11 @@ "meta.embedded.block.go": "go", "meta.embedded.block.groovy": "groovy", "meta.embedded.block.pug": "jade", + "meta.embedded.block.ignore": "ignore", "meta.embedded.block.javascript": "javascript", "meta.embedded.block.json": "json", "meta.embedded.block.jsonc": "jsonc", + "meta.embedded.block.jsonl": "jsonl", "meta.embedded.block.latex": "latex", "meta.embedded.block.less": "less", "meta.embedded.block.objc": "objc", @@ -75,6 +77,7 @@ "meta.embedded.block.perl6": "perl6", "meta.embedded.block.powershell": "powershell", "meta.embedded.block.python": "python", + "meta.embedded.block.restructuredtext": "restructuredtext", "meta.embedded.block.rust": "rust", "meta.embedded.block.scala": "scala", "meta.embedded.block.shellscript": "shellscript", diff --git a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json index c6d5110bd02e4..ac6f7b5ee6a31 100644 --- a/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json +++ b/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/548ccb91ef58ba40ac745b400d889933ccd5eb4d", + "version": "https://github.com/microsoft/vscode-markdown-tm-grammar/commit/0812fc4b190efc17bfed0d5b4ff918eff8e4e377", "name": "Markdown", "scopeName": "text.html.markdown", "patterns": [ @@ -959,6 +959,39 @@ } ] }, + "fenced_code_block_ignore": { + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(gitignore|ignore)((\\s+|:|,|\\{|\\?)[^`]*)?$)", + "name": "markup.fenced_code.block.markdown", + "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", + "beginCaptures": { + "3": { + "name": "punctuation.definition.markdown" + }, + "4": { + "name": "fenced_code.block.language.markdown" + }, + "5": { + "name": "fenced_code.block.language.attributes.markdown" + } + }, + "endCaptures": { + "3": { + "name": "punctuation.definition.markdown" + } + }, + "patterns": [ + { + "begin": "(^|\\G)(\\s*)(.*)", + "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", + "contentName": "meta.embedded.block.ignore", + "patterns": [ + { + "include": "source.ignore" + } + ] + } + ] + }, "fenced_code_block_js": { "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(js|jsx|javascript|es6|mjs|cjs|dataviewjs|\\{\\.js.+?\\})((\\s+|:|,|\\{|\\?)[^`]*)?$)", "name": "markup.fenced_code.block.markdown", @@ -1091,6 +1124,39 @@ } ] }, + "fenced_code_block_jsonl": { + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(jsonl|jsonlines)((\\s+|:|,|\\{|\\?)[^`]*)?$)", + "name": "markup.fenced_code.block.markdown", + "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", + "beginCaptures": { + "3": { + "name": "punctuation.definition.markdown" + }, + "4": { + "name": "fenced_code.block.language.markdown" + }, + "5": { + "name": "fenced_code.block.language.attributes.markdown" + } + }, + "endCaptures": { + "3": { + "name": "punctuation.definition.markdown" + } + }, + "patterns": [ + { + "begin": "(^|\\G)(\\s*)(.*)", + "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", + "contentName": "meta.embedded.block.jsonl", + "patterns": [ + { + "include": "source.json.lines" + } + ] + } + ] + }, "fenced_code_block_less": { "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(less)((\\s+|:|,|\\{|\\?)[^`]*)?$)", "name": "markup.fenced_code.block.markdown", @@ -1916,6 +1982,105 @@ } ] }, + "fenced_code_block_yang": { + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(yang)((\\s+|:|,|\\{|\\?)[^`]*)?$)", + "name": "markup.fenced_code.block.markdown", + "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", + "beginCaptures": { + "3": { + "name": "punctuation.definition.markdown" + }, + "4": { + "name": "fenced_code.block.language.markdown" + }, + "5": { + "name": "fenced_code.block.language.attributes.markdown" + } + }, + "endCaptures": { + "3": { + "name": "punctuation.definition.markdown" + } + }, + "patterns": [ + { + "begin": "(^|\\G)(\\s*)(.*)", + "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", + "contentName": "meta.embedded.block.yang", + "patterns": [ + { + "include": "source.yang" + } + ] + } + ] + }, + "fenced_code_block_abap": { + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(abap)((\\s+|:|,|\\{|\\?)[^`]*)?$)", + "name": "markup.fenced_code.block.markdown", + "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", + "beginCaptures": { + "3": { + "name": "punctuation.definition.markdown" + }, + "4": { + "name": "fenced_code.block.language.markdown" + }, + "5": { + "name": "fenced_code.block.language.attributes.markdown" + } + }, + "endCaptures": { + "3": { + "name": "punctuation.definition.markdown" + } + }, + "patterns": [ + { + "begin": "(^|\\G)(\\s*)(.*)", + "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", + "contentName": "meta.embedded.block.abap", + "patterns": [ + { + "include": "source.abap" + } + ] + } + ] + }, + "fenced_code_block_restructuredtext": { + "begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(restructuredtext|rst)((\\s+|:|,|\\{|\\?)[^`]*)?$)", + "name": "markup.fenced_code.block.markdown", + "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", + "beginCaptures": { + "3": { + "name": "punctuation.definition.markdown" + }, + "4": { + "name": "fenced_code.block.language.markdown" + }, + "5": { + "name": "fenced_code.block.language.attributes.markdown" + } + }, + "endCaptures": { + "3": { + "name": "punctuation.definition.markdown" + } + }, + "patterns": [ + { + "begin": "(^|\\G)(\\s*)(.*)", + "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", + "contentName": "meta.embedded.block.restructuredtext", + "patterns": [ + { + "include": "source.rst" + } + ] + } + ] + }, "fenced_code_block": { "patterns": [ { @@ -1999,6 +2164,9 @@ { "include": "#fenced_code_block_pug" }, + { + "include": "#fenced_code_block_ignore" + }, { "include": "#fenced_code_block_js" }, @@ -2011,6 +2179,9 @@ { "include": "#fenced_code_block_jsonc" }, + { + "include": "#fenced_code_block_jsonl" + }, { "include": "#fenced_code_block_less" }, @@ -2086,6 +2257,15 @@ { "include": "#fenced_code_block_twig" }, + { + "include": "#fenced_code_block_yang" + }, + { + "include": "#fenced_code_block_abap" + }, + { + "include": "#fenced_code_block_restructuredtext" + }, { "include": "#fenced_code_block_unknown" } diff --git a/package-lock.json b/package-lock.json index 91bd7824ee14f..cef5e6a90ae2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,7 +96,7 @@ "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.9.1", "debounce": "^1.0.0", - "deemon": "^1.13.5", + "deemon": "^1.13.6", "electron": "37.3.1", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", @@ -151,7 +151,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^6.0.0-dev.20250825", + "typescript": "^6.0.0-dev.20250827", "typescript-eslint": "^8.39.0", "util": "^0.12.4", "webpack": "^5.94.0", @@ -2756,9 +2756,9 @@ } }, "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20250825.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20250825.1.tgz", - "integrity": "sha512-DQB0+7nluvRjON/qc/4k6eYxbHII5sMCvMzUX1OOyT6h8nBXATfogBtqQ/NJ3193p+U/9cEgN0uIR+WBcBOuDw==", + "version": "7.0.0-dev.20250827.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20250827.1.tgz", + "integrity": "sha512-AdSaD57/ZXM4UEyuzfqt4daiGeN2y8CFogwAKNjyQV6muQWi5QCmuLNkEbQk7Teg33lCeycrHCTjz9aPgbrJIw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2768,19 +2768,19 @@ "node": ">=20.6.0" }, "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250825.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250825.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20250825.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250825.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20250825.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250825.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20250825.1" + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250827.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250827.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20250827.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250827.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20250827.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250827.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20250827.1" } }, "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20250825.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20250825.1.tgz", - "integrity": "sha512-RT1IMIimoSK4xlcRVDtEwN3m1T5a9FDgdRZSDYlZsQr9e09GkaIAJgPv5yLkpFbAAq57BiBonNM8+hFJcYyH0A==", + "version": "7.0.0-dev.20250827.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20250827.1.tgz", + "integrity": "sha512-lTgoQK7wkbkb/gxNn6ZcdpBtEh+tBgXJQbl7feMP/BdpNvFJNFKW6DN+1wlXAJj47GYfMvmCN+vk571a9oO4cw==", "cpu": [ "arm64" ], @@ -2795,9 +2795,9 @@ } }, "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20250825.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20250825.1.tgz", - "integrity": "sha512-k8/wn0POmMoXczdgywN9gTGiTql3ICJBoKIvdiE3yjJHSipjQcX9WbQ0HSCfX2cliVNFRANfoKArsJUIk9fN3w==", + "version": "7.0.0-dev.20250827.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20250827.1.tgz", + "integrity": "sha512-xPyU3x81DdhAiuh+cqLYK2733+mQuRqMaHCrdwL8s1dy+swjqnwLq9e685Kj00QndOBXm0y21EQ32uH1okv7Ig==", "cpu": [ "x64" ], @@ -2812,9 +2812,9 @@ } }, "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20250825.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20250825.1.tgz", - "integrity": "sha512-7EqleheGT7N4V5eGNT2n9ZDMoesi190pnYjbolwbIPSZqklZ/4AXApZhYz8vqqALt+TzGBUQ8MWjX/alzDpl5A==", + "version": "7.0.0-dev.20250827.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20250827.1.tgz", + "integrity": "sha512-vvkrXO9HvkWMzKlvdM3y9oV3TEupfD/IWttFuTiKBa2oUSxAo9sptHYkPi43756f1jrVmagCn5PsFWJNO4TA5Q==", "cpu": [ "arm" ], @@ -2829,9 +2829,9 @@ } }, "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20250825.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20250825.1.tgz", - "integrity": "sha512-VfiMRCipZY5i+PwgNDFHH8/Sm5w5/9U7Uf/Jb69ZgSdgZGpYev++6CAnWhl7wzJBAI72kNK1hD2s870j0RiNyw==", + "version": "7.0.0-dev.20250827.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20250827.1.tgz", + "integrity": "sha512-pUKOi7wwNtRCBY/mGtSRAf/6DwOz+5XbFeifWuiFCxPvFEJrbUaF6naAh2d1bJVOCCwiZCCBGMRy708eoclvBw==", "cpu": [ "arm64" ], @@ -2846,9 +2846,9 @@ } }, "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20250825.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250825.1.tgz", - "integrity": "sha512-da+bqP2uOD3BrQZgt5fdgvinkekNMh+9enOmjQkv0tdb53PZ5Sf3xEl+yC43TrLfJAFpYHDBU5WTVnSorryZ2Q==", + "version": "7.0.0-dev.20250827.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20250827.1.tgz", + "integrity": "sha512-W4VRgMd+JdiC6+9S8ai/PqV5dwRfv/U77Es7yfWCR2RsKBloZQXQzwN9bbxErbo3LlGznw/7QZWBJRrMrQuwVg==", "cpu": [ "x64" ], @@ -2863,9 +2863,9 @@ } }, "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20250825.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20250825.1.tgz", - "integrity": "sha512-jivNNnPdc/9OFbMXvCwpwVhwU5gDGb95ER9G5tDhs3zUwynw/Z4pp5DJXDYBqj0oaZBeYDWOyxBKh+TiKYk9qQ==", + "version": "7.0.0-dev.20250827.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20250827.1.tgz", + "integrity": "sha512-vfdlEP+zAHYvPZnyz2HB1FTTjPxYLYl1A30JrEBpod7rqPZU04rah3ykcI6+7BRPehClHBCeoTMC7Bn0hi/CeA==", "cpu": [ "arm64" ], @@ -2880,9 +2880,9 @@ } }, "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20250825.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250825.1.tgz", - "integrity": "sha512-G6bnjriPrsTQ3ZkUg4wzxv8rP7BJ/lrecTMS97BJuHorJCOqF3uoAGFHZRTnlnanxNr2Akm+hD0d77neOZpgFA==", + "version": "7.0.0-dev.20250827.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20250827.1.tgz", + "integrity": "sha512-fuU+XxEbUzqsMEO41HK+oUd5vu4mkP7zi0UIs1HOl9N0h548GaC5MVHTk7ErrvNiRSY8ffNiCxhiy32YjpJD1Q==", "cpu": [ "x64" ], @@ -6156,9 +6156,9 @@ } }, "node_modules/deemon": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.13.5.tgz", - "integrity": "sha512-mmQi4Rz7ehx/xmUoDKzYAfzoEIv0HIGuDL88aj7iIxxYq0OwxRc/edFqso+aUdgame1ZBGLoV4Ucd/5/WCmu4w==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.13.6.tgz", + "integrity": "sha512-+/fizwuGaBl+DwvFgP5I0+cYXCPHrjCBzFegm2xIcVmkC2sPTxK5KRwIVtyY0kIngoqwf9bENDkFEpfjMr0H2g==", "dev": true, "license": "MIT", "dependencies": { @@ -6169,7 +6169,7 @@ "deemon": "src/deemon.js" }, "engines": { - "node": ">=10" + "node": ">=22" } }, "node_modules/deep-equal": { @@ -17553,9 +17553,9 @@ "dev": true }, "node_modules/typescript": { - "version": "6.0.0-dev.20250825", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20250825.tgz", - "integrity": "sha512-pvFX8/dZF3oRmIswsPQKlLDwZwR1FdLYGgpANAO9mX1sraGOS1NZTDZPBi5plkeQ8tOBPstWRjSLZoou9jUcGA==", + "version": "6.0.0-dev.20250827", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20250827.tgz", + "integrity": "sha512-TNrtA9r9AMgInuUMAV5hVTQWClBfvG+HWhIanl7ZO8GWSwjzL9mRgauvwzMfXpdwoAR1h+XC5zSj9WbbGGOtxg==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index fadcb4bf3846f..4ed2f0d67c3ee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.104.0", - "distro": "15940fffbc870ef4790b846e92f19e606420387b", + "distro": "2f34d547f778e973bb1aa6f37a4d85220dd7fc15", "author": { "name": "Microsoft Corporation" }, @@ -156,7 +156,7 @@ "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.9.1", "debounce": "^1.0.0", - "deemon": "^1.13.5", + "deemon": "^1.13.6", "electron": "37.3.1", "eslint": "^9.11.1", "eslint-formatter-compact": "^8.40.0", @@ -211,7 +211,7 @@ "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "typescript": "^6.0.0-dev.20250825", + "typescript": "^6.0.0-dev.20250827", "typescript-eslint": "^8.39.0", "util": "^0.12.4", "webpack": "^5.94.0", diff --git a/src/vs/platform/mcp/common/mcpGalleryManifestService.ts b/src/vs/platform/mcp/common/mcpGalleryManifestService.ts index bc84aa1a477bc..03655a2776c8c 100644 --- a/src/vs/platform/mcp/common/mcpGalleryManifestService.ts +++ b/src/vs/platform/mcp/common/mcpGalleryManifestService.ts @@ -29,18 +29,20 @@ export class McpGalleryManifestService extends Disposable implements IMcpGallery } protected createMcpGalleryManifest(url: string): IMcpGalleryManifest { - const serversUrl = url.endsWith('servers.json') ? url : `${url}/servers`; + const isVSCodeGalleryUrl = this.productService.extensionsGallery?.mcpUrl === url; + const serversUrl = isVSCodeGalleryUrl ? url : `${url}/servers`; const resources = [ { id: serversUrl, type: McpGalleryResourceType.McpQueryService - }, - { - id: `${serversUrl}/{id}`, - type: McpGalleryResourceType.McpServerManifestUri } ]; - + if (!isVSCodeGalleryUrl) { + resources.push({ + id: `${serversUrl}/{id}`, + type: McpGalleryResourceType.McpServerManifestUri + }); + } return { url, resources diff --git a/src/vs/platform/mcp/common/mcpGalleryService.ts b/src/vs/platform/mcp/common/mcpGalleryService.ts index dde9134941da8..bbb593d19ce09 100644 --- a/src/vs/platform/mcp/common/mcpGalleryService.ts +++ b/src/vs/platform/mcp/common/mcpGalleryService.ts @@ -18,6 +18,7 @@ import { IGalleryMcpServer, IMcpGalleryService, IMcpServerManifest, IQueryOption import { IMcpGalleryManifestService, McpGalleryManifestStatus, getMcpGalleryManifestResourceUri, McpGalleryResourceType, IMcpGalleryManifest } from './mcpGalleryManifest.js'; import { IPageIterator, IPager, PageIteratorPager, singlePagePager } from '../../../base/common/paging.js'; import { CancellationError } from '../../../base/common/errors.js'; +import { basename } from '../../../base/common/path.js'; interface IRawGalleryServerMetadata { readonly count: number; @@ -139,13 +140,29 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService }); } - async getMcpServers(names: string[]): Promise { + async getMcpServers(urls: string[]): Promise { const mcpGalleryManifest = await this.mcpGalleryManifestService.getMcpGalleryManifest(); if (!mcpGalleryManifest) { return []; } - const { servers } = await this.queryGalleryMcpServers(new Query(), mcpGalleryManifest, CancellationToken.None); + const mcpServers: IGalleryMcpServer[] = []; + await Promise.allSettled(urls.map(async url => { + const mcpServerUrl = this.getManifestUrlFromId(basename(url), mcpGalleryManifest); + if (mcpServerUrl !== url) { + return; + } + const mcpServer = await this.getMcpServer(mcpServerUrl, mcpGalleryManifest); + if (mcpServer) { + mcpServers.push(mcpServer); + } + })); + + return mcpServers; + } + + async getMcpServersFromVSCodeGallery(names: string[]): Promise { + const servers = await this.fetchMcpServersFromVSCodeGallery(); return servers.filter(item => names.includes(item.name)); } @@ -229,8 +246,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService } let icon: { light: string; dark: string } | undefined; - const mcpGalleryUrl = this.getMcpGalleryUrl(mcpGalleryManifest); - if (mcpGalleryUrl && this.productService.extensionsGallery?.mcpUrl !== mcpGalleryUrl) { + if (this.productService.extensionsGallery?.mcpUrl !== mcpGalleryManifest.url) { if (item.iconUrl) { icon = { light: item.iconUrl, @@ -245,11 +261,13 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService } } + const manifestUrl = this.getManifestUrl(item, mcpGalleryManifest); + return { id: item.id ?? item.name, name: item.name, displayName: item.displayName ?? nameParts[nameParts.length - 1].split('-').map(s => uppercaseFirstLetter(s)).join(' '), - url: item.repository?.url, + url: manifestUrl, description: item.description, version: item.version_detail?.version, lastUpdated: item.version_detail ? Date.parse(item.version_detail.release_date) : undefined, @@ -257,7 +275,7 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService codicon: item.codicon, icon, readmeUrl: item.readmeUrl, - manifestUrl: this.getManifestUrl(item, mcpGalleryManifest), + manifestUrl, packageTypes: item.package_types ?? [], publisher, publisherDisplayName: item.publisher?.displayName, @@ -305,15 +323,53 @@ export class McpGalleryService extends Disposable implements IMcpGalleryService return result || { servers: [] }; } + private async getMcpServer(mcpServerUrl: string, mcpGalleryManifest: IMcpGalleryManifest): Promise { + const context = await this.requestService.request({ + type: 'GET', + url: mcpServerUrl, + }, CancellationToken.None); + + if (context.res.statusCode && context.res.statusCode >= 400 && context.res.statusCode < 500) { + return undefined; + } + + const item = await asJson(context); + if (!item) { + return undefined; + } + + return this.toGalleryMcpServer(item, mcpGalleryManifest); + } + + private async fetchMcpServersFromVSCodeGallery(): Promise { + const mcpGalleryUrl = this.productService.extensionsGallery?.mcpUrl; + if (!mcpGalleryUrl) { + return []; + } + + const context = await this.requestService.request({ + type: 'GET', + url: mcpGalleryUrl, + }, CancellationToken.None); + + const result = await asJson(context); + const mcpGalleryManifest: IMcpGalleryManifest = { url: mcpGalleryUrl, resources: [] }; + return result?.servers.map(item => this.toGalleryMcpServer(item, mcpGalleryManifest)) ?? []; + } + private getManifestUrl(item: IRawGalleryMcpServer, mcpGalleryManifest: IMcpGalleryManifest): string | undefined { if (!item.id) { return undefined; } + return this.getManifestUrlFromId(item.id, mcpGalleryManifest); + } + + private getManifestUrlFromId(id: string, mcpGalleryManifest: IMcpGalleryManifest): string | undefined { const resourceUriTemplate = getMcpGalleryManifestResourceUri(mcpGalleryManifest, McpGalleryResourceType.McpServerManifestUri); if (!resourceUriTemplate) { return undefined; } - return format2(resourceUriTemplate, { id: item.id }); + return format2(resourceUriTemplate, { id }); } private getMcpGalleryUrl(mcpGalleryManifest: IMcpGalleryManifest): string | undefined { diff --git a/src/vs/platform/mcp/common/mcpManagement.ts b/src/vs/platform/mcp/common/mcpManagement.ts index 76d8dbc3598a5..b69cae2a105b8 100644 --- a/src/vs/platform/mcp/common/mcpManagement.ts +++ b/src/vs/platform/mcp/common/mcpManagement.ts @@ -21,8 +21,8 @@ export interface ILocalMcpServer { readonly mcpResource: URI; readonly location?: URI; readonly displayName?: string; - readonly url?: string; readonly description?: string; + readonly galleryUrl?: string; readonly repositoryUrl?: string; readonly readmeUrl?: URI; readonly publisher?: string; @@ -138,7 +138,8 @@ export interface IMcpGalleryService { readonly _serviceBrand: undefined; isEnabled(): boolean; query(options?: IQueryOptions, token?: CancellationToken): Promise>; - getMcpServers(servers: string[]): Promise; + getMcpServersFromVSCodeGallery(servers: string[]): Promise; + getMcpServers(urls: string[]): Promise; getManifest(extension: IGalleryMcpServer, token: CancellationToken): Promise; getReadme(extension: IGalleryMcpServer, token: CancellationToken): Promise; } diff --git a/src/vs/platform/mcp/common/mcpManagementService.ts b/src/vs/platform/mcp/common/mcpManagementService.ts index 839f84318260a..3c2f765f47269 100644 --- a/src/vs/platform/mcp/common/mcpManagementService.ts +++ b/src/vs/platform/mcp/common/mcpManagementService.ts @@ -11,6 +11,7 @@ import { IMarkdownString, MarkdownString } from '../../../base/common/htmlConten import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../base/common/map.js'; import { equals } from '../../../base/common/objects.js'; +import { isString } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { ConfigurationTarget } from '../../configuration/common/configuration.js'; @@ -29,7 +30,7 @@ export interface ILocalMcpServerInfo { version?: string; id?: string; displayName?: string; - url?: string; + galleryUrl?: string; description?: string; repositoryUrl?: string; publisher?: string; @@ -335,7 +336,7 @@ export abstract class AbstractMcpResourceManagementService extends AbstractCommo protected async scanLocalServer(name: string, config: IMcpServerConfiguration): Promise { let mcpServerInfo = await this.getLocalServerInfo(name, config); if (!mcpServerInfo) { - mcpServerInfo = { name, version: config.version }; + mcpServerInfo = { name, version: config.version, galleryUrl: isString(config.gallery) ? config.gallery : undefined }; } return { @@ -348,6 +349,7 @@ export abstract class AbstractMcpResourceManagementService extends AbstractCommo description: mcpServerInfo.description, publisher: mcpServerInfo.publisher, publisherDisplayName: mcpServerInfo.publisherDisplayName, + galleryUrl: mcpServerInfo.galleryUrl, repositoryUrl: mcpServerInfo.repositoryUrl, readmeUrl: mcpServerInfo.readmeUrl, icon: mcpServerInfo.icon, @@ -438,6 +440,7 @@ export class McpUserResourceManagementService extends AbstractMcpResourceManagem const manifestPath = this.uriIdentityService.extUri.joinPath(location, 'manifest.json'); const local: ILocalMcpServerInfo = { id: gallery.id, + galleryUrl: gallery.url, name: gallery.name, displayName: gallery.displayName, description: gallery.description, diff --git a/src/vs/platform/mcp/common/mcpPlatformTypes.ts b/src/vs/platform/mcp/common/mcpPlatformTypes.ts index 5fb46fb72eead..b5ea52d7b88ca 100644 --- a/src/vs/platform/mcp/common/mcpPlatformTypes.ts +++ b/src/vs/platform/mcp/common/mcpPlatformTypes.ts @@ -35,7 +35,7 @@ export const enum McpServerType { export interface ICommonMcpServerConfiguration { readonly type: McpServerType; readonly version?: string; - readonly gallery?: boolean; + readonly gallery?: boolean | string; } export interface IMcpStdioServerConfiguration extends ICommonMcpServerConfiguration { diff --git a/src/vs/platform/mcp/node/mcpManagementService.ts b/src/vs/platform/mcp/node/mcpManagementService.ts index 705ad39bed9e7..60b2eebb0a069 100644 --- a/src/vs/platform/mcp/node/mcpManagementService.ts +++ b/src/vs/platform/mcp/node/mcpManagementService.ts @@ -49,7 +49,7 @@ export class McpUserResourceManagementService extends CommonMcpUserResourceManag name: server.name, config: { ...config, - gallery: true, + gallery: server.url ?? true, version: server.version }, inputs diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index efbb3782c3724..df7a26168b023 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -120,17 +120,27 @@ export class ObservableChatSession extends Disposable implements ChatSession { if (sessionContent.hasActiveResponseCallback && !this.interruptActiveResponseCallback) { this.interruptActiveResponseCallback = async () => { + const confirmInterrupt = () => { + if (this._disposalPending) { + this._proxy.$disposeChatSessionContent(this._providerHandle, this.sessionId); + this._disposalPending = false; + } + this._proxy.$interruptChatSessionActiveResponse(this._providerHandle, this.sessionId, 'ongoing'); + return true; + }; + + if (sessionContent.supportsInterruption) { + // If the session supports hot reload, interrupt without confirmation + return confirmInterrupt(); + } + + // Prompt the user to confirm interruption return this._dialogService.confirm({ message: localize('interruptActiveResponse', 'Are you sure you want to interrupt the active session?') }).then(confirmed => { if (confirmed.confirmed) { // User confirmed interruption - dispose the session content on extension host - if (this._disposalPending) { - this._proxy.$disposeChatSessionContent(this._providerHandle, this.sessionId); - this._disposalPending = false; - } - this._proxy.$interruptChatSessionActiveResponse(this._providerHandle, this.sessionId, 'ongoing'); - return true; + return confirmInterrupt(); } else { // When user cancels the interruption, fire an empty progress message to keep the session alive // This matches the behavior of the old implementation diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 1361b81143e6d..de74a2f28a3e4 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1518,9 +1518,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatSessionsProvider'); return extHostChatSessions.registerChatSessionItemProvider(extension, chatSessionType, provider); }, - registerChatSessionContentProvider(chatSessionType: string, provider: vscode.ChatSessionContentProvider) { + registerChatSessionContentProvider(chatSessionType: string, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities) { checkProposedApiEnabled(extension, 'chatSessionsProvider'); - return extHostChatSessions.registerChatSessionContentProvider(extension, chatSessionType, provider); + return extHostChatSessions.registerChatSessionContentProvider(extension, chatSessionType, provider, capabilities); }, registerChatOutputRenderer: (viewType: string, renderer: vscode.ChatOutputRenderer) => { checkProposedApiEnabled(extension, 'chatOutputRenderer'); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 090aaaa1c05e6..b5c295f625b8a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3145,6 +3145,7 @@ export interface ChatSessionDto { history: Array; hasActiveResponseCallback: boolean; hasRequestHandler: boolean; + supportsInterruption: boolean; } diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 765fef204604e..7779ae391f8c3 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -57,6 +57,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio private readonly _chatSessionContentProviders = new Map(); private _nextChatSessionItemProviderHandle = 0; @@ -110,11 +111,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }; } - registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionType: string, provider: vscode.ChatSessionContentProvider): vscode.Disposable { + registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionType: string, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable { const handle = this._nextChatSessionContentProviderHandle++; const disposables = new DisposableStore(); - this._chatSessionContentProviders.set(handle, { provider, extension, disposable: disposables }); + this._chatSessionContentProviders.set(handle, { provider, extension, capabilities, disposable: disposables }); this._proxy.$registerChatSessionContentProvider(handle, chatSessionType); return new extHostTypes.Disposable(() => { @@ -243,11 +244,12 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio this._proxy.$handleProgressComplete(handle, id, 'ongoing'); }); } - + const { capabilities } = provider; return { id: sessionId + '', hasActiveResponseCallback: !!session.activeResponseCallback, hasRequestHandler: !!session.requestHandler, + supportsInterruption: !!capabilities?.supportsInterruptions, history: session.history.map(turn => { if (turn instanceof extHostTypes.ChatRequestTurn) { return { type: 'request' as const, prompt: turn.prompt, participant: turn.participant }; diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions.ts b/src/vs/workbench/contrib/chat/browser/chatSessions.ts index edefb7187d130..845a8d6a9fe5d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions.ts @@ -406,36 +406,18 @@ class LocalChatSessionsProvider extends Disposable implements IChatSessionItemPr // Add chat view instance const chatWidget = this.chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Panel) .find(widget => typeof widget.viewContext === 'object' && 'viewId' in widget.viewContext && widget.viewContext.viewId === LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID); - let status: ChatSessionStatus | undefined; - let widgetTimestamp: number | undefined; - if (chatWidget?.viewModel?.model) { - status = this.modelToStatus(chatWidget.viewModel.model); - // Get the last interaction timestamp from the model - const requests = chatWidget.viewModel.model.getRequests(); - if (requests.length > 0) { - const lastRequest = requests[requests.length - 1]; - widgetTimestamp = lastRequest.timestamp; - } else { - // Fallback to current time if no requests yet - widgetTimestamp = Date.now(); - } - } - if (chatWidget) { - const widgetSession: ILocalChatSessionItem & ChatSessionItemWithProvider = { - id: LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID, - label: chatWidget.viewModel?.model.title || nls.localize2('chat.sessions.chatView', "Chat").value, - description: nls.localize('chat.sessions.chatView.description', "Chat View"), - iconPath: Codicon.chatSparkle, - widget: chatWidget, - sessionType: 'widget', - status, - provider: this, - timing: { - startTime: widgetTimestamp ?? 0 - } - }; - sessions.push(widgetSession); - } + const status = chatWidget?.viewModel?.model ? this.modelToStatus(chatWidget.viewModel.model) : undefined; + const widgetSession: ILocalChatSessionItem & ChatSessionItemWithProvider = { + id: LocalChatSessionsProvider.CHAT_WIDGET_VIEW_ID, + label: chatWidget?.viewModel?.model.title || nls.localize2('chat.sessions.chatView', "Chat").value, + description: nls.localize('chat.sessions.chatView.description', "Chat View"), + iconPath: Codicon.chatSparkle, + widget: chatWidget, + sessionType: 'widget', + status, + provider: this + }; + sessions.push(widgetSession); // Build editor-based sessions in the order specified by editorOrder this.editorOrder.forEach((editorKey, index) => { @@ -479,17 +461,13 @@ class LocalChatSessionsProvider extends Disposable implements IChatSessionItemPr } }); - // Sort sessions by timestamp (newest first), but keep "Show history..." at the end - const normalSessions = sessions.filter(s => s.id !== 'show-history'); - processSessionsWithTimeGrouping(normalSessions); - // Add "Show history..." node at the end const historyNode: IChatSessionItem = { id: 'show-history', label: nls.localize('chat.sessions.showHistory', "History"), }; - return [...normalSessions, historyNode]; + return [...sessions, historyNode]; } } @@ -606,22 +584,41 @@ class ChatSessionsViewPaneContainer extends ViewPaneContainer { if (container && providers.length > 0) { const viewDescriptorsToRegister: IViewDescriptor[] = []; - let index = 1; - providers.forEach(provider => { + // Separate providers by type and prepare display names + const localProvider = providers.find(p => p.chatSessionType === 'local'); + const historyProvider = providers.find(p => p.chatSessionType === 'history'); + const otherProviders = providers.filter(p => p.chatSessionType !== 'local' && p.chatSessionType !== 'history'); + + // Sort other providers alphabetically by display name + const providersWithDisplayNames = otherProviders.map(provider => { + const extContribution = extensionPointContributions.find(c => c.type === provider.chatSessionType); + if (!extContribution) { + this.logService.warn(`No extension contribution found for chat session type: ${provider.chatSessionType}`); + return null; + } + return { + provider, + displayName: extContribution.displayName + }; + }).filter(item => item !== null) as Array<{ provider: IChatSessionItemProvider; displayName: string }>; + + // Sort alphabetically by display name + providersWithDisplayNames.sort((a, b) => a.displayName.localeCompare(b.displayName)); + + // Register views in priority order: local, history, then alphabetically sorted others + const orderedProviders = [ + ...(localProvider ? [{ provider: localProvider, displayName: 'Local Chat Sessions', baseOrder: 0 }] : []), + ...(historyProvider ? [{ provider: historyProvider, displayName: 'History', baseOrder: 1 }] : []), + ...providersWithDisplayNames.map((item, index) => ({ + ...item, + baseOrder: 2 + index // Start from 2 for other providers + })) + ]; + + orderedProviders.forEach(({ provider, displayName, baseOrder }) => { // Only register if not already registered if (!this.registeredViewDescriptors.has(provider.chatSessionType)) { - let displayName = ''; - if (provider.chatSessionType === 'local') { - displayName = 'Local Chat Sessions'; - } else { - const extContribution = extensionPointContributions.find(c => c.type === provider.chatSessionType); - if (!extContribution) { - this.logService.warn(`No extension contribution found for chat session type: ${provider.chatSessionType}`); - return; // Skip if no contribution found - } - displayName = extContribution.displayName; - } const viewDescriptor: IViewDescriptor = { id: `${VIEWLET_ID}.${provider.chatSessionType}`, name: { @@ -631,7 +628,7 @@ class ChatSessionsViewPaneContainer extends ViewPaneContainer { ctorDescriptor: new SyncDescriptor(SessionsViewPane, [provider, this.sessionTracker]), canToggleVisibility: true, canMoveView: true, - order: provider.chatSessionType === 'local' ? 0 : provider.chatSessionType === 'history' ? 1 : index++, + order: baseOrder, // Use computed order based on priority and alphabetical sorting }; viewDescriptorsToRegister.push(viewDescriptor); @@ -762,11 +759,17 @@ class SessionsDataSource implements IAsyncDataSource { static readonly ITEM_HEIGHT = 22; - static readonly ITEM_HEIGHT_WITH_DESCRIPTION = 38; // Slightly smaller for cleaner look + static readonly ITEM_HEIGHT_WITH_DESCRIPTION = 40; // Slightly smaller for cleaner look + + constructor(private readonly configurationService: IConfigurationService) { } getHeight(element: ChatSessionItemWithProvider): number { // Return consistent height for all items (single-line layout) - return SessionsDelegate.ITEM_HEIGHT; + if (element.description && this.configurationService.getValue('chat.showAgentSessionsViewDescription')) { + return SessionsDelegate.ITEM_HEIGHT_WITH_DESCRIPTION; + } else { + return SessionsDelegate.ITEM_HEIGHT; + } } getTemplateId(element: ChatSessionItemWithProvider): string { @@ -781,6 +784,7 @@ interface ISessionTemplateData { actionBar: ActionBar; elementDisposable: DisposableStore; timestamp: HTMLElement; + descriptionRow: HTMLElement; } // Renderer for session items in the tree @@ -793,6 +797,7 @@ class SessionsRenderer extends Disposable implements ITreeRenderer; @@ -1450,10 +1465,10 @@ class SessionsViewPane extends ViewPane { // Focus the existing editor await element.group.openEditor(element.editor, { pinned: true }); return; - } else if (element.sessionType === 'widget' && element.widget) { + } else if (element.sessionType === 'widget') { // Focus the chat widget const chatViewPane = await this.viewsService.openView(ChatViewId) as ChatViewPane; - if (chatViewPane && element.widget.viewModel?.model) { + if (chatViewPane && element?.widget?.viewModel?.model) { await chatViewPane.loadSession(element.widget.viewModel.model.sessionId); } return; diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index 134d4084ed9ce..74ac92d0edf9d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -435,7 +435,12 @@ class ChatStatusDashboard extends Disposable { for (const { displayName, count } of inProgress) { if (count > 0) { - const text = localize('inProgressChatSession', "$(loading~spin) {0} {1} in progress", count, displayName); + let lowerCaseName = displayName.toLocaleLowerCase(); + // Very specific case for providers that end in session/sessions to ensure we pluralize correctly + if (lowerCaseName.endsWith('session') || lowerCaseName.endsWith('sessions')) { + lowerCaseName = lowerCaseName.replace(/session$|sessions$/g, count > 1 ? 'sessions' : 'session'); + } + const text = localize('inProgressChatSession', "$(loading~spin) {0} {1} in progress", count, lowerCaseName); chatSessionsElement = this.element.appendChild($('div.description')); const parts = renderLabelWithIcons(text); chatSessionsElement.append(...parts); diff --git a/src/vs/workbench/contrib/chat/browser/media/chatSessions.css b/src/vs/workbench/contrib/chat/browser/media/chatSessions.css index 6cf39a104746a..8e10e4d18c70b 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatSessions.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatSessions.css @@ -61,6 +61,18 @@ align-items: center; width: 100%; min-height: 22px; + line-height: 22px; +} + +.chat-sessions-tree-container .chat-session-item .description-row { + display: none; + opacity: 0.5; + font-size: 0.9em; + line-height: 1em; + margin: 2px 22px 0 22px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .chat-sessions-tree-container .chat-session-item .actions { diff --git a/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts index 6e58763bb9710..5f682ca56a1e7 100644 --- a/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts +++ b/src/vs/workbench/contrib/issue/browser/baseIssueReporterService.ts @@ -66,12 +66,14 @@ export class BaseIssueReporterService extends Disposable { public loadingExtensionData = false; public selectedExtension = ''; public delayedSubmit = new Delayer(300); - public onGithubButton!: Button | ButtonWithDropdown; + public publicGithubButton!: Button | ButtonWithDropdown; + public internalGithubButton!: Button | ButtonWithDropdown; public nonGitHubIssueUrl = false; public needsUpdate = false; public acknowledged = false; private createAction: Action; private previewAction: Action; + private privateAction: Action; constructor( public disableExtensions: boolean, @@ -121,7 +123,7 @@ export class BaseIssueReporterService extends Disposable { const currentAuthState = !!githubAccessToken; if (previousAuthState !== currentAuthState) { - this.recreateGithubButton(); + this.updateButtonStates(); } })); @@ -131,25 +133,19 @@ export class BaseIssueReporterService extends Disposable { this.createAction = this._register(new Action('issueReporter.create', localize('create', "Create on GitHub"), undefined, true, async () => { this.delayedSubmit.trigger(async () => { - this.createIssue(); + this.createIssue(true); // create issue }); })); this.previewAction = this._register(new Action('issueReporter.preview', localize('preview', "Preview on GitHub"), undefined, true, async () => { this.delayedSubmit.trigger(async () => { - this.createIssue(true); + this.createIssue(false); // preview issue + }); + })); + this.privateAction = this._register(new Action('issueReporter.privateCreate', localize('privateCreate', "Create Internally"), undefined, true, async () => { + this.delayedSubmit.trigger(async () => { + this.createIssue(true, true); // create private issue }); })); - - const issueReporterElement = this.getElementById('issue-reporter'); - if (issueReporterElement) { - // Create button based on GitHub access token availability - this.recreateGithubButton(); - - const issueRepoName = document.createElement('a'); - issueReporterElement.appendChild(issueRepoName); - issueRepoName.id = 'show-repo-name'; - issueRepoName.classList.add('hidden'); - } const issueTitle = data.issueTitle; if (issueTitle) { @@ -191,6 +187,12 @@ export class BaseIssueReporterService extends Disposable { if ((data.data || data.uri) && targetExtension) { this.updateExtensionStatus(targetExtension); } + + // initialize the reporting button(s) + const issueReporterElement = this.getElementById('issue-reporter'); + if (issueReporterElement) { + this.updateButtonStates(); + } } render(): void { @@ -208,6 +210,188 @@ export class BaseIssueReporterService extends Disposable { } } + public updateButtonStates() { + const issueReporterElement = this.getElementById('issue-reporter'); + if (!issueReporterElement) { + // shouldn't occur -- throw? + return; + } + + + // public elements section + let publicElements = this.getElementById('public-elements'); + if (!publicElements) { + publicElements = document.createElement('div'); + publicElements.id = 'public-elements'; + publicElements.classList.add('public-elements'); + issueReporterElement.appendChild(publicElements); + } + this.updatePublicGithubButton(publicElements); + this.updatePublicRepoLink(publicElements); + + + // private filing section + let internalElements = this.getElementById('internal-elements'); + if (!internalElements) { + internalElements = document.createElement('div'); + internalElements.id = 'internal-elements'; + internalElements.classList.add('internal-elements'); + internalElements.classList.add('hidden'); + issueReporterElement.appendChild(internalElements); + } + let filingRow = this.getElementById('internal-top-row'); + if (!filingRow) { + filingRow = document.createElement('div'); + filingRow.id = 'internal-top-row'; + filingRow.classList.add('internal-top-row'); + internalElements.appendChild(filingRow); + } + this.updateInternalFilingNote(filingRow); + this.updateInternalGithubButton(filingRow); + this.updateInternalElementsVisibility(); + } + + private updateInternalFilingNote(container: HTMLElement) { + let filingNote = this.getElementById('internal-preview-message'); + if (!filingNote) { + filingNote = document.createElement('span'); + filingNote.id = 'internal-preview-message'; + filingNote.classList.add('internal-preview-message'); + container.appendChild(filingNote); + } + + filingNote.textContent = escape(localize('internalPreviewMessage', 'If your copilot debug logs contain private information:')); + } + + private updatePublicGithubButton(container: HTMLElement): void { + const issueReporterElement = this.getElementById('issue-reporter'); + if (!issueReporterElement) { + return; + } + + // Dispose of the existing button + if (this.publicGithubButton) { + this.publicGithubButton.dispose(); + } + + // setup button + dropdown if applicable + if (!this.acknowledged && this.needsUpdate) { // * old version and hasn't ack'd + this.publicGithubButton = this._register(new Button(container, unthemedButtonStyles)); + this.publicGithubButton.label = localize('acknowledge', "Confirm Version Acknowledgement"); + this.publicGithubButton.enabled = false; + } else if (this.data.githubAccessToken && this.isPreviewEnabled()) { // * has access token, create by default, preview dropdown + this.publicGithubButton = this._register(new ButtonWithDropdown(container, { + contextMenuProvider: this.contextMenuService, + actions: [this.previewAction], + addPrimaryActionToDropdown: false, + ...unthemedButtonStyles + })); + this._register(this.publicGithubButton.onDidClick(() => { + this.createAction.run(); + })); + this.publicGithubButton.label = localize('createOnGitHub', "Create on GitHub"); + this.publicGithubButton.enabled = true; + } else if (this.data.githubAccessToken && !this.isPreviewEnabled()) { // * Access token but invalid preview state: simple Button (create only) + this.publicGithubButton = this._register(new Button(container, unthemedButtonStyles)); + this._register(this.publicGithubButton.onDidClick(() => { + this.createAction.run(); + })); + this.publicGithubButton.label = localize('createOnGitHub', "Create on GitHub"); + this.publicGithubButton.enabled = true; + } else { // * No access token: simple Button (preview only) + this.publicGithubButton = this._register(new Button(container, unthemedButtonStyles)); + this._register(this.publicGithubButton.onDidClick(() => { + this.previewAction.run(); + })); + this.publicGithubButton.label = localize('previewOnGitHub', "Preview on GitHub"); + this.publicGithubButton.enabled = true; + } + + // make sure that the repo link is after the button + const repoLink = this.getElementById('show-repo-name'); + if (repoLink) { + container.insertBefore(this.publicGithubButton.element, repoLink); + } + } + + private updatePublicRepoLink(container: HTMLElement): void { + let issueRepoName = this.getElementById('show-repo-name') as HTMLAnchorElement; + if (!issueRepoName) { + issueRepoName = document.createElement('a'); + issueRepoName.id = 'show-repo-name'; + issueRepoName.classList.add('hidden'); + container.appendChild(issueRepoName); + } + + + const selectedExtension = this.issueReporterModel.getData().selectedExtension; + if (selectedExtension && selectedExtension.uri) { + const urlString = URI.revive(selectedExtension.uri).toString(); + issueRepoName.href = urlString; + issueRepoName.addEventListener('click', (e) => this.openLink(e)); + issueRepoName.addEventListener('auxclick', (e) => this.openLink(e)); + const gitHubInfo = this.parseGitHubUrl(urlString); + issueRepoName.textContent = gitHubInfo ? gitHubInfo.owner + '/' + gitHubInfo.repositoryName : urlString; + Object.assign(issueRepoName.style, { + alignSelf: 'flex-end', + display: 'block', + fontSize: '13px', + padding: '4px 0px', + textDecoration: 'none', + width: 'auto' + }); + show(issueRepoName); + } else if (issueRepoName) { + // clear styles + issueRepoName.removeAttribute('style'); + hide(issueRepoName); + } + } + + private updateInternalGithubButton(container: HTMLElement): void { + const issueReporterElement = this.getElementById('issue-reporter'); + if (!issueReporterElement) { + return; + } + + // Dispose of the existing button + if (this.internalGithubButton) { + this.internalGithubButton.dispose(); + } + + if (this.data.githubAccessToken && this.data.privateUri) { + this.internalGithubButton = this._register(new Button(container, unthemedButtonStyles)); + this._register(this.internalGithubButton.onDidClick(() => { + this.privateAction.run(); + })); + + this.internalGithubButton.element.id = 'internal-create-btn'; + this.internalGithubButton.element.classList.add('internal-create-subtle'); + this.internalGithubButton.label = localize('createInternally', "Create Internally"); + this.internalGithubButton.enabled = true; + this.internalGithubButton.setTitle(this.data.privateUri.path!.slice(1)); + } + } + + private updateInternalElementsVisibility(): void { + const container = this.getElementById('internal-elements'); + if (!container) { + // shouldn't happen + return; + } + + if (this.data.githubAccessToken && this.data.privateUri) { + show(container); + container.style.display = ''; //todo: necessary even with show? + if (this.internalGithubButton) { + this.internalGithubButton.enabled = this.publicGithubButton?.enabled ?? false; + } + } else { + hide(container); + container.style.display = 'none'; //todo: necessary even with hide? + } + } + private async updateIssueReporterUri(extension: IssueReporterExtensionData): Promise { try { if (extension.uri) { @@ -297,11 +481,8 @@ export class BaseIssueReporterService extends Disposable { if (openReporterData) { if (this.selectedExtension === selectedExtensionId) { this.removeLoading(iconElement, true); - // this.configuration.data = openReporterData; this.data = openReporterData; } - // else if (this.selectedExtension !== selectedExtensionId) { - // } } else { if (!this.loadingExtensionData) { @@ -328,6 +509,9 @@ export class BaseIssueReporterService extends Disposable { this.updateExtensionStatus(matches[0]); } } + + // Update internal action visibility after explicit selection + this.updateInternalElementsVisibility(); }); } @@ -357,7 +541,7 @@ export class BaseIssueReporterService extends Disposable { const acknowledgementCheckbox = this.getElementById('includeAcknowledgement'); if (acknowledgementCheckbox) { this.acknowledged = acknowledgementCheckbox.checked; - this.updatePreviewButtonState(); + this.updateButtonStates(); } } @@ -534,99 +718,7 @@ export class BaseIssueReporterService extends Disposable { const state = this.issueReporterModel.getData(); this.updateProcessInfo(state); this.updateWorkspaceInfo(state); - this.updatePreviewButtonState(); - } - - private recreateGithubButton(): void { - const issueReporterElement = this.getElementById('issue-reporter'); - if (!issueReporterElement) { - return; - } - - // Dispose of the existing button - if (this.onGithubButton) { - this.onGithubButton.dispose(); - } - - // Find the repo name element to insert the button before it - const issueRepoName = this.getElementById('show-repo-name'); - - // Create button based on GitHub access token availability - if (this.data.githubAccessToken) { - this.onGithubButton = this._register(new ButtonWithDropdown(issueReporterElement, { - contextMenuProvider: this.contextMenuService, - actions: [this.previewAction], - addPrimaryActionToDropdown: false, - ...unthemedButtonStyles - })); - - // Set up click handler for primary button (create) - this._register(this.onGithubButton.onDidClick(() => { - this.createAction.run(); - })); - } else { - // No access token: create simple Button (preview only) - this.onGithubButton = this._register(new Button(issueReporterElement, unthemedButtonStyles)); - - // Set up click handler for preview - this._register(this.onGithubButton.onDidClick(() => { - this.previewAction.run(); - })); - } - - // Ensure button appears before repo name by moving it if necessary - if (issueRepoName && this.onGithubButton.element.nextSibling !== issueRepoName) { - issueReporterElement.insertBefore(this.onGithubButton.element, issueRepoName); - } - - // Update the button state after recreation - this.updatePreviewButtonState(); - } - - public updatePreviewButtonState() { - if (!this.acknowledged && this.needsUpdate) { - this.onGithubButton.label = localize('acknowledge', "Confirm Version Acknowledgement"); - this.onGithubButton.enabled = false; - } else if (this.isPreviewEnabled()) { - // Set button label to match the primary action - if (this.data.githubAccessToken) { - this.onGithubButton.label = localize('createOnGitHub', "Create on GitHub"); - } else { - this.onGithubButton.label = localize('previewOnGitHub', "Preview on GitHub"); - } - this.onGithubButton.enabled = true; - } else { - this.onGithubButton.enabled = false; - this.onGithubButton.label = localize('loadingData', "Loading data..."); - } - - const issueRepoName = this.getElementById('show-repo-name')! as HTMLAnchorElement; - const selectedExtension = this.issueReporterModel.getData().selectedExtension; - if (selectedExtension && selectedExtension.uri) { - const urlString = URI.revive(selectedExtension.uri).toString(); - issueRepoName.href = urlString; - issueRepoName.addEventListener('click', (e) => this.openLink(e)); - issueRepoName.addEventListener('auxclick', (e) => this.openLink(e)); - const gitHubInfo = this.parseGitHubUrl(urlString); - issueRepoName.textContent = gitHubInfo ? gitHubInfo.owner + '/' + gitHubInfo.repositoryName : urlString; - Object.assign(issueRepoName.style, { - alignSelf: 'flex-end', - display: 'block', - fontSize: '13px', - marginBottom: '10px', - padding: '4px 0px', - textDecoration: 'none', - width: 'auto' - }); - show(issueRepoName); - } else if (issueRepoName) { - // clear styles - issueRepoName.removeAttribute('style'); - hide(issueRepoName); - } - - // Initial check when first opened. - this.getExtensionGitHubUrl(); + this.updateButtonStates(); } private isPreviewEnabled() { @@ -954,7 +1046,7 @@ export class BaseIssueReporterService extends Disposable { hide(descriptionTextArea); reset(descriptionTitle, localize('handlesIssuesElsewhere', "This extension handles issues outside of VS Code")); reset(descriptionSubtitle, localize('elsewhereDescription', "The '{0}' extension prefers to use an external issue reporter. To be taken to that issue reporting experience, click the button below.", selectedExtension.displayName)); - this.onGithubButton.label = localize('openIssueReporter', "Open External Issue Reporter"); + this.publicGithubButton.label = localize('openIssueReporter', "Open External Issue Reporter"); return; } @@ -1077,11 +1169,10 @@ export class BaseIssueReporterService extends Disposable { return true; } - public async createIssue(preview?: boolean): Promise { + public async createIssue(shouldCreate?: boolean, privateUri?: boolean): Promise { const selectedExtension = this.issueReporterModel.getData().selectedExtension; - const hasUri = this.nonGitHubIssueUrl; // Short circuit if the extension provides a custom issue handler - if (hasUri) { + if (this.nonGitHubIssueUrl) { const url = this.getExtensionBugsUrl(); if (url) { this.hasBeenSubmitted = true; @@ -1123,19 +1214,18 @@ export class BaseIssueReporterService extends Disposable { const issueTitle = (this.getElementById('issue-title')).value; const issueBody = this.issueReporterModel.serialize(); - let issueUrl = this.getIssueUrl(); + let issueUrl = privateUri ? this.getPrivateIssueUrl() : this.getIssueUrl(); if (!issueUrl) { - console.error('No issue url found'); + console.error(`No ${privateUri ? 'private ' : ''}issue url found`); return false; } - if (selectedExtension?.uri) { const uri = URI.revive(selectedExtension.uri); issueUrl = uri.toString(); } const gitHubDetails = this.parseGitHubUrl(issueUrl); - if (this.data.githubAccessToken && gitHubDetails && !preview) { + if (this.data.githubAccessToken && gitHubDetails && shouldCreate) { return this.submitToGitHub(issueTitle, issueBody, gitHubDetails); } @@ -1194,6 +1284,12 @@ export class BaseIssueReporterService extends Disposable { : this.product.reportIssueUrl!; } + // for when command 'workbench.action.openIssueReporter' passes along a + // `privateUri` UriComponents value + public getPrivateIssueUrl(): string | undefined { + return URI.revive(this.data.privateUri)?.toString(); + } + public parseGitHubUrl(url: string): undefined | { repositoryName: string; owner: string } { // Assumes a GitHub url to a particular repo, https://github.com/repositoryName/owner. // Repository name and owner cannot contain '/' @@ -1244,6 +1340,7 @@ export class BaseIssueReporterService extends Disposable { this.data.issueBody = this.data.issueBody || ''; this.data.data = undefined; this.data.uri = undefined; + this.data.privateUri = undefined; } public async updateExtensionStatus(extension: IssueReporterExtensionData) { @@ -1280,7 +1377,7 @@ export class BaseIssueReporterService extends Disposable { const title = (this.getElementById('issue-title')).value; this.searchExtensionIssues(title); - this.updatePreviewButtonState(); + this.updateButtonStates(); this.renderBlocks(); } @@ -1292,7 +1389,7 @@ export class BaseIssueReporterService extends Disposable { const extension = this.issueReporterModel.getData().selectedExtension; if (!extension) { - this.onGithubButton.enabled = true; + this.publicGithubButton.enabled = true; return; } @@ -1302,10 +1399,10 @@ export class BaseIssueReporterService extends Disposable { const hasValidGitHubUrl = this.getExtensionGitHubUrl(); if (hasValidGitHubUrl) { - this.onGithubButton.enabled = true; + this.publicGithubButton.enabled = true; } else { this.setExtensionValidationMessage(); - this.onGithubButton.enabled = false; + this.publicGithubButton.enabled = false; } } @@ -1313,7 +1410,7 @@ export class BaseIssueReporterService extends Disposable { // Show loading this.openReporter = true; this.loadingExtensionData = true; - this.updatePreviewButtonState(); + this.updateButtonStates(); const extensionDataCaption = this.getElementById('extension-id')!; hide(extensionDataCaption); @@ -1334,7 +1431,7 @@ export class BaseIssueReporterService extends Disposable { public removeLoading(element: HTMLElement, fromReporter: boolean = false) { this.openReporter = fromReporter; this.loadingExtensionData = false; - this.updatePreviewButtonState(); + this.updateButtonStates(); const extensionDataCaption = this.getElementById('extension-id')!; show(extensionDataCaption); diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts b/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts index e739f44817f78..10fde69b316d0 100644 --- a/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterPage.ts @@ -167,4 +167,5 @@ export default (): string => ` + `; diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterService.ts b/src/vs/workbench/contrib/issue/browser/issueReporterService.ts index 600ca24943556..f95ac61a686fd 100644 --- a/src/vs/workbench/contrib/issue/browser/issueReporterService.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterService.ts @@ -60,7 +60,7 @@ export class IssueWebReporter extends BaseIssueReporterService { descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); } - this.updatePreviewButtonState(); + this.updateButtonStates(); this.setSourceOptions(); this.render(); }); diff --git a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css index 0e567d945092f..bc997b189ebf3 100644 --- a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css +++ b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css @@ -247,9 +247,7 @@ body.issue-reporter-body { .issue-reporter-body #github-submit-btn { flex-shrink: 0; - margin-left: auto; - margin-top: 10px; - margin-bottom: 10px; + margin: 0; /* Reset margin since parent container handles spacing */ } .issue-reporter-body .two-col { @@ -559,3 +557,58 @@ body.issue-reporter-body { top: 0; z-index: 1000; } + +/* Public elements section styling */ +.issue-reporter-body .public-elements { + display: flex; + flex-direction: column; + align-items: flex-end; + margin-top: 10px; + margin-bottom: 10px; +} + +.issue-reporter-body .public-elements .monaco-text-button, +.issue-reporter-body .public-elements .monaco-button-dropdown { + align-self: flex-end; +} + +.issue-reporter-body .public-elements #show-repo-name { + align-self: flex-end; + font-size: 12px; +} + +/* Internal elements section styling */ +.issue-reporter-body .internal-elements { + display: flex; + flex-direction: column; + align-items: flex-end; + margin-top: 8px; +} + +.issue-reporter-body .internal-elements .internal-top-row { + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; + justify-content: flex-end; +} + +.issue-reporter-body .internal-elements .internal-preview-message { + font-size: 10px; + opacity: 0.8; + text-align: right; + white-space: nowrap; + display: inline-flex; + align-items: center; + line-height: 15px; /* approximate button height for vertical centering */ +} + +.issue-reporter-body .internal-elements .monaco-text-button { + font-size: 10px; + padding: 2px 8px; +} + +.issue-reporter-body .internal-elements #show-private-repo-name { + align-self: flex-end; + font-size: 12px; +} diff --git a/src/vs/workbench/contrib/issue/common/issue.ts b/src/vs/workbench/contrib/issue/common/issue.ts index 5e12f63c9714e..bf9c4d0e51e21 100644 --- a/src/vs/workbench/contrib/issue/common/issue.ts +++ b/src/vs/workbench/contrib/issue/common/issue.ts @@ -61,6 +61,7 @@ export interface IssueReporterExtensionData { extensionTemplate?: string; data?: string; uri?: UriComponents; + privateUri?: UriComponents; } export interface IssueReporterData extends WindowData { @@ -77,6 +78,7 @@ export interface IssueReporterData extends WindowData { issueBody?: string; data?: string; uri?: UriComponents; + privateUri?: UriComponents; } export interface ISettingSearchResult { diff --git a/src/vs/workbench/contrib/issue/electron-browser/issueReporterService.ts b/src/vs/workbench/contrib/issue/electron-browser/issueReporterService.ts index f7eabf2207ed4..4a46d4de5ef48 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issueReporterService.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issueReporterService.ts @@ -18,6 +18,7 @@ import { INativeHostService } from '../../../../platform/native/common/native.js import { IProcessService } from '../../../../platform/process/common/process.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { applyZoom } from '../../../../platform/window/electron-browser/window.js'; import { IAuthenticationService } from '../../../services/authentication/common/authentication.js'; import { BaseIssueReporterService } from '../browser/baseIssueReporterService.js'; @@ -52,6 +53,7 @@ export class IssueReporter extends BaseIssueReporterService { @IFileService fileService: IFileService, @IFileDialogService fileDialogService: IFileDialogService, @IUpdateService private readonly updateService: IUpdateService, + @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, @IAuthenticationService authenticationService: IAuthenticationService ) { @@ -62,7 +64,7 @@ export class IssueReporter extends BaseIssueReporterService { this.receivedSystemInfo = true; this.updateSystemInfo(this.issueReporterModel.getData()); - this.updatePreviewButtonState(); + this.updateButtonStates(); }); if (this.data.issueType === IssueType.PerformanceIssue) { this.processService.getPerformanceInfo().then(info => { @@ -110,7 +112,7 @@ export class IssueReporter extends BaseIssueReporterService { descriptionTextArea.placeholder = localize('undefinedPlaceholder', "Please enter a title"); } - this.updatePreviewButtonState(); + this.updateButtonStates(); this.setSourceOptions(); this.render(); }); @@ -168,11 +170,10 @@ export class IssueReporter extends BaseIssueReporterService { return true; } - public override async createIssue(preview?: boolean): Promise { + public override async createIssue(shouldCreate?: boolean, privateUri?: boolean): Promise { const selectedExtension = this.issueReporterModel.getData().selectedExtension; - const hasUri = this.nonGitHubIssueUrl; // Short circuit if the extension provides a custom issue handler - if (hasUri) { + if (this.nonGitHubIssueUrl) { const url = this.getExtensionBugsUrl(); if (url) { this.hasBeenSubmitted = true; @@ -216,15 +217,13 @@ export class IssueReporter extends BaseIssueReporterService { const issueTitle = (this.getElementById('issue-title')).value; const issueBody = this.issueReporterModel.serialize(); - let issueUrl = this.getIssueUrl(); - if (!issueUrl) { - console.error('No issue url found'); - return false; - } - - if (selectedExtension?.uri) { + let issueUrl = privateUri ? this.getPrivateIssueUrl() : this.getIssueUrl(); + if (!issueUrl && selectedExtension?.uri) { const uri = URI.revive(selectedExtension.uri); issueUrl = uri.toString(); + } else if (!issueUrl) { + console.error(`No ${privateUri ? 'private ' : ''}issue url found`); + return false; } const gitHubDetails = this.parseGitHubUrl(issueUrl); @@ -234,7 +233,7 @@ export class IssueReporter extends BaseIssueReporterService { url = this.addTemplateToUrl(url, gitHubDetails?.owner, gitHubDetails?.repositoryName); - if (this.data.githubAccessToken && gitHubDetails && !preview) { + if (this.data.githubAccessToken && gitHubDetails && shouldCreate) { if (await this.submitToGitHub(issueTitle, issueBody, gitHubDetails)) { return true; } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts index 90f8f119e384e..b60af3b229b9b 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpWorkbenchService.ts @@ -19,7 +19,7 @@ import { IFileService } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { IGalleryMcpServer, IMcpGalleryService, IQueryOptions, IInstallableMcpServer, IMcpServerManifest, ILocalMcpServer, mcpAccessConfig, McpAccessValue } from '../../../../platform/mcp/common/mcpManagement.js'; +import { IGalleryMcpServer, IMcpGalleryService, IQueryOptions, IInstallableMcpServer, IMcpServerManifest, mcpAccessConfig, McpAccessValue } from '../../../../platform/mcp/common/mcpManagement.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; @@ -173,7 +173,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ readonly onReset = this._onReset.event; constructor( - @IMcpGalleryManifestService mcpGalleryManifestService: IMcpGalleryManifestService, + @IMcpGalleryManifestService private readonly mcpGalleryManifestService: IMcpGalleryManifestService, @IMcpGalleryService private readonly mcpGalleryService: IMcpGalleryService, @IWorkbenchMcpManagementService private readonly mcpManagementService: IWorkbenchMcpManagementService, @IEditorService private readonly editorService: IEditorService, @@ -240,7 +240,7 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ } servers.push(this.onDidInstallMcpServer(result.local, result.source)); } - if (servers.some(server => server.local?.source === 'gallery' && !server.gallery)) { + if (servers.some(server => server.local?.galleryUrl && !server.gallery)) { this.syncInstalledMcpServers(); } } @@ -253,6 +253,9 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ } else { server = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), local, gallery, undefined); } + if (!local.galleryUrl) { + server.gallery = undefined; + } this._local = this._local.filter(server => !this.areSameMcpServers(server.local, local)); this._local.push(server); this._onChange.fire(server); @@ -288,41 +291,56 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ } private async syncInstalledMcpServers(): Promise { - const installedGalleryServers: ILocalMcpServer[] = []; + const galleryMcpServerUrls: string[] = []; + const vscodeGalleryMcpServerNames: string[] = []; + for (const installed of this.local) { if (installed.local?.source !== 'gallery') { continue; } - installedGalleryServers.push(installed.local); + if (installed.local.galleryUrl) { + galleryMcpServerUrls.push(installed.local.galleryUrl); + } else if (!installed.local.manifest) { + vscodeGalleryMcpServerNames.push(installed.local.name); + } + } + + if (galleryMcpServerUrls.length) { + const galleryServers = await this.mcpGalleryService.getMcpServers(galleryMcpServerUrls); + if (galleryServers.length) { + await this.syncInstalledMcpServersWithGallery(galleryServers, false); + } } - if (installedGalleryServers.length) { - const galleryServers = await this.mcpGalleryService.getMcpServers(installedGalleryServers.map(server => server.name)); + + if (vscodeGalleryMcpServerNames.length) { + const galleryServers = await this.mcpGalleryService.getMcpServersFromVSCodeGallery(vscodeGalleryMcpServerNames); if (galleryServers.length) { - this.syncInstalledMcpServersWithGallery(galleryServers); + await this.syncInstalledMcpServersWithGallery(galleryServers, true); } } } - private async syncInstalledMcpServersWithGallery(gallery: IGalleryMcpServer[]): Promise { - const galleryMap = new Map(gallery.map(server => [server.name, server])); + private async syncInstalledMcpServersWithGallery(gallery: IGalleryMcpServer[], vscodeGallery: boolean): Promise { + const galleryMap = new Map(gallery.map(server => [vscodeGallery ? server.name : (server.url ?? server.name), server])); for (const mcpServer of this.local) { - if (!mcpServer.gallery) { - if (!mcpServer.local) { - continue; - } - if (mcpServer.gallery) { - continue; - } - const galleryServer = galleryMap.get(mcpServer.name); - if (!galleryServer) { - continue; - } + if (!mcpServer.local) { + continue; + } + const key = vscodeGallery ? mcpServer.local.name : mcpServer.local.galleryUrl; + if (!key) { + continue; + } + const galleryServer = galleryMap.get(key); + if (!galleryServer) { + continue; + } + if (!vscodeGallery) { mcpServer.gallery = galleryServer; - if (!mcpServer.id) { - mcpServer.local = await this.mcpManagementService.updateMetadata(mcpServer.local, galleryServer); - } - this._onChange.fire(mcpServer); } + if (!mcpServer.local.manifest) { + mcpServer.local = await this.mcpManagementService.updateMetadata(mcpServer.local, galleryServer); + } + this._onChange.fire(mcpServer); } } @@ -345,7 +363,13 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ async queryLocal(): Promise { const installed = await this.mcpManagementService.getInstalled(); this._local = installed.map(i => { - const local = this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, undefined); + const existing = this._local.find(local => { + if (i.galleryUrl) { + return local.local?.galleryUrl === i.galleryUrl; + } + return local.id === i.id; + }); + const local = existing ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, undefined); local.local = i; return local; }); @@ -593,10 +617,24 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ } async handleURL(uri: URI): Promise { - if (uri.path !== 'mcp/install') { - return false; + if (uri.path === 'mcp/install') { + return this.handleMcpInstallUri(uri); } + if (uri.path.startsWith('mcp/')) { + const manifest = await this.mcpGalleryManifestService.getMcpGalleryManifest(); + if (!manifest) { + this.logService.info('No MCP gallery manifest available'); + return true; + } + const mcpServerUrl = uri.path.substring(4); + if (mcpServerUrl) { + return this.handleMcpServerUrl(`${URI.parse(manifest.url).scheme}://${mcpServerUrl}`); + } + } + return false; + } + private async handleMcpInstallUri(uri: URI): Promise { let parsed: IMcpServerConfiguration & { name: string; inputs?: IMcpServerVariable[]; gallery?: boolean }; try { parsed = JSON.parse(decodeURIComponent(uri.query)); @@ -608,13 +646,12 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ const { name, inputs, gallery, ...config } = parsed; if (gallery || !config || Object.keys(config).length === 0) { - const [galleryServer] = await this.mcpGalleryService.getMcpServers([name]); + const [galleryServer] = await this.mcpGalleryService.getMcpServersFromVSCodeGallery([name]); if (!galleryServer) { throw new Error(`MCP server '${name}' not found in gallery`); } const local = this.local.find(e => e.name === name && e.local?.scope !== LocalMcpServerScope.Workspace) - ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, undefined, { name, config, inputs }); - local.gallery = galleryServer; + ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, galleryServer, undefined); this.open(local); } else { if (config.type === undefined) { @@ -628,6 +665,22 @@ export class McpWorkbenchService extends Disposable implements IMcpWorkbenchServ return true; } + private async handleMcpServerUrl(url: string): Promise { + try { + const [gallery] = await this.mcpGalleryService.getMcpServers([url]); + if (!gallery) { + this.logService.info(`MCP server '${url}' not found`); + return true; + } + const local = this.local.find(e => e.url === url) ?? this.instantiationService.createInstance(McpWorkbenchServer, e => this.getInstallState(e), undefined, gallery, undefined); + this.open(local); + } catch (e) { + // ignore + this.logService.error(e); + } + return true; + } + async open(extension: IWorkbenchMcpServer, options?: IEditorOptions): Promise { await this.editorService.openEditor(this.instantiationService.createInstance(McpServerEditorInput, extension), options, ACTIVE_GROUP); } diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 92ba38d36e421..acebca716e662 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -111,7 +111,6 @@ declare module 'vscode' { } export interface ChatSession { - /** * The full history of the session * @@ -171,7 +170,14 @@ declare module 'vscode' { * * @returns A disposable that unregisters the provider when disposed. */ - export function registerChatSessionContentProvider(chatSessionType: string, provider: ChatSessionContentProvider): Disposable; + export function registerChatSessionContentProvider(chatSessionType: string, provider: ChatSessionContentProvider, capabilities?: ChatSessionCapabilities): Disposable; + } + + export interface ChatSessionCapabilities { + /** + * Whether sessions can be interrupted and resumed without side-effects. + */ + supportsInterruptions?: boolean; } export interface ChatSessionShowOptions {