Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4bd5ae0
[WIP] Integrated Browser
kycutler Nov 20, 2025
8c37204
clean
kycutler Nov 21, 2025
feea74d
refactor
kycutler Nov 21, 2025
4b29dda
structure
kycutler Nov 22, 2025
e1ebea2
focus
kycutler Nov 24, 2025
aa1e9f1
tooltips
kycutler Nov 24, 2025
e2c43db
rename
kycutler Nov 24, 2025
80b1b68
polish
kycutler Nov 24, 2025
33efb28
start unpinned
kycutler Nov 24, 2025
9ecfeda
More polish
kycutler Nov 25, 2025
d62b23d
commands
kycutler Nov 26, 2025
b34d92e
tweaks, new tab support
kycutler Nov 26, 2025
759a495
clean
kycutler Nov 26, 2025
7557c13
shortcut fixes
kycutler Nov 26, 2025
a1336ab
warnings
kycutler Nov 26, 2025
39031a3
Update src/vs/workbench/contrib/browserView/electron-browser/browserE…
kycutler Nov 27, 2025
bc6de66
Telemetry
kycutler Nov 27, 2025
d2ec0ec
load errors
kycutler Nov 27, 2025
d7736eb
PR feedback
kycutler Nov 27, 2025
0245ed4
PR feedback
kycutler Dec 1, 2025
1e54da1
Merge branch 'main' into kycutler/rich-browser
kycutler Dec 1, 2025
faf1c6c
Permissions, unloads, trust
kycutler Dec 4, 2025
7c805eb
Storage controls
kycutler Dec 4, 2025
58d54aa
Handle render process gone
kycutler Dec 4, 2025
5a5bf55
Merge branch 'main' into kycutler/rich-browser
kycutler Dec 4, 2025
8229bec
devtools
kycutler Dec 5, 2025
9c00484
Screenshot rect
kycutler Dec 5, 2025
d8b430c
close
kycutler Dec 6, 2025
0e95ee2
Fix focused context
kycutler Dec 8, 2025
5b6cfd1
Merge branch 'main' into kycutler/rich-browser
kycutler Dec 9, 2025
86167ee
Fix merge
kycutler Dec 9, 2025
aca4396
disposables
kycutler Dec 9, 2025
077294b
Merge branch 'main' into kycutler/rich-browser
bpasero Dec 19, 2025
97e1fad
:lipstick:
bpasero Dec 19, 2025
a656469
Merge branch 'main' into kycutler/rich-browser
kycutler Dec 17, 2025
ac7df40
Multi-window improvements
kycutler Dec 17, 2025
b6a19a8
Fix reopen
kycutler Dec 19, 2025
ca8dcbb
PR feedback
kycutler Dec 19, 2025
39beb94
Actions fixes
kycutler Dec 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/vs/base/common/keyCodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ const userSettingsUSMap = new KeyCodeStrMap();
const userSettingsGeneralMap = new KeyCodeStrMap();
export const EVENT_KEY_CODE_MAP: { [keyCode: number]: KeyCode } = new Array(230);
export const NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE: { [nativeKeyCode: string]: KeyCode } = {};
export const SCAN_CODE_STR_TO_EVENT_KEY_CODE: { [scanCodeStr: string]: number } = {};
const scanCodeIntToStr: string[] = [];
const scanCodeStrToInt: { [code: string]: number } = Object.create(null);
const scanCodeLowerCaseStrToInt: { [code: string]: number } = Object.create(null);
Expand Down Expand Up @@ -755,6 +756,9 @@ for (let i = 0; i <= KeyCode.MAX_VALUE; i++) {
if (eventKeyCode) {
EVENT_KEY_CODE_MAP[eventKeyCode] = keyCode;
}
if (scanCodeStr) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for such a low level change, it'd be good to have a test.

SCAN_CODE_STR_TO_EVENT_KEY_CODE[scanCodeStr] = eventKeyCode;
}
if (vkey) {
NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE[vkey] = keyCode;
}
Expand Down
5 changes: 5 additions & 0 deletions src/vs/base/common/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ export namespace Schemas {
*/
export const vscodeWebview = 'vscode-webview';

/**
* Scheme used for integrated browser tabs using WebContentsView.
*/
export const vscodeBrowser = 'vscode-browser';

/**
* Scheme used for extension pages
*/
Expand Down
17 changes: 15 additions & 2 deletions src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { DialogMainService, IDialogMainService } from '../../platform/dialogs/el
import { IEncryptionMainService } from '../../platform/encryption/common/encryptionService.js';
import { EncryptionMainService } from '../../platform/encryption/electron-main/encryptionMainService.js';
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 { NativeParsedArgs } from '../../platform/environment/common/argv.js';
import { IEnvironmentMainService } from '../../platform/environment/electron-main/environmentMainService.js';
import { isLaunchedFromCli } from '../../platform/environment/node/argvHelper.js';
Expand Down Expand Up @@ -411,11 +413,15 @@ export class CodeApplication extends Disposable {
this.auxiliaryWindowsMainService?.registerWindow(contents);
}

// Block any in-page navigation
// Handle any in-page navigation
contents.on('will-navigate', event => {
if (BrowserViewMainService.isBrowserViewWebContents(contents)) {
return; // Allow navigation in integrated browser views
}

this.logService.error('webContents#will-navigate: Prevented webcontent navigation');

event.preventDefault();
event.preventDefault(); // Prevent any in-page navigation
});

// All Windows: only allow about:blank auxiliary windows to open
Expand Down Expand Up @@ -1021,6 +1027,9 @@ export class CodeApplication extends Disposable {
// Browser Elements
services.set(INativeBrowserElementsMainService, new SyncDescriptor(NativeBrowserElementsMainService, undefined, false /* proxied to other processes */));

// Browser View
services.set(IBrowserViewMainService, new SyncDescriptor(BrowserViewMainService, undefined, false /* proxied to other processes */));

// Keyboard Layout
services.set(IKeyboardLayoutMainService, new SyncDescriptor(KeyboardLayoutMainService));

Expand Down Expand Up @@ -1168,6 +1177,10 @@ export class CodeApplication extends Disposable {
mainProcessElectronServer.registerChannel('browserElements', browserElementsChannel);
sharedProcessClient.then(client => client.registerChannel('browserElements', browserElementsChannel));

// Browser View
const browserViewChannel = ProxyChannel.fromService(accessor.get(IBrowserViewMainService), disposables);
mainProcessElectronServer.registerChannel(ipcBrowserViewChannelName, browserViewChannel);

// Signing
const signChannel = ProxyChannel.fromService(accessor.get(ISignService), disposables);
mainProcessElectronServer.registerChannel('sign', signChannel);
Expand Down
2 changes: 2 additions & 0 deletions src/vs/platform/actions/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ export class MenuId {
static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar');
static readonly DiffEditorHunkToolbar = new MenuId('DiffEditorHunkToolbar');
static readonly DiffEditorSelectionToolbar = new MenuId('DiffEditorSelectionToolbar');
static readonly BrowserNavigationToolbar = new MenuId('BrowserNavigationToolbar');
static readonly BrowserActionsToolbar = new MenuId('BrowserActionsToolbar');
static readonly AgentSessionsViewerFilterSubMenu = new MenuId('AgentSessionsViewerFilterSubMenu');
static readonly AgentSessionsContext = new MenuId('AgentSessionsContext');
static readonly AgentSessionsCreateSubMenu = new MenuId('AgentSessionsCreateSubMenu');
Expand Down
216 changes: 216 additions & 0 deletions src/vs/platform/browserView/common/browserView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Event } from '../../../base/common/event.js';
import { VSBuffer } from '../../../base/common/buffer.js';

export interface IBrowserViewBounds {
windowId: number;
x: number;
y: number;
width: number;
height: number;
zoomFactor: number;
}

export interface IBrowserViewCaptureScreenshotOptions {
quality?: number;
rect?: { x: number; y: number; width: number; height: number };
}

export interface IBrowserViewState {
url: string;
title: string;
canGoBack: boolean;
canGoForward: boolean;
loading: boolean;
isDevToolsOpen: boolean;
lastScreenshot: VSBuffer | undefined;
lastFavicon: string | undefined;
lastError: IBrowserViewLoadError | undefined;
storageScope: BrowserViewStorageScope;
}

export interface IBrowserViewNavigationEvent {
url: string;
canGoBack: boolean;
canGoForward: boolean;
}

export interface IBrowserViewLoadingEvent {
loading: boolean;
error?: IBrowserViewLoadError;
}

export interface IBrowserViewLoadError {
url: string;
errorCode: number;
errorDescription: string;
}

export interface IBrowserViewFocusEvent {
focused: boolean;
}

export interface IBrowserViewDevToolsStateEvent {
isDevToolsOpen: boolean;
}

export interface IBrowserViewKeyDownEvent {
key: string;
keyCode: number;
code: string;
ctrlKey: boolean;
shiftKey: boolean;
altKey: boolean;
metaKey: boolean;
repeat: boolean;
}

export interface IBrowserViewTitleChangeEvent {
title: string;
}

export interface IBrowserViewFaviconChangeEvent {
favicon: string;
}

export interface IBrowserViewNewPageRequest {
url: string;
name?: string;
background: boolean;
}

export enum BrowserViewStorageScope {
Global = 'global',
Workspace = 'workspace',
Ephemeral = 'ephemeral'
}

export const ipcBrowserViewChannelName = 'browserView';

export interface IBrowserViewService {
/**
* Dynamic events that return an Event for a specific browser view ID.
*/
onDynamicDidNavigate(id: string): Event<IBrowserViewNavigationEvent>;
onDynamicDidChangeLoadingState(id: string): Event<IBrowserViewLoadingEvent>;
onDynamicDidChangeFocus(id: string): Event<IBrowserViewFocusEvent>;
onDynamicDidChangeDevToolsState(id: string): Event<IBrowserViewDevToolsStateEvent>;
onDynamicDidKeyCommand(id: string): Event<IBrowserViewKeyDownEvent>;
onDynamicDidChangeTitle(id: string): Event<IBrowserViewTitleChangeEvent>;
onDynamicDidChangeFavicon(id: string): Event<IBrowserViewFaviconChangeEvent>;
onDynamicDidRequestNewPage(id: string): Event<IBrowserViewNewPageRequest>;
onDynamicDidClose(id: string): Event<void>;

/**
* Get or create a browser view instance
* @param id The browser view identifier
* @param scope The storage scope for the browser view. Ignored if the view already exists.
* @param workspaceId Workspace identifier for session isolation. Only used if scope is 'workspace'.
*/
getOrCreateBrowserView(id: string, scope: BrowserViewStorageScope, workspaceId?: string): Promise<IBrowserViewState>;

/**
* Destroy a browser view instance
* @param id The browser view identifier
*/
destroyBrowserView(id: string): Promise<void>;

/**
* Update the bounds of a browser view
* @param id The browser view identifier
* @param bounds The new bounds for the view
*/
layout(id: string, bounds: IBrowserViewBounds): Promise<void>;

/**
* Set the visibility of a browser view
* @param id The browser view identifier
* @param visible Whether the view should be visible
*/
setVisible(id: string, visible: boolean): Promise<void>;

/**
* Navigate the browser view to a URL
* @param id The browser view identifier
* @param url The URL to navigate to
*/
loadURL(id: string, url: string): Promise<void>;

/**
* Get the current URL of a browser view
* @param id The browser view identifier
*/
getURL(id: string): Promise<string>;

/**
* Go back in navigation history
* @param id The browser view identifier
*/
goBack(id: string): Promise<void>;

/**
* Go forward in navigation history
* @param id The browser view identifier
*/
goForward(id: string): Promise<void>;

/**
* Reload the current page
* @param id The browser view identifier
*/
reload(id: string): Promise<void>;

/**
* Toggle developer tools for the browser view.
* @param id The browser view identifier
*/
toggleDevTools(id: string): Promise<void>;

/**
* Check if the view can go back
* @param id The browser view identifier
*/
canGoBack(id: string): Promise<boolean>;

/**
* Check if the view can go forward
* @param id The browser view identifier
*/
canGoForward(id: string): Promise<boolean>;

/**
* Capture a screenshot of the browser view
* @param id The browser view identifier
* @param options Screenshot options (quality and rect)
* @returns Screenshot as a buffer
*/
captureScreenshot(id: string, options?: IBrowserViewCaptureScreenshotOptions): Promise<VSBuffer>;

/**
* Dispatch a key event to the browser view
* @param id The browser view identifier
* @param keyEvent The key event data
*/
dispatchKeyEvent(id: string, keyEvent: IBrowserViewKeyDownEvent): Promise<void>;

/**
* Focus the browser view
* @param id The browser view identifier
*/
focus(id: string): Promise<void>;

/**
* Clear all storage data for the global browser session
*/
clearGlobalStorage(): Promise<void>;

/**
* Clear all storage data for a specific workspace browser session
* @param workspaceId The workspace identifier
*/
clearWorkspaceStorage(workspaceId: string): Promise<void>;
}
62 changes: 62 additions & 0 deletions src/vs/platform/browserView/common/browserViewUri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Schemas } from '../../../base/common/network.js';
import { URI } from '../../../base/common/uri.js';
import { generateUuid } from '../../../base/common/uuid.js';

/**
* Helper for creating and parsing browser view URIs.
*/
export namespace BrowserViewUri {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also seems like a good class for unit tests


export const scheme = Schemas.vscodeBrowser;

/**
* Creates a resource URI for a browser view with the given URL.
* Optionally accepts an ID; if not provided, a new UUID is generated.
*/
export function forUrl(url: string | undefined, id?: string): URI {
const viewId = id ?? generateUuid();
return URI.from({
scheme,
path: `/${viewId}`,
query: url ? `url=${encodeURIComponent(url)}` : undefined
});
}

/**
* Parses a browser view resource URI to extract the ID and URL.
*/
export function parse(resource: URI): { id: string; url: string } | undefined {
if (resource.scheme !== scheme) {
return undefined;
}

// Remove leading slash if present
const id = resource.path.startsWith('/') ? resource.path.substring(1) : resource.path;
if (!id) {
return undefined;
}

const url = resource.query ? new URLSearchParams(resource.query).get('url') ?? '' : '';

return { id, url };
}

/**
* Extracts the ID from a browser view resource URI.
*/
export function getId(resource: URI): string | undefined {
return parse(resource)?.id;
}

/**
* Extracts the URL from a browser view resource URI.
*/
export function getUrl(resource: URI): string | undefined {
return parse(resource)?.url;
}
}
Loading
Loading