From 06c51228920d546f6adce54ad666ed1f3181788e Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Mon, 16 Feb 2026 15:19:57 -0800 Subject: [PATCH 1/2] Bootstrap Playwright service (#295261) * Bootstrap Playwright service * feedback --- package-lock.json | 26 ++ package.json | 1 + src/vs/code/electron-main/app.ts | 3 + .../sharedProcess/sharedProcessMain.ts | 9 + .../browserView/common/browserView.ts | 5 + .../browserView/common/playwrightService.ts | 18 ++ .../browserViewCDPProxyServer.ts | 238 ++++++++++++++++++ .../electron-main/browserViewMainService.ts | 10 +- .../browserView/node/playwrightService.ts | 85 +++++++ .../contrib/browserView/common/browserView.ts | 5 + .../browserViewWorkbenchService.ts | 4 + .../playwrightWorkbenchService.ts | 9 + src/vs/workbench/workbench.desktop.main.ts | 1 + 13 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 src/vs/platform/browserView/common/playwrightService.ts create mode 100644 src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts create mode 100644 src/vs/platform/browserView/node/playwrightService.ts create mode 100644 src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts diff --git a/package-lock.json b/package-lock.json index c44531c484a8e..2fa11b4294c6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "native-keymap": "^3.3.5", "node-pty": "^1.2.0-beta.10", "open": "^10.1.2", + "playwright-core": "^1.58.2", "tas-client": "0.3.1", "undici": "^7.18.2", "v8-inspect-profiler": "^0.1.1", @@ -2044,6 +2045,19 @@ "node": ">=18" } }, + "node_modules/@playwright/browser-chromium/node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@playwright/test": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", @@ -13958,6 +13972,18 @@ } }, "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/playwright-core": { "version": "1.56.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", diff --git a/package.json b/package.json index c5522f49881ed..24482f3d05f60 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "native-keymap": "^3.3.5", "node-pty": "^1.2.0-beta.10", "open": "^10.1.2", + "playwright-core": "^1.58.2", "tas-client": "0.3.1", "undici": "^7.18.2", "v8-inspect-profiler": "^0.1.1", diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 0c6cc59730901..fb4dc89183063 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -38,6 +38,7 @@ import { EncryptionMainService } from '../../platform/encryption/electron-main/e import { NativeBrowserElementsMainService, INativeBrowserElementsMainService } from '../../platform/browserElements/electron-main/nativeBrowserElementsMainService.js'; import { ipcBrowserViewChannelName } from '../../platform/browserView/common/browserView.js'; import { BrowserViewMainService, IBrowserViewMainService } from '../../platform/browserView/electron-main/browserViewMainService.js'; +import { BrowserViewCDPProxyServer, IBrowserViewCDPProxyServer } from '../../platform/browserView/electron-main/browserViewCDPProxyServer.js'; import { NativeParsedArgs } from '../../platform/environment/common/argv.js'; import { IEnvironmentMainService } from '../../platform/environment/electron-main/environmentMainService.js'; import { isLaunchedFromCli } from '../../platform/environment/node/argvHelper.js'; @@ -1040,6 +1041,7 @@ export class CodeApplication extends Disposable { services.set(INativeBrowserElementsMainService, new SyncDescriptor(NativeBrowserElementsMainService, undefined, false /* proxied to other processes */)); // Browser View + services.set(IBrowserViewCDPProxyServer, new SyncDescriptor(BrowserViewCDPProxyServer, undefined, true)); services.set(IBrowserViewMainService, new SyncDescriptor(BrowserViewMainService, undefined, false /* proxied to other processes */)); // Keyboard Layout @@ -1202,6 +1204,7 @@ export class CodeApplication extends Disposable { // Browser View const browserViewChannel = ProxyChannel.fromService(accessor.get(IBrowserViewMainService), disposables); mainProcessElectronServer.registerChannel(ipcBrowserViewChannelName, browserViewChannel); + sharedProcessClient.then(client => client.registerChannel(ipcBrowserViewChannelName, browserViewChannel)); // Signing const signChannel = ProxyChannel.fromService(accessor.get(ISignService), disposables); diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index ae5f6def0951d..6569ef1fbc808 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -134,6 +134,8 @@ import { IMcpGalleryManifestService } from '../../../platform/mcp/common/mcpGall import { McpGalleryManifestIPCService } from '../../../platform/mcp/common/mcpGalleryManifestServiceIpc.js'; import { IMeteredConnectionService } from '../../../platform/meteredConnection/common/meteredConnection.js'; import { MeteredConnectionChannelClient, METERED_CONNECTION_CHANNEL } from '../../../platform/meteredConnection/common/meteredConnectionIpc.js'; +import { IPlaywrightService } from '../../../platform/browserView/common/playwrightService.js'; +import { PlaywrightService } from '../../../platform/browserView/node/playwrightService.js'; class SharedProcessMain extends Disposable implements IClientConnectionFilter { @@ -401,6 +403,9 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { // Web Content Extractor services.set(ISharedWebContentExtractorService, new SyncDescriptor(SharedWebContentExtractorService)); + // Playwright + services.set(IPlaywrightService, new SyncDescriptor(PlaywrightService)); + return new InstantiationService(services); } @@ -467,6 +472,10 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { // Web Content Extractor const webContentExtractorChannel = ProxyChannel.fromService(accessor.get(ISharedWebContentExtractorService), this._store); this.server.registerChannel('sharedWebContentExtractor', webContentExtractorChannel); + + // Playwright + const playwrightChannel = ProxyChannel.fromService(accessor.get(IPlaywrightService), this._store); + this.server.registerChannel('playwright', playwrightChannel); } private registerErrorHandler(logService: ILogService): void { diff --git a/src/vs/platform/browserView/common/browserView.ts b/src/vs/platform/browserView/common/browserView.ts index f22fd39e70b0c..5c8c9517dfb4a 100644 --- a/src/vs/platform/browserView/common/browserView.ts +++ b/src/vs/platform/browserView/common/browserView.ts @@ -275,4 +275,9 @@ export interface IBrowserViewService { * @param id The browser view identifier */ clearStorage(id: string): Promise; + + /** + * Get a CDP WebSocket endpoint URL. + */ + getDebugWebSocketEndpoint(): Promise; } diff --git a/src/vs/platform/browserView/common/playwrightService.ts b/src/vs/platform/browserView/common/playwrightService.ts new file mode 100644 index 0000000000000..75240497f8832 --- /dev/null +++ b/src/vs/platform/browserView/common/playwrightService.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from '../../instantiation/common/instantiation.js'; + +export const IPlaywrightService = createDecorator('playwrightService'); + +/** + * A service for using Playwright to connect to and automate the integrated browser. + */ +export interface IPlaywrightService { + readonly _serviceBrand: undefined; + + // TODO@kycutler: define a more specific API. + initialize(): Promise; +} diff --git a/src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts b/src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts new file mode 100644 index 0000000000000..9142b497166dd --- /dev/null +++ b/src/vs/platform/browserView/electron-main/browserViewCDPProxyServer.ts @@ -0,0 +1,238 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; +import { ILogService } from '../../log/common/log.js'; +import type * as http from 'http'; +import { AddressInfo, Socket } from 'net'; +import { upgradeToISocket } from '../../../base/parts/ipc/node/ipc.net.js'; +import { generateUuid } from '../../../base/common/uuid.js'; +import { VSBuffer } from '../../../base/common/buffer.js'; +import { CDPBrowserProxy } from '../common/cdp/proxy.js'; +import { CDPEvent, CDPRequest, CDPError, CDPErrorCode, ICDPBrowserTarget, ICDPConnection } from '../common/cdp/types.js'; +import { disposableTimeout } from '../../../base/common/async.js'; +import { ISocket } from '../../../base/parts/ipc/common/ipc.net.js'; +import { createDecorator } from '../../instantiation/common/instantiation.js'; + +export const IBrowserViewCDPProxyServer = createDecorator('browserViewCDPProxyServer'); + +export interface IBrowserViewCDPProxyServer { + readonly _serviceBrand: undefined; + + /** + * Returns a debug endpoint with a short-lived, single-use token. + */ + getWebSocketEndpoint(): Promise; +} + +/** + * WebSocket server that provides CDP debugging for browser views. + */ +export class BrowserViewCDPProxyServer extends Disposable implements IBrowserViewCDPProxyServer { + declare readonly _serviceBrand: undefined; + + private server: http.Server | undefined; + private port: number | undefined; + private readonly tokens: TokenManager; + + constructor( + private readonly browserTarget: ICDPBrowserTarget, + @ILogService private readonly logService: ILogService + ) { + super(); + + this.tokens = this._register(new TokenManager()); + } + + /** + * Returns a debug endpoint with a short-lived, single-use token in the + * WebSocket URL. The token is revoked once a WebSocket connection is made + * or after 30 seconds, whichever comes first. + */ + async getWebSocketEndpoint(): Promise { + await this.ensureServerStarted(); + + const token = await this.tokens.issueToken(); + return this.getWebSocketUrl(token); + } + + private getWebSocketUrl(token: string): string { + return `ws://localhost:${this.port}/devtools/browser?token=${token}`; + } + + private async ensureServerStarted(): Promise { + if (this.server) { + return; + } + + const http = await import('http'); + this.server = http.createServer(); + + await new Promise((resolve, reject) => { + // Only listen on localhost to prevent external access + this.server!.listen(0, '127.0.0.1', () => resolve()); + this.server!.once('error', reject); + }); + + const address = this.server.address() as AddressInfo; + this.port = address.port; + + this.server.on('request', (req, res) => this.handleHttpRequest(req, res)); + this.server.on('upgrade', (req: http.IncomingMessage, socket: Socket) => this.handleWebSocketUpgrade(req, socket)); + } + + private async handleHttpRequest(_req: http.IncomingMessage, res: http.ServerResponse): Promise { + this.logService.debug(`[BrowserViewDebugProxy] HTTP request at ${_req.url}`); + // No support for HTTP endpoints for now. + res.writeHead(404); + res.end(); + } + + private handleWebSocketUpgrade(req: http.IncomingMessage, socket: Socket): void { + const [pathname, params] = (req.url || '').split('?'); + + const token = new URLSearchParams(params).get('token'); + if (!token || !this.tokens.consumeToken(token)) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.end(); + return; + } + + const browserMatch = pathname.match(/^\/devtools\/browser(\/.*)?$/); + + this.logService.debug(`[BrowserViewDebugProxy] WebSocket upgrade requested: ${pathname}`); + + if (!browserMatch) { + this.logService.warn(`[BrowserViewDebugProxy] Rejecting WebSocket on unknown path: ${pathname}`); + socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); + socket.end(); + return; + } + + this.logService.debug(`[BrowserViewDebugProxy] WebSocket connected: ${pathname}`); + + const upgraded = upgradeToISocket(req, socket, { + debugLabel: 'browser-view-cdp-' + generateUuid(), + enableMessageSplitting: false, + }); + + if (!upgraded) { + return; + } + + const proxy = new CDPBrowserProxy(this.browserTarget); + const disposables = this.wireWebSocket(upgraded, proxy); + this._register(disposables); + this._register(upgraded); + } + + /** + * Wire a WebSocket (ISocket) to an ICDPConnection bidirectionally. + * Returns a DisposableStore that cleans up all subscriptions. + */ + private wireWebSocket(upgraded: ISocket, connection: ICDPConnection): DisposableStore { + const disposables = new DisposableStore(); + + // Socket -> Connection: parse JSON, call sendMessage, write response/error + disposables.add(upgraded.onData((rawData: VSBuffer) => { + try { + const message = rawData.toString(); + const { id, method, params, sessionId } = JSON.parse(message) as CDPRequest; + this.logService.debug(`[BrowserViewDebugProxy] <- ${message}`); + connection.sendMessage(method, params, sessionId) + .then((result: unknown) => { + const response = { id, result, sessionId }; + const responseStr = JSON.stringify(response); + this.logService.debug(`[BrowserViewDebugProxy] -> ${responseStr}`); + upgraded.write(VSBuffer.fromString(responseStr)); + }) + .catch((error: Error) => { + const response = { + id, + error: { + code: error instanceof CDPError ? error.code : CDPErrorCode.ServerError, + message: error.message || 'Unknown error' + }, + sessionId + }; + const responseStr = JSON.stringify(response); + this.logService.debug(`[BrowserViewDebugProxy] -> ${responseStr}`); + upgraded.write(VSBuffer.fromString(responseStr)); + }); + } catch (error) { + this.logService.error('[BrowserViewDebugProxy] Error parsing message:', error); + upgraded.end(); + } + })); + + // Connection -> Socket: serialize events and write + disposables.add(connection.onEvent((event: CDPEvent) => { + const eventStr = JSON.stringify(event); + this.logService.debug(`[BrowserViewDebugProxy] -> ${eventStr}`); + upgraded.write(VSBuffer.fromString(eventStr)); + })); + + // Connection close -> close socket + disposables.add(connection.onClose(() => { + this.logService.debug(`[BrowserViewDebugProxy] WebSocket closing`); + upgraded.end(); + })); + + // Socket closed -> cleanup + disposables.add(upgraded.onClose(() => { + this.logService.debug(`[BrowserViewDebugProxy] WebSocket closed`); + connection.dispose(); + disposables.dispose(); + })); + + return disposables; + } + + override dispose(): void { + if (this.server) { + this.server.close(); + this.server = undefined; + } + + super.dispose(); + } +} + +class TokenManager extends Disposable { + /** Map of currently valid single-use tokens. Each expires after 30 seconds. */ + private readonly tokens = new Map(); + + /** + * Creates a short-lived, single-use token. + * The token is revoked once consumed or after 30 seconds. + */ + async issueToken(): Promise { + const token = this.makeToken(); + this.tokens.set(token, { expiresAt: Date.now() + 30_000 }); + this._register(disposableTimeout(() => this.tokens.delete(token), 30_000)); + return token; + } + + consumeToken(token: string): boolean { + if (!token) { + return false; + } + const info = this.tokens.get(token); + if (!info) { + return false; + } + this.tokens.delete(token); + return Date.now() <= info.expiresAt; + } + + private makeToken(): string { + const bytes = crypto.getRandomValues(new Uint8Array(32)); + const binary = Array.from(bytes).map(b => String.fromCharCode(b)).join(''); + const base64 = btoa(binary); + const urlSafeToken = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); + + return urlSafeToken; + } +} diff --git a/src/vs/platform/browserView/electron-main/browserViewMainService.ts b/src/vs/platform/browserView/electron-main/browserViewMainService.ts index 34956252adeb7..f29b4c18a438c 100644 --- a/src/vs/platform/browserView/electron-main/browserViewMainService.ts +++ b/src/vs/platform/browserView/electron-main/browserViewMainService.ts @@ -15,12 +15,15 @@ import { generateUuid } from '../../../base/common/uuid.js'; import { BrowserViewUri } from '../common/browserViewUri.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; import { BrowserSession } from './browserSession.js'; +import { IBrowserViewCDPProxyServer } from './browserViewCDPProxyServer.js'; import { IProductService } from '../../product/common/productService.js'; import { CDPBrowserProxy } from '../common/cdp/proxy.js'; export const IBrowserViewMainService = createDecorator('browserViewMainService'); export interface IBrowserViewMainService extends IBrowserViewService, ICDPBrowserTarget { + readonly _serviceBrand: undefined; + tryGetBrowserView(id: string): BrowserView | undefined; } @@ -48,7 +51,8 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa @IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, - @IProductService private readonly productService: IProductService + @IProductService private readonly productService: IProductService, + @IBrowserViewCDPProxyServer private readonly cdpProxyServer: IBrowserViewCDPProxyServer, ) { super(); } @@ -359,4 +363,8 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa ); await browserSession.electronSession.clearData(); } + + async getDebugWebSocketEndpoint(): Promise { + return this.cdpProxyServer.getWebSocketEndpoint(); + } } diff --git a/src/vs/platform/browserView/node/playwrightService.ts b/src/vs/platform/browserView/node/playwrightService.ts new file mode 100644 index 0000000000000..2dcd8fbd04e9c --- /dev/null +++ b/src/vs/platform/browserView/node/playwrightService.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../base/common/lifecycle.js'; +import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { ILogService } from '../../log/common/log.js'; +import { IBrowserViewService, ipcBrowserViewChannelName } from '../common/browserView.js'; +import { IPlaywrightService } from '../common/playwrightService.js'; +import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; + +// eslint-disable-next-line local/code-import-patterns +import type { Browser } from 'playwright-core'; + +/** + * Shared-process implementation of {@link IPlaywrightService}. + */ +export class PlaywrightService extends Disposable implements IPlaywrightService { + declare readonly _serviceBrand: undefined; + + private readonly browserViewService: IBrowserViewService; + private _browser: Browser | undefined; + private _initPromise: Promise | undefined; + + constructor( + @IMainProcessService mainProcessService: IMainProcessService, + @ILogService private readonly logService: ILogService, + ) { + super(); + + const channel = mainProcessService.getChannel(ipcBrowserViewChannelName); + this.browserViewService = ProxyChannel.toService(channel); + } + + async initialize(): Promise { + if (this._browser?.isConnected()) { + return; + } + + if (this._initPromise) { + return this._initPromise; + } + + this._initPromise = (async () => { + try { + this.logService.debug('[PlaywrightService] Connecting to browser via CDP'); + + const playwright = await import('playwright-core'); + const endpoint = await this.browserViewService.getDebugWebSocketEndpoint(); + const browser = await playwright.chromium.connectOverCDP(endpoint); + + this.logService.debug('[PlaywrightService] Connected to browser'); + + browser.on('disconnected', () => { + this.logService.debug('[PlaywrightService] Browser disconnected'); + if (this._browser === browser) { + this._browser = undefined; + } + }); + + // This can happen if the service was disposed while we were waiting for the connection. In that case, clean up immediately. + if (this._initPromise === undefined) { + browser.close().catch(() => { /* ignore */ }); + throw new Error('PlaywrightService was disposed during initialization'); + } + + this._browser = browser; + } finally { + this._initPromise = undefined; + } + })(); + + return this._initPromise; + } + + override dispose(): void { + if (this._browser) { + this._browser.close().catch(() => { /* ignore */ }); + this._browser = undefined; + } + this._initPromise = undefined; + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/browserView/common/browserView.ts b/src/vs/workbench/contrib/browserView/common/browserView.ts index 732fa1974e481..d717ada29b959 100644 --- a/src/vs/workbench/contrib/browserView/common/browserView.ts +++ b/src/vs/workbench/contrib/browserView/common/browserView.ts @@ -68,6 +68,11 @@ export interface IBrowserViewWorkbenchService { * Clear all storage data for the current workspace browser session */ clearWorkspaceStorage(): Promise; + + /** + * Get the endpoint for connecting to a browser view's CDP proxy server + */ + getDebugWebSocketEndpoint(): Promise; } diff --git a/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts b/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts index 68d2376c587d1..30199e27cbf40 100644 --- a/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts +++ b/src/vs/workbench/contrib/browserView/electron-browser/browserViewWorkbenchService.ts @@ -54,4 +54,8 @@ export class BrowserViewWorkbenchService implements IBrowserViewWorkbenchService const workspaceId = this.workspaceContextService.getWorkspace().id; return this._browserViewService.clearWorkspaceStorage(workspaceId); } + + async getDebugWebSocketEndpoint() { + return this._browserViewService.getDebugWebSocketEndpoint(); + } } diff --git a/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts b/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts new file mode 100644 index 0000000000000..f50672fd91398 --- /dev/null +++ b/src/vs/workbench/services/browserView/electron-browser/playwrightWorkbenchService.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IPlaywrightService } from '../../../../platform/browserView/common/playwrightService.js'; +import { registerSharedProcessRemoteService } from '../../../../platform/ipc/electron-browser/services.js'; + +registerSharedProcessRemoteService(IPlaywrightService, 'playwright'); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index e720674e03f88..8291f38e8d17d 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -91,6 +91,7 @@ import '../platform/userDataProfile/electron-browser/userDataProfileStorageServi import './services/auxiliaryWindow/electron-browser/auxiliaryWindowService.js'; import '../platform/extensionManagement/electron-browser/extensionsProfileScannerService.js'; import '../platform/webContentExtractor/electron-browser/webContentExtractorService.js'; +import './services/browserView/electron-browser/playwrightWorkbenchService.js'; import './services/process/electron-browser/processService.js'; import './services/power/electron-browser/powerService.js'; From 9135a7b73a2e1d720ed718a631393b572a417d2c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 02:59:04 +0000 Subject: [PATCH 2/2] Make session target picker responsive for all session types (#295034) * Initial plan * Make session target picker responsive for all session types Update renderLabel logic to hide labels for all session types when the view is narrow (width < 650px), not just for the Local type. All session types have icons, so they can safely display as icon-only when space is constrained. Fixes the issue where non-Local session types would still show labels in narrow views, causing visual clutter. Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com> --- .../chat/browser/widget/input/sessionTargetPickerActionItem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index c5ab3aae3f321..7095abd9ecdb4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -205,7 +205,7 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { const labelElements = []; labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); - if (currentType !== AgentSessionProviders.Local || !this.pickerOptions.onlyShowIconsForDefaultActions.get()) { + if (!this.pickerOptions.onlyShowIconsForDefaultActions.get()) { labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); } labelElements.push(...renderLabelWithIcons(`$(chevron-down)`));