Support additional actions in integrated browser (#287653)

* Add setting to redirect simple browser

* Open in external browser

* Open settings

* Move to new window

* clean

* Add element shortcut

* Menu item updates

* PR feedback
This commit is contained in:
Kyle Cutler
2026-01-14 12:43:16 -08:00
committed by GitHub
parent 7341d1baed
commit 4e9b0b7fc3
6 changed files with 199 additions and 54 deletions

View File

@@ -51,6 +51,15 @@
"default": true,
"title": "Focus Lock Indicator Enabled",
"description": "%configuration.focusLockIndicator.enabled.description%"
},
"simpleBrowser.useIntegratedBrowser": {
"type": "boolean",
"default": false,
"markdownDescription": "%configuration.useIntegratedBrowser.description%",
"scope": "application",
"tags": [
"experimental"
]
}
}
}

View File

@@ -1,5 +1,6 @@
{
"displayName": "Simple Browser",
"description": "A very basic built-in webview for displaying web content.",
"configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser."
"configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser.",
"configuration.useIntegratedBrowser.description": "When enabled, the `simpleBrowser.show` command will open URLs in the integrated browser instead of the Simple Browser webview. **Note:** This setting is experimental and only available on desktop."
}

View File

@@ -14,6 +14,8 @@ declare class URL {
const openApiCommand = 'simpleBrowser.api.open';
const showCommand = 'simpleBrowser.show';
const integratedBrowserCommand = 'workbench.action.browser.open';
const useIntegratedBrowserSetting = 'simpleBrowser.useIntegratedBrowser';
const enabledHosts = new Set<string>([
'localhost',
@@ -31,6 +33,27 @@ const enabledHosts = new Set<string>([
const openerId = 'simpleBrowser.open';
/**
* Checks if the integrated browser should be used instead of the simple browser
*/
async function shouldUseIntegratedBrowser(): Promise<boolean> {
const config = vscode.workspace.getConfiguration();
if (!config.get<boolean>(useIntegratedBrowserSetting, false)) {
return false;
}
// Verify that the integrated browser command is available
const commands = await vscode.commands.getCommands(true);
return commands.includes(integratedBrowserCommand);
}
/**
* Opens a URL in the integrated browser
*/
async function openInIntegratedBrowser(url?: string): Promise<void> {
await vscode.commands.executeCommand(integratedBrowserCommand, url);
}
export function activate(context: vscode.ExtensionContext) {
const manager = new SimpleBrowserManager(context.extensionUri);
@@ -43,6 +66,10 @@ export function activate(context: vscode.ExtensionContext) {
}));
context.subscriptions.push(vscode.commands.registerCommand(showCommand, async (url?: string) => {
if (await shouldUseIntegratedBrowser()) {
return openInIntegratedBrowser(url);
}
if (!url) {
url = await vscode.window.showInputBox({
placeHolder: vscode.l10n.t("https://example.com"),
@@ -55,11 +82,15 @@ export function activate(context: vscode.ExtensionContext) {
}
}));
context.subscriptions.push(vscode.commands.registerCommand(openApiCommand, (url: vscode.Uri, showOptions?: {
context.subscriptions.push(vscode.commands.registerCommand(openApiCommand, async (url: vscode.Uri, showOptions?: {
preserveFocus?: boolean;
viewColumn: vscode.ViewColumn;
}) => {
manager.show(url, showOptions);
if (await shouldUseIntegratedBrowser()) {
await openInIntegratedBrowser(url.toString(true));
} else {
manager.show(url, showOptions);
}
}));
context.subscriptions.push(vscode.window.registerExternalUriOpener(openerId, {
@@ -74,10 +105,14 @@ export function activate(context: vscode.ExtensionContext) {
return vscode.ExternalUriOpenerPriority.None;
},
openExternalUri(resolveUri: vscode.Uri) {
return manager.show(resolveUri, {
viewColumn: vscode.window.activeTextEditor ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active
});
async openExternalUri(resolveUri: vscode.Uri) {
if (await shouldUseIntegratedBrowser()) {
await openInIntegratedBrowser(resolveUri.toString(true));
} else {
return manager.show(resolveUri, {
viewColumn: vscode.window.activeTextEditor ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active
});
}
}
}, {
schemes: ['http', 'https'],

View File

@@ -8,7 +8,7 @@ import { Disposable } from '../../../base/common/lifecycle.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { VSBuffer } from '../../../base/common/buffer.js';
import { IBrowserViewBounds, IBrowserViewDevToolsStateEvent, IBrowserViewFocusEvent, IBrowserViewKeyDownEvent, IBrowserViewState, IBrowserViewNavigationEvent, IBrowserViewLoadingEvent, IBrowserViewLoadError, IBrowserViewTitleChangeEvent, IBrowserViewFaviconChangeEvent, IBrowserViewNewPageRequest, BrowserViewStorageScope, IBrowserViewCaptureScreenshotOptions } from '../common/browserView.js';
import { EVENT_KEY_CODE_MAP, KeyCode, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js';
import { EVENT_KEY_CODE_MAP, KeyCode, KeyMod, SCAN_CODE_STR_TO_EVENT_KEY_CODE } from '../../../base/common/keyCodes.js';
import { IThemeMainService } from '../../theme/electron-main/themeMainService.js';
import { IWindowsMainService } from '../../windows/electron-main/windows.js';
import { IBaseWindow, ICodeWindow } from '../../window/electron-main/window.js';
@@ -17,19 +17,17 @@ import { IAuxiliaryWindow } from '../../auxiliaryWindow/electron-main/auxiliaryW
import { ILogService } from '../../log/common/log.js';
import { isMacintosh } from '../../../base/common/platform.js';
const nativeShortcutKeys = new Set(['KeyA', 'KeyC', 'KeyV', 'KeyX', 'KeyZ']);
function shouldIgnoreNativeShortcut(input: Electron.Input): boolean {
const isControlInput = isMacintosh ? input.meta : input.control;
const isAltOnlyInput = input.alt && !input.control && !input.meta;
// Ignore Alt-only inputs (often used for accented characters or menu accelerators)
if (isAltOnlyInput) {
return true;
}
// Ignore Ctrl/Cmd + [A,C,V,X,Z] shortcuts to allow native handling (e.g. copy/paste)
return isControlInput && nativeShortcutKeys.has(input.code);
}
/** Key combinations that are used in system-level shortcuts. */
const nativeShortcuts = new Set([
KeyMod.CtrlCmd | KeyCode.KeyA,
KeyMod.CtrlCmd | KeyCode.KeyC,
KeyMod.CtrlCmd | KeyCode.KeyV,
KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyV,
KeyMod.CtrlCmd | KeyCode.KeyX,
...(isMacintosh ? [] : [KeyMod.CtrlCmd | KeyCode.KeyY]),
KeyMod.CtrlCmd | KeyCode.KeyZ,
KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyZ
]);
/**
* Represents a single browser view instance with its WebContentsView and all associated logic.
@@ -237,28 +235,8 @@ export class BrowserView extends Disposable {
// Key down events - listen for raw key input events
webContents.on('before-input-event', async (event, input) => {
if (input.type === 'keyDown' && !this._isSendingKeyEvent) {
if (shouldIgnoreNativeShortcut(input)) {
return;
}
const eventKeyCode = SCAN_CODE_STR_TO_EVENT_KEY_CODE[input.code] || 0;
const keyCode = EVENT_KEY_CODE_MAP[eventKeyCode] || KeyCode.Unknown;
const hasCommandModifier = input.control || input.alt || input.meta;
const isNonEditingKey =
keyCode >= KeyCode.F1 && keyCode <= KeyCode.F24 ||
keyCode >= KeyCode.AudioVolumeMute;
if (hasCommandModifier || isNonEditingKey) {
if (this.tryHandleCommand(input)) {
event.preventDefault();
this._onDidKeyCommand.fire({
key: input.key,
keyCode: eventKeyCode,
code: input.code,
ctrlKey: input.control || false,
shiftKey: input.shift || false,
altKey: input.alt || false,
metaKey: input.meta || false,
repeat: input.isAutoRepeat || false
});
}
}
});
@@ -467,6 +445,54 @@ export class BrowserView extends Disposable {
super.dispose();
}
/**
* Potentially handle an input event as a VS Code command.
* Returns `true` if the event was forwarded to VS Code and should not be handled natively.
*/
private tryHandleCommand(input: Electron.Input): boolean {
const eventKeyCode = SCAN_CODE_STR_TO_EVENT_KEY_CODE[input.code] || 0;
const keyCode = EVENT_KEY_CODE_MAP[eventKeyCode] || KeyCode.Unknown;
const isArrowKey = keyCode >= KeyCode.LeftArrow && keyCode <= KeyCode.DownArrow;
const isNonEditingKey =
keyCode === KeyCode.Escape ||
keyCode >= KeyCode.F1 && keyCode <= KeyCode.F24 ||
keyCode >= KeyCode.AudioVolumeMute;
// Ignore most Alt-only inputs (often used for accented characters or menu accelerators)
const isAltOnlyInput = input.alt && !input.control && !input.meta;
if (isAltOnlyInput && !isNonEditingKey && !isArrowKey) {
return false;
}
// Only reroute if there's a command modifier or it's a non-editing key
const hasCommandModifier = input.control || input.alt || input.meta;
if (!hasCommandModifier && !isNonEditingKey) {
return false;
}
// Ignore Ctrl/Cmd + [A,C,V,X,Z] shortcuts to allow native handling (e.g. copy/paste)
const isControlInput = isMacintosh ? input.meta : input.control;
const modifiedKeyCode = keyCode |
(isControlInput ? KeyMod.CtrlCmd : 0) |
(input.shift ? KeyMod.Shift : 0) |
(input.alt ? KeyMod.Alt : 0);
if (nativeShortcuts.has(modifiedKeyCode)) {
return false;
}
this._onDidKeyCommand.fire({
key: input.key,
keyCode: eventKeyCode,
code: input.code,
ctrlKey: input.control || false,
shiftKey: input.shift || false,
altKey: input.alt || false,
metaKey: input.meta || false,
repeat: input.isAutoRepeat || false
});
return true;
}
private windowById(windowId: number | undefined): ICodeWindow | IAuxiliaryWindow | undefined {
return this.codeWindowById(windowId) ?? this.auxiliaryWindowById(windowId);

View File

@@ -101,7 +101,7 @@ class BrowserNavigationBar extends Disposable {
{
hoverDelegate,
highlightToggledItems: true,
toolbarOptions: { primaryGroup: 'actions' },
toolbarOptions: { primaryGroup: (group) => group.startsWith('actions'), useSeparatorsInPrimaryActions: true },
menuOptions: { shouldForwardArgs: true }
}
));
@@ -431,6 +431,10 @@ export class BrowserEditor extends EditorPane {
this.updateVisibility();
}
getUrl(): string | undefined {
return this._model?.url;
}
async navigateToUrl(url: string): Promise<void> {
if (this._model) {
this.group.pinEditor(this.input); // pin editor on navigation

View File

@@ -16,6 +16,8 @@ import { BrowserViewUri } from '../../../../platform/browserView/common/browserV
import { IBrowserViewWorkbenchService } from '../common/browserView.js';
import { BrowserViewStorageScope } from '../../../../platform/browserView/common/browserView.js';
import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
// Context key expression to check if browser editor is active
const BROWSER_EDITOR_ACTIVE = ContextKeyExpr.equals('activeEditor', BrowserEditor.ID);
@@ -55,7 +57,7 @@ class GoBackAction extends Action2 {
group: 'navigation',
order: 1,
},
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_BACK),
precondition: CONTEXT_BROWSER_CAN_GO_BACK,
keybinding: {
when: BROWSER_EDITOR_ACTIVE,
weight: KeybindingWeight.WorkbenchContrib,
@@ -87,9 +89,9 @@ class GoForwardAction extends Action2 {
id: MenuId.BrowserNavigationToolbar,
group: 'navigation',
order: 2,
when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_FORWARD)
when: CONTEXT_BROWSER_CAN_GO_FORWARD
},
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_CAN_GO_FORWARD),
precondition: CONTEXT_BROWSER_CAN_GO_FORWARD,
keybinding: {
when: BROWSER_EDITOR_ACTIVE,
weight: KeybindingWeight.WorkbenchContrib,
@@ -123,7 +125,7 @@ class ReloadAction extends Action2 {
order: 3,
},
keybinding: {
when: CONTEXT_BROWSER_FOCUSED, // Keybinding is only active when focus is within the browser editor
when: CONTEXT_BROWSER_FOCUSED,
weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over debug
primary: KeyCode.F5,
secondary: [KeyMod.CtrlCmd | KeyCode.KeyR],
@@ -155,7 +157,16 @@ class AddElementToChatAction extends Action2 {
group: 'actions',
order: 1,
when: ChatContextKeys.enabled
}
},
keybinding: [{
when: BROWSER_EDITOR_ACTIVE,
weight: KeybindingWeight.WorkbenchContrib + 50, // Priority over terminal
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyC,
}, {
when: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE),
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyCode.Escape
}]
});
}
@@ -174,14 +185,18 @@ class ToggleDevToolsAction extends Action2 {
id: ToggleDevToolsAction.ID,
title: localize2('browser.toggleDevToolsAction', 'Toggle Developer Tools'),
category: BrowserCategory,
icon: Codicon.tools,
icon: Codicon.console,
f1: false,
toggled: ContextKeyExpr.equals(CONTEXT_BROWSER_DEVTOOLS_OPEN.key, true),
menu: {
id: MenuId.BrowserActionsToolbar,
group: 'actions',
order: 2,
when: BROWSER_EDITOR_ACTIVE
group: '1_developer',
order: 1,
},
keybinding: {
when: BROWSER_EDITOR_ACTIVE,
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyCode.F12
}
});
}
@@ -193,6 +208,35 @@ class ToggleDevToolsAction extends Action2 {
}
}
class OpenInExternalBrowserAction extends Action2 {
static readonly ID = 'workbench.action.browser.openExternal';
constructor() {
super({
id: OpenInExternalBrowserAction.ID,
title: localize2('browser.openExternalAction', 'Open in External Browser'),
category: BrowserCategory,
icon: Codicon.linkExternal,
f1: false,
menu: {
id: MenuId.BrowserActionsToolbar,
group: '2_export',
order: 1
}
});
}
async run(accessor: ServicesAccessor, browserEditor = accessor.get(IEditorService).activeEditorPane): Promise<void> {
if (browserEditor instanceof BrowserEditor) {
const url = browserEditor.getUrl();
if (url) {
const openerService = accessor.get(IOpenerService);
await openerService.open(url, { openExternal: true });
}
}
}
}
class ClearGlobalBrowserStorageAction extends Action2 {
static readonly ID = 'workbench.action.browser.clearGlobalStorage';
@@ -205,7 +249,7 @@ class ClearGlobalBrowserStorageAction extends Action2 {
f1: true,
menu: {
id: MenuId.BrowserActionsToolbar,
group: 'storage',
group: '3_settings',
order: 1,
when: ContextKeyExpr.equals(CONTEXT_BROWSER_STORAGE_SCOPE.key, BrowserViewStorageScope.Global)
}
@@ -230,7 +274,7 @@ class ClearWorkspaceBrowserStorageAction extends Action2 {
f1: true,
menu: {
id: MenuId.BrowserActionsToolbar,
group: 'storage',
group: '3_settings',
order: 2,
when: ContextKeyExpr.equals(CONTEXT_BROWSER_STORAGE_SCOPE.key, BrowserViewStorageScope.Workspace)
}
@@ -243,6 +287,30 @@ class ClearWorkspaceBrowserStorageAction extends Action2 {
}
}
class OpenBrowserSettingsAction extends Action2 {
static readonly ID = 'workbench.action.browser.openSettings';
constructor() {
super({
id: OpenBrowserSettingsAction.ID,
title: localize2('browser.openSettingsAction', 'Open Browser Settings'),
category: BrowserCategory,
icon: Codicon.settingsGear,
f1: false,
menu: {
id: MenuId.BrowserActionsToolbar,
group: '3_settings',
order: 3
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const preferencesService = accessor.get(IPreferencesService);
await preferencesService.openSettings({ query: '@id:workbench.browser.*,chat.sendElementsToChat.*' });
}
}
// Register actions
registerAction2(OpenIntegratedBrowserAction);
registerAction2(GoBackAction);
@@ -250,5 +318,7 @@ registerAction2(GoForwardAction);
registerAction2(ReloadAction);
registerAction2(AddElementToChatAction);
registerAction2(ToggleDevToolsAction);
registerAction2(OpenInExternalBrowserAction);
registerAction2(ClearGlobalBrowserStorageAction);
registerAction2(ClearWorkspaceBrowserStorageAction);
registerAction2(OpenBrowserSettingsAction);