From b63bcf97278d91513fdbcb7c88142c0832e46636 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:40:48 -0700 Subject: [PATCH 01/16] Marking a few more events in vscode.d.ts readonly These should only be used for registration, never overwritten --- src/vscode-dts/vscode.d.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 0f4fa464aaf..0cbebceee1d 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -1666,7 +1666,7 @@ declare module 'vscode' { /** * An {@link Event} which fires upon cancellation. */ - onCancellationRequested: Event; + readonly onCancellationRequested: Event; } /** @@ -8603,7 +8603,7 @@ declare module 'vscode' { /** * Fires when a secret is stored or deleted. */ - onDidChange: Event; + readonly onDidChange: Event; } /** @@ -13069,7 +13069,7 @@ declare module 'vscode' { * (Examples include: an explicit call to {@link QuickInput.hide}, * the user pressing Esc, some other input UI opening, etc.) */ - onDidHide: Event; + readonly onDidHide: Event; /** * Dispose of this input UI and any associated resources. If it is still @@ -18184,7 +18184,7 @@ declare module 'vscode' { * Fired when a user has changed whether this is a default profile. The * event contains the new value of {@link isDefault} */ - onDidChangeDefault: Event; + readonly onDidChangeDefault: Event; /** * Whether this profile supports continuous running of requests. If so, @@ -18563,7 +18563,7 @@ declare module 'vscode' { * An event fired when the editor is no longer interested in data * associated with the test run. */ - onDidDispose: Event; + readonly onDidDispose: Event; } /** @@ -19639,7 +19639,7 @@ declare module 'vscode' { * The passed {@link ChatResultFeedback.result result} is guaranteed to have the same properties as the result that was * previously returned from this chat participant's handler. */ - onDidReceiveFeedback: Event; + readonly onDidReceiveFeedback: Event; /** * Dispose this participant and free resources. @@ -20678,7 +20678,7 @@ declare module 'vscode' { /** * An event that fires when access information changes. */ - onDidChange: Event; + readonly onDidChange: Event; /** * Checks if a request can be made to a language model. @@ -20867,6 +20867,7 @@ declare module 'vscode' { * Options to hint at how many tokens the tool should return in its response, and enable the tool to count tokens * accurately. */ + // TODO api: how does this work if the model also has a countTokens? tokenizationOptions?: LanguageModelToolTokenizationOptions; } @@ -20933,6 +20934,7 @@ declare module 'vscode' { * * The provided {@link LanguageModelToolInvocationOptions.input} has been validated against the declared schema. */ + // TODO:API Should model always be required here? invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken): ProviderResult; /** From 44954a3fd636fe79730a6a9fe5823a47094f1869 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:12:34 -0700 Subject: [PATCH 02/16] Remove todo comments --- src/vscode-dts/vscode.d.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 0cbebceee1d..bf5a6ae00c4 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -20867,7 +20867,6 @@ declare module 'vscode' { * Options to hint at how many tokens the tool should return in its response, and enable the tool to count tokens * accurately. */ - // TODO api: how does this work if the model also has a countTokens? tokenizationOptions?: LanguageModelToolTokenizationOptions; } @@ -20934,7 +20933,6 @@ declare module 'vscode' { * * The provided {@link LanguageModelToolInvocationOptions.input} has been validated against the declared schema. */ - // TODO:API Should model always be required here? invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken): ProviderResult; /** From 50e37a458d877100ed90fe48f0319b88a8effd5a Mon Sep 17 00:00:00 2001 From: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com> Date: Mon, 29 Sep 2025 22:31:39 -0700 Subject: [PATCH 03/16] Fallback to .well-known/oauth-protected-resource on 401s without WWW-Authenticate (#268977) * refactor resource metadata reading into oauth base file the logic is going to get more complicated, so I want to encapsulate it where it should go. * fix test * Fallback to .well-known/oauth-protected-resource on 401s without WWW-Authenticate Fixes https://github.com/microsoft/vscode/issues/268210 --- src/vs/base/common/oauth.ts | 117 ++++++ src/vs/base/test/common/oauth.test.ts | 475 ++++++++++++++++++++++ src/vs/workbench/api/common/extHostMcp.ts | 51 +-- 3 files changed, 604 insertions(+), 39 deletions(-) diff --git a/src/vs/base/common/oauth.ts b/src/vs/base/common/oauth.ts index ce4889265b2..4b332685a8c 100644 --- a/src/vs/base/common/oauth.ts +++ b/src/vs/base/common/oauth.ts @@ -957,3 +957,120 @@ export function scopesMatch(scopes1: readonly string[], scopes2: readonly string return sortedScopes1.every((scope, index) => scope === sortedScopes2[index]); } + +interface CommonResponse { + status: number; + statusText: string; + json(): Promise; + text(): Promise; +} + +interface IFetcher { + (input: string, init: { method: string; headers: Record }): Promise; +} + +export interface IFetchResourceMetadataOptions { + /** + * Headers to include only when the resource metadata URL has the same origin as the target resource + */ + sameOriginHeaders?: Record; + /** + * Optional custom fetch implementation (defaults to global fetch) + */ + fetch?: IFetcher; +} + +/** + * Fetches and validates OAuth 2.0 protected resource metadata from the given URL. + * + * @param targetResource The target resource URL to compare origins with (e.g., the MCP server URL) + * @param resourceMetadataUrl Optional URL to fetch the resource metadata from. If not provided, will try well-known URIs. + * @param options Configuration options for the fetch operation + * @returns Promise that resolves to the validated resource metadata + * @throws Error if the fetch fails, returns non-200 status, or the response is invalid + */ +export async function fetchResourceMetadata( + targetResource: string, + resourceMetadataUrl: string | undefined, + options: IFetchResourceMetadataOptions = {} +): Promise { + const { + sameOriginHeaders = {}, + fetch: fetchImpl = fetch + } = options; + + const targetResourceUrlObj = new URL(targetResource); + + // If no resourceMetadataUrl is provided, try well-known URIs as per RFC 9728 + let urlsToTry: string[]; + if (!resourceMetadataUrl) { + // Try in order: 1) with path appended, 2) at root + const pathComponent = targetResourceUrlObj.pathname === '/' ? undefined : targetResourceUrlObj.pathname; + const rootUrl = `${targetResourceUrlObj.origin}${AUTH_PROTECTED_RESOURCE_METADATA_DISCOVERY_PATH}`; + if (pathComponent) { + // Only try both URLs if we have a path component + urlsToTry = [ + `${rootUrl}${pathComponent}`, + rootUrl + ]; + } else { + // If target is already at root, only try the root URL once + urlsToTry = [rootUrl]; + } + } else { + urlsToTry = [resourceMetadataUrl]; + } + + const errors: Error[] = []; + for (const urlToTry of urlsToTry) { + try { + // Determine if we should include same-origin headers + let headers: Record = { + 'Accept': 'application/json' + }; + + const resourceMetadataUrlObj = new URL(urlToTry); + if (resourceMetadataUrlObj.origin === targetResourceUrlObj.origin) { + headers = { + ...headers, + ...sameOriginHeaders + }; + } + + const response = await fetchImpl(urlToTry, { method: 'GET', headers }); + if (response.status !== 200) { + let errorText: string; + try { + errorText = await response.text(); + } catch { + errorText = response.statusText; + } + errors.push(new Error(`Failed to fetch resource metadata from ${urlToTry}: ${response.status} ${errorText}`)); + continue; + } + + const body = await response.json(); + if (isAuthorizationProtectedResourceMetadata(body)) { + // Use URL constructor for normalization - it handles hostname case and trailing slashes + const prmValue = new URL(body.resource).toString(); + const targetValue = targetResourceUrlObj.toString(); + if (prmValue !== targetValue) { + throw new Error(`Protected Resource Metadata resource property value "${prmValue}" (length: ${prmValue.length}) does not match target server url "${targetValue}" (length: ${targetValue.length}). These MUST match to follow OAuth spec https://datatracker.ietf.org/doc/html/rfc9728#PRConfigurationValidation`); + } + return body; + } else { + errors.push(new Error(`Invalid resource metadata from ${urlToTry}. Expected to follow shape of https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata (Hints: is scopes_supported an array? Is resource a string?). Current payload: ${JSON.stringify(body)}`)); + continue; + } + } catch (e) { + errors.push(e instanceof Error ? e : new Error(String(e))); + continue; + } + } + // If we've tried all URLs and none worked, throw the error(s) + if (errors.length === 1) { + throw errors[0]; + } else { + throw new AggregateError(errors, 'Failed to fetch resource metadata from all attempted URLs'); + } +} diff --git a/src/vs/base/test/common/oauth.test.ts b/src/vs/base/test/common/oauth.test.ts index f0cf9cd766e..09819b8d870 100644 --- a/src/vs/base/test/common/oauth.test.ts +++ b/src/vs/base/test/common/oauth.test.ts @@ -17,6 +17,7 @@ import { isAuthorizationTokenResponse, parseWWWAuthenticateHeader, fetchDynamicRegistration, + fetchResourceMetadata, scopesMatch, IAuthorizationJWTClaims, IAuthorizationServerMetadata, @@ -831,4 +832,478 @@ suite('OAuth', () => { ); }); }); + + suite('fetchResourceMetadata', () => { + let sandbox: sinon.SinonSandbox; + let fetchStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + fetchStub = sandbox.stub(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should successfully fetch and validate resource metadata', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const expectedMetadata = { + resource: 'https://example.com/api', + scopes_supported: ['read', 'write'] + }; + + fetchStub.resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata) + }); + + const result = await fetchResourceMetadata( + targetResource, + resourceMetadataUrl, + { fetch: fetchStub } + ); + + assert.deepStrictEqual(result, expectedMetadata); + assert.strictEqual(fetchStub.callCount, 1); + assert.strictEqual(fetchStub.firstCall.args[0], resourceMetadataUrl); + assert.strictEqual(fetchStub.firstCall.args[1].method, 'GET'); + assert.strictEqual(fetchStub.firstCall.args[1].headers['Accept'], 'application/json'); + }); + + test('should include same-origin headers when origins match', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const sameOriginHeaders = { + 'X-Test-Header': 'test-value', + 'X-Custom-Header': 'value' + }; + const expectedMetadata = { + resource: 'https://example.com/api' + }; + + fetchStub.resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata) + }); + + await fetchResourceMetadata( + targetResource, + resourceMetadataUrl, + { fetch: fetchStub, sameOriginHeaders } + ); + + const headers = fetchStub.firstCall.args[1].headers; + assert.strictEqual(headers['Accept'], 'application/json'); + assert.strictEqual(headers['X-Test-Header'], 'test-value'); + assert.strictEqual(headers['X-Custom-Header'], 'value'); + }); + + test('should not include same-origin headers when origins differ', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://other-domain.com/.well-known/oauth-protected-resource'; + const sameOriginHeaders = { + 'X-Test-Header': 'test-value' + }; + const expectedMetadata = { + resource: 'https://example.com/api' + }; + + fetchStub.resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata) + }); + + await fetchResourceMetadata( + targetResource, + resourceMetadataUrl, + { fetch: fetchStub, sameOriginHeaders } + ); + + const headers = fetchStub.firstCall.args[1].headers; + assert.strictEqual(headers['Accept'], 'application/json'); + assert.strictEqual(headers['X-Test-Header'], undefined); + }); + + test('should throw error when fetch returns non-200 status', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + + fetchStub.resolves({ + status: 404, + text: async () => 'Not Found' + }); + + await assert.rejects( + async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), + /Failed to fetch resource metadata from.*404 Not Found/ + ); + }); + + test('should handle error when response.text() throws', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + + fetchStub.resolves({ + status: 500, + statusText: 'Internal Server Error', + text: async () => { throw new Error('Cannot read response'); } + }); + + await assert.rejects( + async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), + /Failed to fetch resource metadata from.*500 Internal Server Error/ + ); + }); + + test('should throw error when resource property does not match target resource', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const metadata = { + resource: 'https://different.com/api' + }; + + fetchStub.resolves({ + status: 200, + json: async () => metadata, + text: async () => JSON.stringify(metadata) + }); + + await assert.rejects( + async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), + /Protected Resource Metadata resource property value.*does not match target server url.*These MUST match to follow OAuth spec/ + ); + }); + + test('should normalize URLs when comparing resource values', async () => { + const targetResource = 'https://EXAMPLE.COM/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const metadata = { + resource: 'https://example.com/api' + }; + + fetchStub.resolves({ + status: 200, + json: async () => metadata, + text: async () => JSON.stringify(metadata) + }); + + // URL normalization should handle hostname case differences + const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); + assert.deepStrictEqual(result, metadata); + }); + + test('should normalize hostnames when comparing resource values', async () => { + const targetResource = 'https://EXAMPLE.COM/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const metadata = { + resource: 'https://example.com/api' + }; + + fetchStub.resolves({ + status: 200, + json: async () => metadata, + text: async () => JSON.stringify(metadata) + }); + + const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); + assert.deepStrictEqual(result, metadata); + }); + + test('should throw error when response is not valid resource metadata', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const invalidMetadata = { + // Missing required 'resource' property + scopes_supported: ['read', 'write'] + }; + + fetchStub.resolves({ + status: 200, + json: async () => invalidMetadata, + text: async () => JSON.stringify(invalidMetadata) + }); + + await assert.rejects( + async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), + /Invalid resource metadata.*Expected to follow shape of.*is scopes_supported an array\? Is resource a string\?/ + ); + }); + + test('should throw error when scopes_supported is not an array', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const invalidMetadata = { + resource: 'https://example.com/api', + scopes_supported: 'not an array' + }; + + fetchStub.resolves({ + status: 200, + json: async () => invalidMetadata, + text: async () => JSON.stringify(invalidMetadata) + }); + + await assert.rejects( + async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), + /Invalid resource metadata/ + ); + }); + + test('should handle metadata with optional fields', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const metadata = { + resource: 'https://example.com/api', + resource_name: 'Example API', + authorization_servers: ['https://auth.example.com'], + jwks_uri: 'https://example.com/jwks', + scopes_supported: ['read', 'write', 'admin'], + bearer_methods_supported: ['header', 'body'], + resource_documentation: 'https://example.com/docs' + }; + + fetchStub.resolves({ + status: 200, + json: async () => metadata, + text: async () => JSON.stringify(metadata) + }); + + const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); + assert.deepStrictEqual(result, metadata); + }); + + test('should use global fetch when custom fetch is not provided', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const metadata = { + resource: 'https://example.com/api' + }; + + const globalFetchStub = sandbox.stub(globalThis, 'fetch').resolves({ + status: 200, + json: async () => metadata, + text: async () => JSON.stringify(metadata) + } as any); + + const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl); + + assert.deepStrictEqual(result, metadata); + assert.strictEqual(globalFetchStub.callCount, 1); + }); + + test('should handle same origin with different ports', async () => { + const targetResource = 'https://example.com:8080/api'; + const resourceMetadataUrl = 'https://example.com:9090/.well-known/oauth-protected-resource'; + const sameOriginHeaders = { + 'X-Test-Header': 'test-value' + }; + const metadata = { + resource: 'https://example.com:8080/api' + }; + + fetchStub.resolves({ + status: 200, + json: async () => metadata, + text: async () => JSON.stringify(metadata) + }); + + await fetchResourceMetadata( + targetResource, + resourceMetadataUrl, + { fetch: fetchStub, sameOriginHeaders } + ); + + // Different ports mean different origins + const headers = fetchStub.firstCall.args[1].headers; + assert.strictEqual(headers['X-Test-Header'], undefined); + }); + + test('should handle same origin with different protocols', async () => { + const targetResource = 'http://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const sameOriginHeaders = { + 'X-Test-Header': 'test-value' + }; + const metadata = { + resource: 'http://example.com/api' + }; + + fetchStub.resolves({ + status: 200, + json: async () => metadata, + text: async () => JSON.stringify(metadata) + }); + + await fetchResourceMetadata( + targetResource, + resourceMetadataUrl, + { fetch: fetchStub, sameOriginHeaders } + ); + + // Different protocols mean different origins + const headers = fetchStub.firstCall.args[1].headers; + assert.strictEqual(headers['X-Test-Header'], undefined); + }); + + test('should include error details in message with length information', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const metadata = { + resource: 'https://different.com/other' + }; + + fetchStub.resolves({ + status: 200, + json: async () => metadata, + text: async () => JSON.stringify(metadata) + }); + + try { + await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); + assert.fail('Should have thrown an error'); + } catch (error: any) { + assert.ok(/length:/.test(error.message), 'Error message should include length information'); + assert.ok(/https:\/\/different\.com\/other/.test(error.message), 'Error message should include actual resource value'); + assert.ok(/https:\/\/example\.com\/api/.test(error.message), 'Error message should include expected resource value'); + } + }); + + test('should fallback to well-known URI with path when no resourceMetadataUrl provided', async () => { + const targetResource = 'https://example.com/api/v1'; + const expectedMetadata = { + resource: 'https://example.com/api/v1', + scopes_supported: ['read', 'write'] + }; + + fetchStub.resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata) + }); + + const result = await fetchResourceMetadata( + targetResource, + undefined, + { fetch: fetchStub } + ); + + assert.deepStrictEqual(result, expectedMetadata); + assert.strictEqual(fetchStub.callCount, 1); + // Should try path-appended version first + assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1'); + }); + + test('should fallback to well-known URI at root when path version fails', async () => { + const targetResource = 'https://example.com/api/v1'; + const expectedMetadata = { + resource: 'https://example.com/api/v1', + scopes_supported: ['read', 'write'] + }; + + // First call fails, second succeeds + fetchStub.onFirstCall().resolves({ + status: 404, + text: async () => 'Not Found', + statusText: 'Not Found' + }); + + fetchStub.onSecondCall().resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata) + }); + + const result = await fetchResourceMetadata( + targetResource, + undefined, + { fetch: fetchStub } + ); + + assert.deepStrictEqual(result, expectedMetadata); + assert.strictEqual(fetchStub.callCount, 2); + // First attempt with path + assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1'); + // Second attempt at root + assert.strictEqual(fetchStub.secondCall.args[0], 'https://example.com/.well-known/oauth-protected-resource'); + }); + + test('should throw error when all well-known URIs fail', async () => { + const targetResource = 'https://example.com/api/v1'; + + fetchStub.resolves({ + status: 404, + text: async () => 'Not Found', + statusText: 'Not Found' + }); + + await assert.rejects( + async () => fetchResourceMetadata(targetResource, undefined, { fetch: fetchStub }), + (error: any) => { + assert.ok(error instanceof AggregateError, 'Should be an AggregateError'); + assert.strictEqual(error.errors.length, 2, 'Should contain 2 errors'); + assert.ok(/Failed to fetch resource metadata from.*\/api\/v1.*404/.test(error.errors[0].message), 'First error should mention /api/v1 and 404'); + assert.ok(/Failed to fetch resource metadata from.*\.well-known.*404/.test(error.errors[1].message), 'Second error should mention .well-known and 404'); + return true; + } + ); assert.strictEqual(fetchStub.callCount, 2); + }); + + test('should not append path when target resource is root', async () => { + const targetResource = 'https://example.com/'; + const expectedMetadata = { + resource: 'https://example.com/', + scopes_supported: ['read'] + }; + + fetchStub.resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata) + }); + + const result = await fetchResourceMetadata( + targetResource, + undefined, + { fetch: fetchStub } + ); + + assert.deepStrictEqual(result, expectedMetadata); + assert.strictEqual(fetchStub.callCount, 1); + // Both URLs should be the same when path is / + assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource'); + }); + + test('should include same-origin headers when using well-known fallback', async () => { + const targetResource = 'https://example.com/api'; + const sameOriginHeaders = { + 'X-Test-Header': 'test-value', + 'X-Custom-Header': 'value' + }; + const expectedMetadata = { + resource: 'https://example.com/api' + }; + + fetchStub.resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata) + }); + + await fetchResourceMetadata( + targetResource, + undefined, + { fetch: fetchStub, sameOriginHeaders } + ); + + const headers = fetchStub.firstCall.args[1].headers; + assert.strictEqual(headers['Accept'], 'application/json'); + assert.strictEqual(headers['X-Test-Header'], 'test-value'); + assert.strictEqual(headers['X-Custom-Header'], 'value'); + }); + }); }); diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index f4232eec7bd..0b6232a4342 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -8,7 +8,7 @@ import { DeferredPromise, raceCancellationError, Sequencer, timeout } from '../. import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { CancellationError } from '../../../base/common/errors.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; -import { AUTH_SERVER_METADATA_DISCOVERY_PATH, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, isAuthorizationProtectedResourceMetadata, isAuthorizationServerMetadata, OPENID_CONNECT_DISCOVERY_PATH, parseWWWAuthenticateHeader } from '../../../base/common/oauth.js'; +import { AUTH_SERVER_METADATA_DISCOVERY_PATH, fetchResourceMetadata, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, isAuthorizationServerMetadata, OPENID_CONNECT_DISCOVERY_PATH, parseWWWAuthenticateHeader } from '../../../base/common/oauth.js'; import { SSEParser } from '../../../base/common/sseParser.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { ConfigurationTarget } from '../../../platform/configuration/common/configuration.js'; @@ -345,24 +345,26 @@ export class McpHTTPHandle extends Disposable { } } } - // Second, fetch that url's well-known server metadata + // Second, fetch the resource metadata either from the challenge URL or from well-known URIs let serverMetadataUrl: string | undefined; let scopesSupported: string[] | undefined; let resource: IAuthorizationProtectedResourceMetadata | undefined; - if (resourceMetadataChallenge) { - const resourceMetadata = await this._getResourceMetadata(resourceMetadataChallenge); - // Use URL constructor for normalization - it handles hostname case and trailing slashes - const prmValue = new URL(resourceMetadata.resource).toString(); - const mcpValue = new URL(mcpUrl).toString(); - if (prmValue !== mcpValue) { - throw new Error(`Protected Resource Metadata resource value "${prmValue}" (length: ${prmValue.length}) does not match MCP server url "${mcpValue}" (length: ${mcpValue.length}). The MCP server must follow OAuth spec https://datatracker.ietf.org/doc/html/rfc9728#PRConfigurationValidation`); - } + try { + const resourceMetadata = await fetchResourceMetadata(mcpUrl, resourceMetadataChallenge, { + sameOriginHeaders: { + ...Object.fromEntries(this._launch.headers), + 'MCP-Protocol-Version': MCP.LATEST_PROTOCOL_VERSION + }, + fetch: (url, init) => this._fetch(url, init) + }); // TODO:@TylerLeonhardt support multiple authorization servers // Consider using one that has an auth provider first, over the dynamic flow serverMetadataUrl = resourceMetadata.authorization_servers?.[0]; this._log(LogLevel.Debug, `Using auth server metadata url: ${serverMetadataUrl}`); scopesSupported = resourceMetadata.scopes_supported; resource = resourceMetadata; + } catch (e) { + this._log(LogLevel.Debug, `Could not fetch resource metadata: ${String(e)}`); } const baseUrl = new URL(originalResponse.url).origin; @@ -401,35 +403,6 @@ export class McpHTTPHandle extends Disposable { this._log(LogLevel.Info, 'Using default auth metadata'); } - private async _getResourceMetadata(resourceMetadata: string): Promise { - // detect if the resourceMetadata, which is a URL, is in the same origin as the MCP server - const resourceMetadataUrl = new URL(resourceMetadata); - const mcpServerUrl = new URL(this._launch.uri.toString(true)); - let additionalHeaders: Record = {}; - if (resourceMetadataUrl.origin === mcpServerUrl.origin) { - additionalHeaders = { - ...Object.fromEntries(this._launch.headers) - }; - } - const resourceMetadataResponse = await this._fetch(resourceMetadata, { - method: 'GET', - headers: { - ...additionalHeaders, - 'Accept': 'application/json', - 'MCP-Protocol-Version': MCP.LATEST_PROTOCOL_VERSION - } - }); - if (resourceMetadataResponse.status !== 200) { - throw new Error(`Failed to fetch resource metadata: ${resourceMetadataResponse.status} ${await this._getErrText(resourceMetadataResponse)}`); - } - const body = await resourceMetadataResponse.json(); - if (isAuthorizationProtectedResourceMetadata(body)) { - return body; - } else { - throw new Error(`Invalid resource metadata. Expected to follow shape of https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata (Hints: is scopes_supported an array? Is resource a string?). Current payload: ${JSON.stringify(body)}`); - } - } - private async _getAuthorizationServerMetadata(authorizationServer: string, addtionalHeaders: Record): Promise { // For the oauth server metadata discovery path, we _INSERT_ // the well known path after the origin and before the path. From abb3be88db7b02383d25c980007e1ec7548a4a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Tue, 30 Sep 2025 10:20:08 +0200 Subject: [PATCH 04/16] isolate smoke test runs between type (#269021) also, move some fs calls to sync also, increase retryDelay for rmSync fixes #268437 --- test/smoke/src/main.ts | 65 +++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 4b30dd20739..15fb7848087 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -12,7 +12,7 @@ import * as minimist from 'minimist'; import * as vscodetest from '@vscode/test-electron'; import fetch from 'node-fetch'; import { Quality, MultiLogger, Logger, ConsoleLogger, FileLogger, measureAndLog, getDevElectronPath, getBuildElectronPath, getBuildVersion, ApplicationOptions } from '../../automation'; -import { retry, timeout } from './utils'; +import { retry } from './utils'; import { setup as setupDataLossTests } from './areas/workbench/data-loss.test'; import { setup as setupPreferencesTests } from './areas/preferences/preferences.test'; @@ -102,7 +102,7 @@ function createLogger(): Logger { } // Prepare logs rot path - fs.rmSync(logsRootPath, { recursive: true, force: true, maxRetries: 3 }); + fs.rmSync(logsRootPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 }); fs.mkdirSync(logsRootPath, { recursive: true }); // Always log to log file @@ -117,19 +117,6 @@ try { logger.log(`Error enabling graceful-fs: ${error}`); } -const testDataPath = path.join(os.tmpdir(), 'vscsmoke'); -if (fs.existsSync(testDataPath)) { - fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10 }); -} -fs.mkdirSync(testDataPath, { recursive: true }); -process.once('exit', () => { - try { - fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10 }); - } catch { - // noop - } -}); - function getTestTypeSuffix(): string { if (opts.web) { return 'browser'; @@ -140,8 +127,21 @@ function getTestTypeSuffix(): string { } } +const testDataPath = path.join(os.tmpdir(), `vscsmoke-${getTestTypeSuffix()}`); +if (fs.existsSync(testDataPath)) { + fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 }); +} +fs.mkdirSync(testDataPath, { recursive: true }); +process.once('exit', () => { + try { + fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 }); + } catch { + // noop + } +}); + const testRepoUrl = 'https://github.com/microsoft/vscode-smoketest-express'; -const workspacePath = path.join(testDataPath, `vscode-smoketest-express-${getTestTypeSuffix()}`); +const workspacePath = path.join(testDataPath, `vscode-smoketest-express`); const extensionsPath = path.join(testDataPath, 'extensions-dir'); fs.mkdirSync(extensionsPath, { recursive: true }); @@ -245,7 +245,7 @@ const userDataDir = path.join(testDataPath, 'd'); async function setupRepository(): Promise { if (opts['test-repo']) { logger.log('Copying test project repository:', opts['test-repo']); - fs.rmSync(workspacePath, { recursive: true, force: true, maxRetries: 10 }); + fs.rmSync(workspacePath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 }); // not platform friendly if (process.platform === 'win32') { cp.execSync(`xcopy /E "${opts['test-repo']}" "${workspacePath}"\\*`); @@ -314,15 +314,9 @@ async function ensureStableCode(): Promise { }, error: error => logger.log(`download stable code error: ${error}`) } - }), 'download stable code', logger), 1000, 3, () => new Promise((resolve, reject) => { - fs.rm(stableCodeDestination, { recursive: true, force: true, maxRetries: 10 }, error => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - })); + }), 'download stable code', logger), 1000, 3, async () => { + fs.rmSync(stableCodeDestination, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 }); + }); if (process.platform === 'darwin') { // Visual Studio Code.app/Contents/MacOS/Electron @@ -388,22 +382,9 @@ before(async function () { // After main suite (after all tests) after(async function () { try { - let deleted = false; - await measureAndLog(() => Promise.race([ - new Promise((resolve, reject) => fs.rm(testDataPath, { recursive: true, force: true, maxRetries: 10 }, error => { - if (error) { - reject(error); - } else { - deleted = true; - resolve(); - } - })), - timeout(30000).then(() => { - if (!deleted) { - throw new Error('giving up after 30s'); - } - }) - ]), 'rimraf(testDataPath)', logger); + await measureAndLog(async () => { + fs.rmSync(testDataPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 1000 }); + }, 'rimraf(testDataPath)', logger); } catch (error) { logger.log(`Unable to delete smoke test workspace: ${error}. This indicates some process is locking the workspace folder.`); } From 0de9bec93f74f8b926a4cd48f303612f4d1c3da8 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:35:56 +0200 Subject: [PATCH 05/16] Settings links in Release Notes do nothing (#268455) * Settings links in Release Notes do nothing Part of #268443 * Update src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix test --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../update/browser/releaseNotesEditor.ts | 6 ++- ...se_notes_renderer_Should_render_TOC.0.snap | 4 +- ...enderer_Should_render_code_settings.0.snap | 7 +++ .../test/browser/releaseNotesRenderer.test.ts | 47 +++++++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/contrib/update/test/browser/__snapshots__/Release_notes_renderer_Should_render_code_settings.0.snap diff --git a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts index 2eeb8c84353..4fc42c37af4 100644 --- a/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts +++ b/src/vs/workbench/contrib/update/browser/releaseNotesEditor.ts @@ -626,8 +626,10 @@ export async function renderReleaseNotesMarkdown( sanitizerConfig: { allowRelativeMediaPaths: true, allowedLinkProtocols: { - override: [Schemas.http, Schemas.https, Schemas.command] - } + override: [Schemas.http, Schemas.https, Schemas.command, Schemas.codeSetting] + }, + allowedTags: { augment: ['nav', 'svg', 'path'] }, + allowedAttributes: { augment: ['aria-role', 'viewBox', 'fill', 'xmlns', 'd'] } }, markedExtensions: [{ renderer: { diff --git a/src/vs/workbench/contrib/update/test/browser/__snapshots__/Release_notes_renderer_Should_render_TOC.0.snap b/src/vs/workbench/contrib/update/test/browser/__snapshots__/Release_notes_renderer_Should_render_TOC.0.snap index f39cc5b43c8..8670cb0c523 100644 --- a/src/vs/workbench/contrib/update/test/browser/__snapshots__/Release_notes_renderer_Should_render_TOC.0.snap +++ b/src/vs/workbench/contrib/update/test/browser/__snapshots__/Release_notes_renderer_Should_render_TOC.0.snap @@ -10,12 +10,12 @@

text

- +

Test

diff --git a/src/vs/workbench/contrib/update/test/browser/__snapshots__/Release_notes_renderer_Should_render_code_settings.0.snap b/src/vs/workbench/contrib/update/test/browser/__snapshots__/Release_notes_renderer_Should_render_code_settings.0.snap new file mode 100644 index 00000000000..c7a7c8a34f7 --- /dev/null +++ b/src/vs/workbench/contrib/update/test/browser/__snapshots__/Release_notes_renderer_Should_render_code_settings.0.snap @@ -0,0 +1,7 @@ +

Here is a setting: + + editor.wordWrap + and another + + editor.wordWrap +

diff --git a/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts b/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts index a3cf541c9be..5db88c9eeb7 100644 --- a/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts +++ b/src/vs/workbench/contrib/update/test/browser/releaseNotesRenderer.test.ts @@ -10,7 +10,10 @@ import { IContextMenuService } from '../../../../../platform/contextview/browser import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { SimpleSettingRenderer } from '../../../markdown/browser/markdownSettingRenderer.js'; +import { IPreferencesService } from '../../../../services/preferences/common/preferences.js'; import { renderReleaseNotesMarkdown } from '../../browser/releaseNotesEditor.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { Emitter } from '../../../../../base/common/event.js'; suite('Release notes renderer', () => { @@ -55,4 +58,48 @@ Navigation End --> const result = await renderReleaseNotesMarkdown(content, extensionService, languageService, instantiationService.createInstance(SimpleSettingRenderer)); await assertSnapshot(result.toString()); }); + + test('Should render code settings', async () => { + // Stub preferences service with a known setting so the SimpleSettingRenderer treats it as valid + const testSettingId = 'editor.wordWrap'; + instantiationService.stub(IPreferencesService, >{ + _serviceBrand: undefined, + onDidDefaultSettingsContentChanged: new Emitter().event, + userSettingsResource: undefined as any, + workspaceSettingsResource: null, + getFolderSettingsResource: () => null, + createPreferencesEditorModel: async () => null, + getDefaultSettingsContent: () => undefined, + hasDefaultSettingsContent: () => false, + createSettings2EditorModel: () => { throw new Error('not needed'); }, + openPreferences: async () => undefined, + openRawDefaultSettings: async () => undefined, + openSettings: async () => undefined, + openApplicationSettings: async () => undefined, + openUserSettings: async () => undefined, + openRemoteSettings: async () => undefined, + openWorkspaceSettings: async () => undefined, + openFolderSettings: async () => undefined, + openGlobalKeybindingSettings: async () => undefined, + openDefaultKeybindingsFile: async () => undefined, + openLanguageSpecificSettings: async () => undefined, + getEditableSettingsURI: async () => null, + getSetting: (id: string) => { + if (id === testSettingId) { + // Provide the minimal fields accessed by SimpleSettingRenderer + return { + key: testSettingId, + value: 'off', + type: 'string' + }; + } + return undefined; + }, + createSplitJsonEditorInput: () => { throw new Error('not needed'); } + }); + + const content = `Here is a setting: \`setting(${testSettingId}:on)\` and another \`setting(${testSettingId}:off)\``; + const result = await renderReleaseNotesMarkdown(content, extensionService, languageService, instantiationService.createInstance(SimpleSettingRenderer)); + await assertSnapshot(result.toString()); + }); }); From 5349099a9175716c04176c157d77ec9ce2beab55 Mon Sep 17 00:00:00 2001 From: Simon Siefke Date: Tue, 30 Sep 2025 14:52:32 +0200 Subject: [PATCH 06/16] fix: memory leak in folding --- src/vs/editor/contrib/folding/browser/folding.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index e51fc289e8d..49b243f0733 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -242,6 +242,7 @@ export class FoldingController extends Disposable implements IEditorContribution this.localToDispose.add(this.hiddenRangeModel.onDidChange(hr => this.onHiddenRangesChanges(hr))); this.updateScheduler = new Delayer(this.updateDebounceInfo.get(model)); + this.localToDispose.add(this.updateScheduler); this.cursorChangedScheduler = new RunOnceScheduler(() => this.revealCursor(), 200); this.localToDispose.add(this.cursorChangedScheduler); From d0891116dbd71400abc894545fc5d788005e25cc Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 30 Sep 2025 15:40:53 +0200 Subject: [PATCH 07/16] update distro (#269074) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 228fd513a9f..3f79dc1be00 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.105.0", - "distro": "9e823daeded19d4a7c8f7e3ba36ca5d3c68d8350", + "distro": "8b551ae26e4dadc3f6ece34bbbc3feea81ce4a97", "author": { "name": "Microsoft Corporation" }, @@ -239,4 +239,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file From 1445f020061a5d8475240fbb437435ff389ebfbb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 30 Sep 2025 16:04:42 +0200 Subject: [PATCH 08/16] enable mcp gallery in stable (#269080) --- src/vs/platform/mcp/common/mcpGalleryManifestService.ts | 2 +- .../mcp/electron-browser/mcpGalleryManifestService.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/vs/platform/mcp/common/mcpGalleryManifestService.ts b/src/vs/platform/mcp/common/mcpGalleryManifestService.ts index cdd12bfc1fa..7557e32fea0 100644 --- a/src/vs/platform/mcp/common/mcpGalleryManifestService.ts +++ b/src/vs/platform/mcp/common/mcpGalleryManifestService.ts @@ -19,7 +19,7 @@ export class McpGalleryManifestService extends Disposable implements IMcpGallery } constructor( - @IProductService protected readonly productService: IProductService, + @IProductService private readonly productService: IProductService, ) { super(); } diff --git a/src/vs/workbench/services/mcp/electron-browser/mcpGalleryManifestService.ts b/src/vs/workbench/services/mcp/electron-browser/mcpGalleryManifestService.ts index 5bc775d4d86..59024b55ed3 100644 --- a/src/vs/workbench/services/mcp/electron-browser/mcpGalleryManifestService.ts +++ b/src/vs/workbench/services/mcp/electron-browser/mcpGalleryManifestService.ts @@ -55,10 +55,6 @@ export class WorkbenchMcpGalleryManifestService extends McpGalleryManifestServic } private async doGetMcpGalleryManifest(): Promise { - if (this.productService.quality === 'stable') { - return; - } - await this.getAndUpdateMcpGalleryManifest(); this._register(this.configurationService.onDidChangeConfiguration(e => { From 3f527562bb6fc163c2c2a51f4c0790836daf676b Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Tue, 30 Sep 2025 17:09:13 +0200 Subject: [PATCH 09/16] Fix cyclic dependency reporting in chat module (#269095) * fix cyclic dependency in chat land * make sure cyclic dependencies are correctly reported when running complie-client task --- build/lib/tsb/builder.js | 2 ++ build/lib/tsb/builder.ts | 2 ++ src/vs/workbench/contrib/chat/browser/actions/chatActions.ts | 3 +-- .../contrib/chat/browser/actions/chatSessionActions.ts | 3 +-- .../contrib/chat/browser/chatSessions.contribution.ts | 3 +-- .../chat/browser/chatSessions/view/chatSessionsView.ts | 4 +--- src/vs/workbench/contrib/chat/common/constants.ts | 2 ++ 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/build/lib/tsb/builder.js b/build/lib/tsb/builder.js index 6267db35f0b..f4f60cf8521 100644 --- a/build/lib/tsb/builder.js +++ b/build/lib/tsb/builder.js @@ -402,7 +402,9 @@ function createTypeScriptBuilder(config, projectFile, cmd) { messageText: `CYCLIC dependency: ${error}` }); } + delete oldErrors[filename]; newErrors[filename] = cyclicDepErrors; + cyclicDepErrors.forEach(d => onError(d)); } }).then(() => { // store the build versions to not rebuilt the next time diff --git a/build/lib/tsb/builder.ts b/build/lib/tsb/builder.ts index 79ae06d73f7..cc7a2b244b6 100644 --- a/build/lib/tsb/builder.ts +++ b/build/lib/tsb/builder.ts @@ -440,7 +440,9 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str messageText: `CYCLIC dependency: ${error}` }); } + delete oldErrors[filename]; newErrors[filename] = cyclicDepErrors; + cyclicDepErrors.forEach(d => onError(d)); } }).then(() => { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index d4a9472ad97..38d0a142e8b 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -66,14 +66,13 @@ import { IChatSessionItem, IChatSessionsService } from '../../common/chatSession import { ChatSessionUri } from '../../common/chatUri.js'; import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js'; import { IChatWidgetHistoryService } from '../../common/chatWidgetHistoryService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, VIEWLET_ID } from '../../common/constants.js'; import { ILanguageModelChatSelector, ILanguageModelsService } from '../../common/languageModels.js'; import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; import { ChatViewId, IChatWidget, IChatWidgetService, showChatView, showCopilotView } from '../chat.js'; import { IChatEditorOptions } from '../chatEditor.js'; import { ChatEditorInput, shouldShowClearEditingSessionConfirmation, showClearEditingSessionConfirmation } from '../chatEditorInput.js'; -import { VIEWLET_ID } from '../chatSessions/view/chatSessionsView.js'; import { ChatViewPane } from '../chatViewPane.js'; import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js'; import { clearChatEditor } from './chatClear.js'; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts index 98ae7370c67..54ed47797b4 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts @@ -28,7 +28,7 @@ import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatService } from '../../common/chatService.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; import { ChatSessionUri } from '../../common/chatUri.js'; -import { ChatConfiguration } from '../../common/constants.js'; +import { ChatConfiguration, VIEWLET_ID } from '../../common/constants.js'; import { ChatViewId, IChatWidgetService } from '../chat.js'; import { IChatEditorOptions } from '../chatEditor.js'; import { ChatEditorInput } from '../chatEditorInput.js'; @@ -36,7 +36,6 @@ import { ChatSessionItemWithProvider, findExistingChatEditorByUri, isLocalChatSe import { ChatViewPane } from '../chatViewPane.js'; import { ACTION_ID_OPEN_CHAT, CHAT_CATEGORY } from './chatActions.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { VIEWLET_ID } from '../chatSessions/view/chatSessionsView.js'; export interface IChatSessionContext { sessionId: string; diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts index 81cd2ecd20d..bf42f545729 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts @@ -23,11 +23,10 @@ import { IChatAgentData, IChatAgentRequest, IChatAgentService } from '../common/ import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatSession, ChatSessionStatus, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionsExtensionPoint, IChatSessionsService } from '../common/chatSessionsService.js'; import { ChatSessionUri } from '../common/chatUri.js'; -import { ChatAgentLocation, ChatModeKind } from '../common/constants.js'; +import { ChatAgentLocation, ChatModeKind, VIEWLET_ID } from '../common/constants.js'; import { CHAT_CATEGORY } from './actions/chatActions.js'; import { IChatEditorOptions } from './chatEditor.js'; import { NEW_CHAT_SESSION_ACTION_ID } from './chatSessions/common.js'; -import { VIEWLET_ID } from './chatSessions/view/chatSessionsView.js'; const extensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'chatSessions', diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/chatSessionsView.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/chatSessionsView.ts index f18d99b0945..017bc082ea7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/chatSessionsView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/chatSessionsView.ts @@ -25,14 +25,12 @@ import { IChatEntitlementService } from '../../../../../services/chat/common/cha import { IExtensionService } from '../../../../../services/extensions/common/extensions.js'; import { IWorkbenchLayoutService } from '../../../../../services/layout/browser/layoutService.js'; import { IChatSessionsService, IChatSessionItemProvider, IChatSessionsExtensionPoint } from '../../../common/chatSessionsService.js'; -import { ChatConfiguration } from '../../../common/constants.js'; +import { ChatConfiguration, VIEWLET_ID } from '../../../common/constants.js'; import { ACTION_ID_OPEN_CHAT } from '../../actions/chatActions.js'; import { ChatSessionTracker } from '../chatSessionTracker.js'; import { LocalChatSessionsProvider } from '../localChatSessionsProvider.js'; import { SessionsViewPane } from './sessionsViewPane.js'; -export const VIEWLET_ID = 'workbench.view.chat.sessions'; - export class ChatSessionsView extends Disposable implements IWorkbenchContribution { static readonly ID = 'workbench.contrib.chatSessions'; diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index e5b5b6ca3a7..4bf8c6b0bef 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -86,3 +86,5 @@ export namespace ChatAgentLocation { export const ChatUnsupportedFileSchemes = new Set([Schemas.vscodeChatEditor, Schemas.walkThrough, Schemas.vscodeChatSession, 'ccreq']); export const TodoListWidgetPositionSettingId = 'chat.todoListWidget.position'; + +export const VIEWLET_ID = 'workbench.view.chat.sessions'; From 9fa2156a37d8e863752c49495c25522c0a68bee9 Mon Sep 17 00:00:00 2001 From: lemurra_microsoft Date: Tue, 30 Sep 2025 16:20:46 +0100 Subject: [PATCH 10/16] Update status bar item hover colors for improved visibility and accessibility --- .../parts/statusbar/media/statusbarpart.css | 4 ++-- src/vs/workbench/common/theme.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index a21f003405a..5f69e79224f 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -177,8 +177,8 @@ outline-offset: -1px; } -.monaco-workbench:not(.hc-light):not(.hc-black) .part.statusbar > .items-container > .statusbar-item a:hover:not(.disabled) { - background-color: var(--vscode-statusBarItem-hoverBackground); +.monaco-workbench .part.statusbar > .items-container > .statusbar-item a:hover:not(.disabled) { + background-color: var(--vscode-statusBarItem-hoverBackground) !important; } /** Status bar entry item kinds */ diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 900b771e3bc..143a09c1bf1 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -336,18 +336,18 @@ export const STATUS_BAR_ITEM_FOCUS_BORDER = registerColor('statusBarItem.focusBo export const STATUS_BAR_ITEM_HOVER_BACKGROUND = registerColor('statusBarItem.hoverBackground', { dark: Color.white.transparent(0.12), - light: Color.white.transparent(0.12), - hcDark: Color.white.transparent(0.12), - hcLight: Color.black.transparent(0.12) + light: Color.black.transparent(0.12), + hcDark: Color.black, + hcLight: Color.white }, localize('statusBarItemHoverBackground', "Status bar item background color when hovering. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_HOVER_FOREGROUND = registerColor('statusBarItem.hoverForeground', STATUS_BAR_FOREGROUND, localize('statusBarItemHoverForeground', "Status bar item foreground color when hovering. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND = registerColor('statusBarItem.compactHoverBackground', { - dark: Color.white.transparent(0.20), - light: Color.white.transparent(0.20), - hcDark: Color.white.transparent(0.20), - hcLight: Color.black.transparent(0.20) + dark: Color.white.transparent(0.12), + light: Color.black.transparent(0.12), + hcDark: Color.black, + hcLight: Color.white }, localize('statusBarItemCompactHoverBackground', "Status bar item background color when hovering an item that contains two hovers. The status bar is shown in the bottom of the window.")); export const STATUS_BAR_PROMINENT_ITEM_FOREGROUND = registerColor('statusBarItem.prominentForeground', STATUS_BAR_FOREGROUND, localize('statusBarProminentItemForeground', "Status bar prominent items foreground color. Prominent items stand out from other status bar entries to indicate importance. The status bar is shown in the bottom of the window.")); From 836137bd808761f902457a7b96585b6b7b99091a Mon Sep 17 00:00:00 2001 From: lemurra_microsoft Date: Tue, 30 Sep 2025 16:32:23 +0100 Subject: [PATCH 11/16] Update status bar item hover background colors for improved visibility --- extensions/theme-defaults/themes/dark_modern.json | 1 + extensions/theme-defaults/themes/light_modern.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/theme-defaults/themes/dark_modern.json b/extensions/theme-defaults/themes/dark_modern.json index 27738936b27..c7e405eaff0 100644 --- a/extensions/theme-defaults/themes/dark_modern.json +++ b/extensions/theme-defaults/themes/dark_modern.json @@ -88,6 +88,7 @@ "sideBarTitle.foreground": "#CCCCCC", "statusBar.background": "#181818", "statusBar.border": "#2B2B2B", + "statusBarItem.hoverBackground": "#F1F1F133", "statusBar.debuggingBackground": "#0078D4", "statusBar.debuggingForeground": "#FFFFFF", "statusBar.focusBorder": "#0078D4", diff --git a/extensions/theme-defaults/themes/light_modern.json b/extensions/theme-defaults/themes/light_modern.json index 2251c4bac30..2440ba3802a 100644 --- a/extensions/theme-defaults/themes/light_modern.json +++ b/extensions/theme-defaults/themes/light_modern.json @@ -104,7 +104,7 @@ "statusBar.background": "#F8F8F8", "statusBar.foreground": "#3B3B3B", "statusBar.border": "#E5E5E5", - "statusBarItem.hoverBackground": "#B8B8B850", + "statusBarItem.hoverBackground": "#1F1F1F11", "statusBarItem.compactHoverBackground": "#CCCCCC", "statusBar.debuggingBackground": "#FD716C", "statusBar.debuggingForeground": "#000000", From e9bf40d9903156636fa944e46e595a9d1ad399aa Mon Sep 17 00:00:00 2001 From: lemurra_microsoft Date: Tue, 30 Sep 2025 16:40:29 +0100 Subject: [PATCH 12/16] Add hover foreground color for status bar items in light and dark themes --- extensions/theme-defaults/themes/dark_modern.json | 1 + extensions/theme-defaults/themes/light_modern.json | 1 + 2 files changed, 2 insertions(+) diff --git a/extensions/theme-defaults/themes/dark_modern.json b/extensions/theme-defaults/themes/dark_modern.json index c7e405eaff0..51e0f371c27 100644 --- a/extensions/theme-defaults/themes/dark_modern.json +++ b/extensions/theme-defaults/themes/dark_modern.json @@ -89,6 +89,7 @@ "statusBar.background": "#181818", "statusBar.border": "#2B2B2B", "statusBarItem.hoverBackground": "#F1F1F133", + "statusBarItem.hoverForeground": "#FFFFFF", "statusBar.debuggingBackground": "#0078D4", "statusBar.debuggingForeground": "#FFFFFF", "statusBar.focusBorder": "#0078D4", diff --git a/extensions/theme-defaults/themes/light_modern.json b/extensions/theme-defaults/themes/light_modern.json index 2440ba3802a..1576ef7e5c1 100644 --- a/extensions/theme-defaults/themes/light_modern.json +++ b/extensions/theme-defaults/themes/light_modern.json @@ -105,6 +105,7 @@ "statusBar.foreground": "#3B3B3B", "statusBar.border": "#E5E5E5", "statusBarItem.hoverBackground": "#1F1F1F11", + "statusBarItem.hoverForeground": "#000000", "statusBarItem.compactHoverBackground": "#CCCCCC", "statusBar.debuggingBackground": "#FD716C", "statusBar.debuggingForeground": "#000000", From f8207b7f491412dfe5a88ec0d7d816ad3c97ac94 Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Tue, 30 Sep 2025 12:38:12 -0400 Subject: [PATCH 13/16] discourage agent from using any/unknown casts in instructions (#269122) add rule about as any/unknown --- .github/copilot-instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4c7aff83c70..29bac72ad52 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -133,3 +133,4 @@ function f(x: number, y: string): void { } - Look for existing test patterns before creating new structures - Use `describe` and `test` consistently with existing patterns - If you create any temporary new files, scripts, or helper files for iteration, clean up these files by removing them at the end of the task +- Do not use `any` or `unknown` as the type for variables, parameters, or return values unless absolutely necessary. If they need type annotations, they should have proper types or interfaces defined. From c2527c9dae1433f3483433a118c5a707459e24d2 Mon Sep 17 00:00:00 2001 From: Sergei Druzhkov Date: Tue, 30 Sep 2025 21:06:38 +0300 Subject: [PATCH 14/16] Improve canSetExpressionValue check (#268952) --- src/vs/workbench/contrib/debug/browser/variablesView.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index db0a162f139..2130f6831f5 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -230,6 +230,10 @@ export class VariablesView extends ViewPane implements IDebugViewWithVariables { return !!e.treeItem.canEdit; } + if (!session.capabilities?.supportsSetVariable && !session.capabilities?.supportsSetExpression) { + return false; + } + return e instanceof Variable && !e.presentationHint?.attributes?.includes('readOnly') && !e.presentationHint?.lazy; } From 99bbe0707b48a1b4bdc84345066ed51671e4d6e7 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Tue, 30 Sep 2025 13:37:59 -0700 Subject: [PATCH 15/16] Fix name in package-lock Forgot to check this in too after fixing the main `package.json` --- extensions/mermaid-chat-features/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/mermaid-chat-features/package-lock.json b/extensions/mermaid-chat-features/package-lock.json index 38a52f7fe20..09481d2b835 100644 --- a/extensions/mermaid-chat-features/package-lock.json +++ b/extensions/mermaid-chat-features/package-lock.json @@ -1,11 +1,11 @@ { - "name": "marmaid-chat-features", + "name": "mermaid-chat-features", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "marmaid-chat-features", + "name": "mermaid-chat-features", "version": "1.0.0", "license": "MIT", "dependencies": { From c3a4ffbcf124066bf1699e229891b91564475a20 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Tue, 30 Sep 2025 13:38:40 -0700 Subject: [PATCH 16/16] mcp: update with icons added to resource templates (#269199) Followup addition from https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1565 --- .../contrib/mcp/browser/mcpResourceQuickAccess.ts | 6 ++++-- src/vs/workbench/contrib/mcp/common/mcpIcons.ts | 12 ++++++++---- src/vs/workbench/contrib/mcp/common/mcpServer.ts | 5 +++-- src/vs/workbench/contrib/mcp/common/mcpTypes.ts | 1 + .../contrib/mcp/common/modelContextProtocol.ts | 2 +- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts b/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts index 00b9f4caf4f..60a21fa126e 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts @@ -37,22 +37,24 @@ export class McpResourcePickHelper { } public static item(resource: IMcpResource | IMcpResourceTemplate): IQuickPickItem { + const icon = resource.icons.getUrl(22); + const iconPath = icon ? { dark: icon, light: icon } : undefined; if (isMcpResourceTemplate(resource)) { return { id: resource.template.template, label: resource.title || resource.name, description: resource.description, detail: localize('mcp.resource.template', 'Resource template: {0}', resource.template.template), + iconPath, }; } - const icon = resource.icons.getUrl(22); return { id: resource.uri.toString(), label: resource.title || resource.name, description: resource.description, detail: resource.mcpUri + (resource.sizeInBytes !== undefined ? ' (' + ByteSize.formatSize(resource.sizeInBytes) + ')' : ''), - iconPath: icon ? { dark: icon, light: icon } : undefined, + iconPath, }; } diff --git a/src/vs/workbench/contrib/mcp/common/mcpIcons.ts b/src/vs/workbench/contrib/mcp/common/mcpIcons.ts index dd88b812fb6..fa0a3c6b149 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpIcons.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpIcons.ts @@ -25,6 +25,7 @@ interface IIcon { sizes: { width: number; height: number }[]; } +export type ParsedMcpIcons = IIcon[]; export type StoredMcpIcons = Dto[]; @@ -68,8 +69,8 @@ function validateIcon(icon: MCP.Icon, launch: McpServerLaunch, logger: ILogger): return; } -export function parseAndValidateMcpIcon(icons: MCP.Icons, launch: McpServerLaunch, logger: ILogger): StoredMcpIcons { - const result: StoredMcpIcons = []; +export function parseAndValidateMcpIcon(icons: MCP.Icons, launch: McpServerLaunch, logger: ILogger): ParsedMcpIcons { + const result: ParsedMcpIcons = []; for (const icon of icons.icons || []) { const uri = validateIcon(icon, launch, logger); if (!uri) { @@ -92,9 +93,12 @@ export function parseAndValidateMcpIcon(icons: MCP.Icons, launch: McpServerLaunc } export class McpIcons implements IMcpIcons { - public static fromStored(icons: StoredMcpIcons | undefined) { - return new McpIcons(icons?.map(i => ({ src: URI.revive(i.src), sizes: i.sizes })) || []); + return McpIcons.fromParsed(icons?.map(i => ({ src: URI.revive(i.src), sizes: i.sizes }))); + } + + public static fromParsed(icons: ParsedMcpIcons | undefined) { + return new McpIcons(icons || []); } protected constructor(private readonly _icons: IIcon[]) { } diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 1957600fb74..c86cbc1c5c7 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -489,7 +489,7 @@ export class McpServer extends Disposable implements IMcpServer { return new AsyncIterableProducer(async emitter => { await McpServer.callOn(this, async (handler) => { for await (const resource of handler.listResourcesIterable({}, cts.token)) { - emitter.emitOne(resource.map(r => new McpResource(this, r, McpIcons.fromStored(this._parseIcons(r))))); + emitter.emitOne(resource.map(r => new McpResource(this, r, McpIcons.fromParsed(this._parseIcons(r))))); if (cts.token.isCancellationRequested) { return; } @@ -501,7 +501,7 @@ export class McpServer extends Disposable implements IMcpServer { public resourceTemplates(token?: CancellationToken): Promise { return McpServer.callOn(this, async (handler) => { const templates = await handler.listResourceTemplates({}, token); - return templates.map(t => new McpResourceTemplate(this, t)); + return templates.map(t => new McpResourceTemplate(this, t, McpIcons.fromParsed(this._parseIcons(t)))); }, token); } @@ -1074,6 +1074,7 @@ class McpResourceTemplate implements IMcpResourceTemplate { constructor( private readonly _server: McpServer, private readonly _definition: MCP.ResourceTemplate, + public readonly icons: IMcpIcons, ) { this.name = _definition.name; this.description = _definition.description; diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 376d47959ff..f566d17a9a9 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -356,6 +356,7 @@ export interface IMcpResourceTemplate { readonly description?: string; readonly mimeType?: string; readonly template: UriTemplate; + readonly icons: IMcpIcons; /** Gets string completions for the given template part. */ complete(templatePart: string, prefix: string, alreadyResolved: Record, token: CancellationToken): Promise; diff --git a/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts index 663f7247633..3b19f8ff44b 100644 --- a/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts +++ b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts @@ -635,7 +635,7 @@ export namespace MCP {/* JSON-RPC types */ /** * A template description for resources available on the server. */ - export interface ResourceTemplate extends BaseMetadata { + export interface ResourceTemplate extends BaseMetadata, Icons { /** * A URI template (according to RFC 6570) that can be used to construct resource URIs. *