Path: blob/main/src/vs/workbench/contrib/browserView/electron-browser/browserEditor.ts
5248 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './media/browser.css';6import { localize } from '../../../../nls.js';7import { $, addDisposableListener, Dimension, EventType, IDomPosition, registerExternalFocusChecker } from '../../../../base/browser/dom.js';8import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';9import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';10import { RawContextKey, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';11import { MenuId } from '../../../../platform/actions/common/actions.js';12import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';13import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';14import { AUX_WINDOW_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';15import { EditorPane } from '../../../browser/parts/editor/editorPane.js';16import { IEditorOpenContext } from '../../../common/editor.js';17import { BrowserEditorInput } from './browserEditorInput.js';18import { BrowserViewUri } from '../../../../platform/browserView/common/browserViewUri.js';19import { IBrowserViewModel } from '../../browserView/common/browserView.js';20import { IThemeService } from '../../../../platform/theme/common/themeService.js';21import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';22import { IStorageService } from '../../../../platform/storage/common/storage.js';23import { IBrowserViewKeyDownEvent, IBrowserViewNavigationEvent, IBrowserViewLoadError, BrowserNewPageLocation } from '../../../../platform/browserView/common/browserView.js';24import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';25import { IEditorOptions } from '../../../../platform/editor/common/editor.js';26import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';27import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';28import { BrowserOverlayManager, BrowserOverlayType, IBrowserOverlayInfo } from './overlayManager.js';29import { getZoomFactor, onDidChangeZoomLevel } from '../../../../base/browser/browser.js';30import { ILogService } from '../../../../platform/log/common/log.js';31import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';32import { Lazy } from '../../../../base/common/lazy.js';33import { WorkbenchHoverDelegate } from '../../../../platform/hover/browser/hover.js';34import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';35import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';36import { IBrowserElementsService } from '../../../services/browserElements/browser/browserElementsService.js';37import { IChatWidgetService } from '../../chat/browser/chat.js';38import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';39import { BrowserFindWidget, CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE } from './browserFindWidget.js';40import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';41import { ThemeIcon } from '../../../../base/common/themables.js';42import { Codicon } from '../../../../base/common/codicons.js';43import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js';44import { IChatRequestVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js';45import { IBrowserTargetLocator, getDisplayNameFromOuterHTML } from '../../../../platform/browserElements/common/browserElements.js';46import { logBrowserOpen } from './browserViewTelemetry.js';47import { URI } from '../../../../base/common/uri.js';4849export const CONTEXT_BROWSER_CAN_GO_BACK = new RawContextKey<boolean>('browserCanGoBack', false, localize('browser.canGoBack', "Whether the browser can go back"));50export const CONTEXT_BROWSER_CAN_GO_FORWARD = new RawContextKey<boolean>('browserCanGoForward', false, localize('browser.canGoForward', "Whether the browser can go forward"));51export const CONTEXT_BROWSER_FOCUSED = new RawContextKey<boolean>('browserFocused', true, localize('browser.editorFocused', "Whether the browser editor is focused"));52export const CONTEXT_BROWSER_STORAGE_SCOPE = new RawContextKey<string>('browserStorageScope', '', localize('browser.storageScope', "The storage scope of the current browser view"));53export const CONTEXT_BROWSER_HAS_URL = new RawContextKey<boolean>('browserHasUrl', false, localize('browser.hasUrl', "Whether the browser has a URL loaded"));54export const CONTEXT_BROWSER_DEVTOOLS_OPEN = new RawContextKey<boolean>('browserDevToolsOpen', false, localize('browser.devToolsOpen', "Whether developer tools are open for the current browser view"));55export const CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE = new RawContextKey<boolean>('browserElementSelectionActive', false, localize('browser.elementSelectionActive', "Whether element selection is currently active"));5657// Re-export find widget context keys for use in actions58export { CONTEXT_BROWSER_FIND_WIDGET_FOCUSED, CONTEXT_BROWSER_FIND_WIDGET_VISIBLE };5960/**61* Get the original implementation of HTMLElement focus (without window auto-focusing)62* before it gets overridden by the workbench.63*/64const originalHtmlElementFocus = HTMLElement.prototype.focus;6566class BrowserNavigationBar extends Disposable {67private readonly _urlInput: HTMLInputElement;6869constructor(70editor: BrowserEditor,71container: HTMLElement,72instantiationService: IInstantiationService,73scopedContextKeyService: IContextKeyService74) {75super();7677// Create hover delegate for toolbar buttons78const hoverDelegate = this._register(79instantiationService.createInstance(80WorkbenchHoverDelegate,81'element',82undefined,83{ position: { hoverPosition: HoverPosition.ABOVE } }84)85);8687// Create navigation toolbar (left side) with scoped context88const navContainer = $('.browser-nav-toolbar');89const scopedInstantiationService = instantiationService.createChild(new ServiceCollection(90[IContextKeyService, scopedContextKeyService]91));92const navToolbar = this._register(scopedInstantiationService.createInstance(93MenuWorkbenchToolBar,94navContainer,95MenuId.BrowserNavigationToolbar,96{97hoverDelegate,98highlightToggledItems: true,99// Render all actions inline regardless of group100toolbarOptions: { primaryGroup: () => true, useSeparatorsInPrimaryActions: true },101menuOptions: { shouldForwardArgs: true }102}103));104105// URL input106this._urlInput = $<HTMLInputElement>('input.browser-url-input');107this._urlInput.type = 'text';108this._urlInput.placeholder = localize('browser.urlPlaceholder', "Enter a URL");109110// Create actions toolbar (right side) with scoped context111const actionsContainer = $('.browser-actions-toolbar');112const actionsToolbar = this._register(scopedInstantiationService.createInstance(113MenuWorkbenchToolBar,114actionsContainer,115MenuId.BrowserActionsToolbar,116{117hoverDelegate,118highlightToggledItems: true,119toolbarOptions: { primaryGroup: (group) => group.startsWith('actions'), useSeparatorsInPrimaryActions: true },120menuOptions: { shouldForwardArgs: true }121}122));123124navToolbar.context = editor;125actionsToolbar.context = editor;126127// Assemble layout: nav | url | actions128container.appendChild(navContainer);129container.appendChild(this._urlInput);130container.appendChild(actionsContainer);131132// Setup URL input handler133this._register(addDisposableListener(this._urlInput, EventType.KEY_DOWN, (e: KeyboardEvent) => {134if (e.key === 'Enter') {135const url = this._urlInput.value.trim();136if (url) {137editor.navigateToUrl(url);138}139}140}));141142// Select all URL bar text when the URL bar receives focus (like in regular browsers)143this._register(addDisposableListener(this._urlInput, EventType.FOCUS, () => {144this._urlInput.select();145}));146}147148/**149* Update the navigation bar state from a navigation event150*/151updateFromNavigationEvent(event: IBrowserViewNavigationEvent): void {152// URL input is updated, action enablement is handled by context keys153this._urlInput.value = event.url;154}155156/**157* Focus the URL input and select all text158*/159focusUrlInput(): void {160this._urlInput.select();161this._urlInput.focus();162}163164clear(): void {165this._urlInput.value = '';166}167}168169export class BrowserEditor extends EditorPane {170static readonly ID = 'workbench.editor.browser';171172private _overlayVisible = false;173private _editorVisible = false;174private _currentKeyDownEvent: IBrowserViewKeyDownEvent | undefined;175176private _navigationBar!: BrowserNavigationBar;177private _browserContainer!: HTMLElement;178private _placeholderScreenshot!: HTMLElement;179private _overlayPauseContainer!: HTMLElement;180private _overlayPauseHeading!: HTMLElement;181private _overlayPauseDetail!: HTMLElement;182private _errorContainer!: HTMLElement;183private _welcomeContainer!: HTMLElement;184private _findWidgetContainer!: HTMLElement;185private _findWidget!: Lazy<BrowserFindWidget>;186private _canGoBackContext!: IContextKey<boolean>;187private _canGoForwardContext!: IContextKey<boolean>;188private _storageScopeContext!: IContextKey<string>;189private _hasUrlContext!: IContextKey<boolean>;190private _devToolsOpenContext!: IContextKey<boolean>;191private _elementSelectionActiveContext!: IContextKey<boolean>;192193private _model: IBrowserViewModel | undefined;194private readonly _inputDisposables = this._register(new DisposableStore());195private overlayManager: BrowserOverlayManager | undefined;196private _elementSelectionCts: CancellationTokenSource | undefined;197private _screenshotTimeout: ReturnType<typeof setTimeout> | undefined;198199constructor(200group: IEditorGroup,201@ITelemetryService telemetryService: ITelemetryService,202@IThemeService themeService: IThemeService,203@IStorageService storageService: IStorageService,204@IKeybindingService private readonly keybindingService: IKeybindingService,205@ILogService private readonly logService: ILogService,206@IInstantiationService private readonly instantiationService: IInstantiationService,207@IContextKeyService private readonly contextKeyService: IContextKeyService,208@IEditorService private readonly editorService: IEditorService,209@IBrowserElementsService private readonly browserElementsService: IBrowserElementsService,210@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,211@IConfigurationService private readonly configurationService: IConfigurationService212) {213super(BrowserEditor.ID, group, telemetryService, themeService, storageService);214}215216protected override createEditor(parent: HTMLElement): void {217// Create scoped context key service for this editor instance218const contextKeyService = this._register(this.contextKeyService.createScoped(parent));219220// Create window-specific overlay manager for this editor221this.overlayManager = this._register(new BrowserOverlayManager(this.window));222223// Bind navigation capability context keys224this._canGoBackContext = CONTEXT_BROWSER_CAN_GO_BACK.bindTo(contextKeyService);225this._canGoForwardContext = CONTEXT_BROWSER_CAN_GO_FORWARD.bindTo(contextKeyService);226this._storageScopeContext = CONTEXT_BROWSER_STORAGE_SCOPE.bindTo(contextKeyService);227this._hasUrlContext = CONTEXT_BROWSER_HAS_URL.bindTo(contextKeyService);228this._devToolsOpenContext = CONTEXT_BROWSER_DEVTOOLS_OPEN.bindTo(contextKeyService);229this._elementSelectionActiveContext = CONTEXT_BROWSER_ELEMENT_SELECTION_ACTIVE.bindTo(contextKeyService);230231// Currently this is always true since it is scoped to the editor container232CONTEXT_BROWSER_FOCUSED.bindTo(contextKeyService);233234// Create root container235const root = $('.browser-root');236parent.appendChild(root);237238// Create toolbar with navigation buttons and URL input239const toolbar = $('.browser-toolbar');240241// Create navigation bar widget with scoped context242this._navigationBar = this._register(new BrowserNavigationBar(this, toolbar, this.instantiationService, contextKeyService));243244root.appendChild(toolbar);245246// Create find widget container (between toolbar and browser container)247this._findWidgetContainer = $('.browser-find-widget-wrapper');248root.appendChild(this._findWidgetContainer);249250// Create find widget (lazy initialization)251this._findWidget = new Lazy(() => {252const findWidget = this.instantiationService.createInstance(253BrowserFindWidget,254this._findWidgetContainer255);256if (this._model) {257findWidget.setModel(this._model);258}259return findWidget;260});261this._register(toDisposable(() => this._findWidget.rawValue?.dispose()));262263// Create browser container (stub element for positioning)264this._browserContainer = $('.browser-container');265this._browserContainer.tabIndex = 0; // make focusable266root.appendChild(this._browserContainer);267268// Create placeholder screenshot (background placeholder when WebContentsView is hidden)269this._placeholderScreenshot = $('.browser-placeholder-screenshot');270this._browserContainer.appendChild(this._placeholderScreenshot);271272// Create overlay pause container (hidden by default via CSS)273this._overlayPauseContainer = $('.browser-overlay-paused');274const overlayPauseMessage = $('.browser-overlay-paused-message');275this._overlayPauseHeading = $('.browser-overlay-paused-heading');276this._overlayPauseDetail = $('.browser-overlay-paused-detail');277overlayPauseMessage.appendChild(this._overlayPauseHeading);278overlayPauseMessage.appendChild(this._overlayPauseDetail);279this._overlayPauseContainer.appendChild(overlayPauseMessage);280this._browserContainer.appendChild(this._overlayPauseContainer);281282// Create error container (hidden by default)283this._errorContainer = $('.browser-error-container');284this._errorContainer.style.display = 'none';285this._browserContainer.appendChild(this._errorContainer);286287// Create welcome container (shown when no URL is loaded)288this._welcomeContainer = this.createWelcomeContainer();289this._browserContainer.appendChild(this._welcomeContainer);290291this._register(addDisposableListener(this._browserContainer, EventType.FOCUS, (event) => {292// When the browser container gets focus, make sure the browser view also gets focused.293// But only if focus was already in the workbench (and not e.g. clicking back into the workbench from the browser view).294if (event.relatedTarget && this._model && this.shouldShowView) {295void this._model.focus();296}297}));298299// Register external focus checker so that cross-window focus logic knows when300// this browser view has focus (since it's outside the normal DOM tree).301// Include window info so that UI like dialogs appear in the correct window.302this._register(registerExternalFocusChecker(() => ({303hasFocus: this._model?.focused ?? false,304window: this._model?.focused ? this.window : undefined305})));306307// Automatically call layoutBrowserContainer() when the browser container changes size.308// Be careful to use `ResizeObserver` from the target window to avoid cross-window issues.309const resizeObserver = new this.window.ResizeObserver(() => this.layoutBrowserContainer());310resizeObserver.observe(this._browserContainer);311this._register(toDisposable(() => resizeObserver.disconnect()));312}313314override async setInput(input: BrowserEditorInput, options: IEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {315await super.setInput(input, options, context, token);316if (token.isCancellationRequested) {317return;318}319320this._inputDisposables.clear();321322// Resolve the browser view model from the input323this._model = await input.resolve();324if (token.isCancellationRequested || this.input !== input) {325return;326}327328this._storageScopeContext.set(this._model.storageScope);329this._devToolsOpenContext.set(this._model.isDevToolsOpen);330331// Update find widget with new model332this._findWidget.rawValue?.setModel(this._model);333334// Clean up on input disposal335this._inputDisposables.add(input.onWillDispose(() => {336this._model = undefined;337}));338339// Initialize UI state and context keys from model340this.updateNavigationState({341url: this._model.url,342canGoBack: this._model.canGoBack,343canGoForward: this._model.canGoForward344});345this.setBackgroundImage(this._model.screenshot);346347if (context.newInGroup) {348if (this._model.url) {349this._browserContainer.focus();350} else {351this.focusUrlInput();352}353}354355// Start / stop screenshots when the model visibility changes356this._inputDisposables.add(this._model.onDidChangeVisibility(() => this.doScreenshot()));357358// Listen to model events for UI updates359this._inputDisposables.add(this._model.onDidKeyCommand(keyEvent => {360// Handle like webview does - convert to webview KeyEvent format361this.handleKeyEventFromBrowserView(keyEvent);362}));363364this._inputDisposables.add(this._model.onDidNavigate((navEvent: IBrowserViewNavigationEvent) => {365this.group.pinEditor(this.input); // pin editor on navigation366367// Update navigation bar and context keys from model368this.updateNavigationState(navEvent);369}));370371this._inputDisposables.add(this._model.onDidChangeLoadingState(() => {372this.updateErrorDisplay();373}));374375this._inputDisposables.add(this._model.onDidChangeFocus(({ focused }) => {376// When the view gets focused, make sure the editor reports that it has focus,377// but focus is removed from the workbench.378if (focused) {379this._onDidFocus?.fire();380this.ensureBrowserFocus();381}382}));383384this._inputDisposables.add(this._model.onDidChangeDevToolsState(e => {385this._devToolsOpenContext.set(e.isDevToolsOpen);386}));387388this._inputDisposables.add(this._model.onDidRequestNewPage(({ resource, location, position }) => {389logBrowserOpen(this.telemetryService, (() => {390switch (location) {391case BrowserNewPageLocation.Background: return 'browserLinkBackground';392case BrowserNewPageLocation.Foreground: return 'browserLinkForeground';393case BrowserNewPageLocation.NewWindow: return 'browserLinkNewWindow';394}395})());396397const targetGroup = location === BrowserNewPageLocation.NewWindow ? AUX_WINDOW_GROUP : this.group;398this.editorService.openEditor({399resource: URI.from(resource),400options: {401pinned: true,402inactive: location === BrowserNewPageLocation.Background,403auxiliary: {404bounds: position,405compact: true406}407}408}, targetGroup);409}));410411this._inputDisposables.add(this.overlayManager!.onDidChangeOverlayState(() => {412this.checkOverlays();413}));414415// Listen for zoom level changes and update browser view zoom factor416this._inputDisposables.add(onDidChangeZoomLevel(targetWindowId => {417if (targetWindowId === this.window.vscodeWindowId) {418this.layoutBrowserContainer();419}420}));421422this.updateErrorDisplay();423this.layoutBrowserContainer();424this.updateVisibility();425this.doScreenshot();426}427428protected override setEditorVisible(visible: boolean): void {429this._editorVisible = visible;430this.updateVisibility();431}432433/**434* Make the browser container the active element without moving focus from the browser view.435*/436private ensureBrowserFocus(): void {437originalHtmlElementFocus.call(this._browserContainer);438}439440private updateVisibility(): void {441const hasUrl = !!this._model?.url;442const hasError = !!this._model?.error;443const isViewingPage = !hasError && hasUrl;444const isPaused = isViewingPage && this._editorVisible && this._overlayVisible;445446// Welcome container: shown when no URL is loaded447this._welcomeContainer.style.display = hasUrl ? 'none' : '';448449// Error container: shown when there's a load error450this._errorContainer.style.display = hasError ? '' : 'none';451452// Placeholder screenshot: shown when there is a page loaded (even when the view is not hidden, so hiding is smooth)453this._placeholderScreenshot.style.display = isViewingPage ? '' : 'none';454455// Pause overlay: fades in when an overlay is detected456this._overlayPauseContainer.classList.toggle('visible', isPaused);457458if (this._model) {459const show = this.shouldShowView;460if (show === this._model.visible) {461return;462}463464if (show) {465this._model.setVisible(true);466if (467this._browserContainer.ownerDocument.hasFocus() &&468this._browserContainer.ownerDocument.activeElement === this._browserContainer469) {470// If the editor is focused, ensure the browser view also gets focus471void this._model.focus();472}473} else {474this.doScreenshot();475476// Hide the browser view just before the next render.477// This attempts to give the screenshot some time to be captured and displayed.478// If we hide immediately it is more likely to flicker while the old screenshot is still visible.479this.window.requestAnimationFrame(() => this._model?.setVisible(false));480}481}482}483484private get shouldShowView(): boolean {485return this._editorVisible && !this._overlayVisible && !this._model?.error && !!this._model?.url;486}487488private checkOverlays(): void {489if (!this.overlayManager) {490return;491}492const overlappingOverlays = this.overlayManager.getOverlappingOverlays(this._browserContainer);493const hasOverlappingOverlay = overlappingOverlays.length > 0;494this.updateOverlayPauseMessage(overlappingOverlays);495if (hasOverlappingOverlay !== this._overlayVisible) {496this._overlayVisible = hasOverlappingOverlay;497this.updateVisibility();498}499}500501private updateOverlayPauseMessage(overlappingOverlays: readonly IBrowserOverlayInfo[]): void {502// Only show the pause message for notification overlays503const hasNotificationOverlay = overlappingOverlays.some(overlay => overlay.type === BrowserOverlayType.Notification);504this._overlayPauseContainer.classList.toggle('show-message', hasNotificationOverlay);505506if (hasNotificationOverlay) {507this._overlayPauseHeading.textContent = localize('browser.overlayPauseHeading.notification', "Paused due to Notification");508this._overlayPauseDetail.textContent = localize('browser.overlayPauseDetail.notification', "Dismiss the notification to continue using the browser.");509} else {510this._overlayPauseHeading.textContent = '';511this._overlayPauseDetail.textContent = '';512}513}514515private updateErrorDisplay(): void {516if (!this._model) {517return;518}519520const error: IBrowserViewLoadError | undefined = this._model.error;521if (error) {522// Update error content523524while (this._errorContainer.firstChild) {525this._errorContainer.removeChild(this._errorContainer.firstChild);526}527528const errorContent = $('.browser-error-content');529const errorTitle = $('.browser-error-title');530errorTitle.textContent = localize('browser.loadErrorLabel', "Failed to Load Page");531532const errorMessage = $('.browser-error-detail');533const errorText = $('span');534errorText.textContent = `${error.errorDescription} (${error.errorCode})`;535errorMessage.appendChild(errorText);536537const errorUrl = $('.browser-error-detail');538const urlLabel = $('strong');539urlLabel.textContent = localize('browser.errorUrlLabel', "URL:");540const urlValue = $('code');541urlValue.textContent = error.url;542errorUrl.appendChild(urlLabel);543errorUrl.appendChild(document.createTextNode(' '));544errorUrl.appendChild(urlValue);545546errorContent.appendChild(errorTitle);547errorContent.appendChild(errorMessage);548errorContent.appendChild(errorUrl);549this._errorContainer.appendChild(errorContent);550551this.setBackgroundImage(undefined);552} else {553this.setBackgroundImage(this._model.screenshot);554}555556this.updateVisibility();557}558559getUrl(): string | undefined {560return this._model?.url;561}562563async navigateToUrl(url: string): Promise<void> {564if (this._model) {565this.group.pinEditor(this.input); // pin editor on navigation566567// Special case localhost URLs (e.g., "localhost:3000") to add http://568if (/^localhost(:|\/|$)/i.test(url)) {569url = 'http://' + url;570} else if (!URL.parse(url)?.protocol) {571// If no scheme provided, default to http (sites will generally upgrade to https)572url = 'http://' + url;573}574575this.ensureBrowserFocus();576await this._model.loadURL(url);577}578}579580focusUrlInput(): void {581this._navigationBar.focusUrlInput();582}583584async goBack(): Promise<void> {585return this._model?.goBack();586}587588async goForward(): Promise<void> {589return this._model?.goForward();590}591592async reload(): Promise<void> {593return this._model?.reload();594}595596async toggleDevTools(): Promise<void> {597return this._model?.toggleDevTools();598}599600async clearStorage(): Promise<void> {601return this._model?.clearStorage();602}603604/**605* Show the find widget, optionally pre-populated with selected text from the browser view606*/607async showFind(): Promise<void> {608// Get selected text from the browser view to pre-populate the search box.609const selectedText = await this._model?.getSelectedText();610611// Only use the selected text if it doesn't contain newlines (single line selection)612const textToReveal = selectedText && !/[\r\n]/.test(selectedText) ? selectedText : undefined;613this._findWidget.value.reveal(textToReveal);614this._findWidget.value.layout(this._findWidgetContainer.clientWidth);615}616617/**618* Hide the find widget619*/620hideFind(): void {621this._findWidget.rawValue?.hide();622}623624/**625* Find the next match626*/627findNext(): void {628this._findWidget.rawValue?.find(false);629}630631/**632* Find the previous match633*/634findPrevious(): void {635this._findWidget.rawValue?.find(true);636}637638/**639* Start element selection in the browser view, wait for a user selection, and add it to chat.640*/641async addElementToChat(): Promise<void> {642// If selection is already active, cancel it643if (this._elementSelectionCts) {644this._elementSelectionCts.dispose(true);645this._elementSelectionCts = undefined;646this._elementSelectionActiveContext.set(false);647return;648}649650// Start new selection651const cts = new CancellationTokenSource();652this._elementSelectionCts = cts;653this._elementSelectionActiveContext.set(true);654655type IntegratedBrowserAddElementToChatStartEvent = {};656657type IntegratedBrowserAddElementToChatStartClassification = {658owner: 'jruales';659comment: 'The user initiated an Add Element to Chat action in Integrated Browser.';660};661662this.telemetryService.publicLog2<IntegratedBrowserAddElementToChatStartEvent, IntegratedBrowserAddElementToChatStartClassification>('integratedBrowser.addElementToChat.start', {});663664try {665// Get the resource URI for this editor666const resourceUri = this.input?.resource;667if (!resourceUri) {668throw new Error('No resource URI found');669}670671// Make the browser the focused view672this.ensureBrowserFocus();673674// Create a locator - for integrated browser, use the URI scheme to identify675// Browser view URIs have a special scheme we can match against676const locator: IBrowserTargetLocator = { browserViewId: BrowserViewUri.getId(this.input.resource) };677678// Start debug session for integrated browser679await this.browserElementsService.startDebugSession(cts.token, locator);680681// Get the browser container bounds682const { width, height } = this._browserContainer.getBoundingClientRect();683684// Get element data from user selection685const elementData = await this.browserElementsService.getElementData({ x: 0, y: 0, width, height }, cts.token, locator);686if (!elementData) {687throw new Error('Element data not found');688}689690const bounds = elementData.bounds;691const toAttach: IChatRequestVariableEntry[] = [];692693// Prepare HTML/CSS context694const displayName = getDisplayNameFromOuterHTML(elementData.outerHTML);695const attachCss = this.configurationService.getValue<boolean>('chat.sendElementsToChat.attachCSS');696let value = (attachCss ? 'Attached HTML and CSS Context' : 'Attached HTML Context') + '\n\n' + elementData.outerHTML;697if (attachCss) {698value += '\n\n' + elementData.computedStyle;699}700701toAttach.push({702id: 'element-' + Date.now(),703name: displayName,704fullName: displayName,705value: value,706kind: 'element',707icon: ThemeIcon.fromId(Codicon.layout.id),708});709710// Attach screenshot if enabled711const attachImages = this.configurationService.getValue<boolean>('chat.sendElementsToChat.attachImages');712if (attachImages && this._model) {713const screenshotBuffer = await this._model.captureScreenshot({714quality: 90,715rect: bounds716});717718toAttach.push({719id: 'element-screenshot-' + Date.now(),720name: 'Element Screenshot',721fullName: 'Element Screenshot',722kind: 'image',723value: screenshotBuffer.buffer724});725}726727// Attach to chat widget728const widget = await this.chatWidgetService.revealWidget() ?? this.chatWidgetService.lastFocusedWidget;729widget?.attachmentModel?.addContext(...toAttach);730731type IntegratedBrowserAddElementToChatAddedEvent = {732attachCss: boolean;733attachImages: boolean;734};735736type IntegratedBrowserAddElementToChatAddedClassification = {737attachCss: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether chat.sendElementsToChat.attachCSS was enabled.' };738attachImages: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether chat.sendElementsToChat.attachImages was enabled.' };739owner: 'jruales';740comment: 'An element was successfully added to chat from Integrated Browser.';741};742743this.telemetryService.publicLog2<IntegratedBrowserAddElementToChatAddedEvent, IntegratedBrowserAddElementToChatAddedClassification>('integratedBrowser.addElementToChat.added', {744attachCss,745attachImages746});747748} catch (error) {749if (!cts.token.isCancellationRequested) {750this.logService.error('BrowserEditor.addElementToChat: Failed to select element', error);751}752} finally {753cts.dispose();754if (this._elementSelectionCts === cts) {755this._elementSelectionCts = undefined;756this._elementSelectionActiveContext.set(false);757}758}759}760761/**762* Update navigation state and context keys763*/764private updateNavigationState(event: IBrowserViewNavigationEvent): void {765// Update navigation bar UI766this._navigationBar.updateFromNavigationEvent(event);767768// Update context keys for command enablement769this._canGoBackContext.set(event.canGoBack);770this._canGoForwardContext.set(event.canGoForward);771this._hasUrlContext.set(!!event.url);772773// Update visibility (welcome screen, error, browser view)774this.updateVisibility();775}776777/**778* Create the welcome container shown when no URL is loaded779*/780private createWelcomeContainer(): HTMLElement {781const container = $('.browser-welcome-container');782const content = $('.browser-welcome-content');783784const iconContainer = $('.browser-welcome-icon');785iconContainer.appendChild(renderIcon(Codicon.globe));786content.appendChild(iconContainer);787788const title = $('.browser-welcome-title');789title.textContent = localize('browser.welcomeTitle', "Browser");790content.appendChild(title);791792const subtitle = $('.browser-welcome-subtitle');793const chatEnabled = this.contextKeyService.getContextKeyValue<boolean>(ChatContextKeys.enabled.key);794subtitle.textContent = chatEnabled795? localize('browser.welcomeSubtitleChat', "Use Add Element to Chat to reference UI elements in chat prompts.")796: localize('browser.welcomeSubtitle', "Enter a URL above to get started.");797content.appendChild(subtitle);798799container.appendChild(content);800return container;801}802803private setBackgroundImage(buffer: VSBuffer | undefined): void {804if (buffer) {805const dataUrl = `data:image/jpeg;base64,${encodeBase64(buffer)}`;806this._placeholderScreenshot.style.backgroundImage = `url('${dataUrl}')`;807} else {808this._placeholderScreenshot.style.backgroundImage = '';809}810}811812private async doScreenshot(): Promise<void> {813if (!this._model) {814return;815}816817// Cancel any existing timeout818this.cancelScheduledScreenshot();819820// Only take screenshots if the model is visible821if (!this._model.visible) {822return;823}824825try {826// Capture screenshot and set as background image827const screenshot = await this._model.captureScreenshot({ quality: 80 });828this.setBackgroundImage(screenshot);829} catch (error) {830this.logService.error('Failed to capture browser view screenshot', error);831}832833// Schedule next screenshot in 1 second834this._screenshotTimeout = setTimeout(() => this.doScreenshot(), 1000);835}836837private cancelScheduledScreenshot(): void {838if (this._screenshotTimeout) {839clearTimeout(this._screenshotTimeout);840this._screenshotTimeout = undefined;841}842}843844forwardCurrentEvent(): boolean {845if (this._currentKeyDownEvent && this._model) {846void this._model.dispatchKeyEvent(this._currentKeyDownEvent);847return true;848}849return false;850}851852private async handleKeyEventFromBrowserView(keyEvent: IBrowserViewKeyDownEvent): Promise<void> {853this._currentKeyDownEvent = keyEvent;854855try {856const syntheticEvent = new KeyboardEvent('keydown', keyEvent);857const standardEvent = new StandardKeyboardEvent(syntheticEvent);858859const handled = this.keybindingService.dispatchEvent(standardEvent, this._browserContainer);860if (!handled) {861this.forwardCurrentEvent();862}863} catch (error) {864this.logService.error('BrowserEditor.handleKeyEventFromBrowserView: Error dispatching key event', error);865} finally {866this._currentKeyDownEvent = undefined;867}868}869870override layout(dimension: Dimension, _position?: IDomPosition): void {871// Layout find widget if it exists872this._findWidget.rawValue?.layout(dimension.width);873}874875/**876* This should be called whenever .browser-container changes in size, or when877* there could be any elements, such as the command palette, overlapping with it.878*879* Note that we don't call layoutBrowserContainer() from layout() but instead rely on using a ResizeObserver and on880* making direct calls to it. This is because we have seen cases where the getBoundingClientRect() values of881* the .browser-container element are not correct during layout() calls, especially during "Move into New Window"882* and "Copy into New Window" operations into a different monitor.883*/884layoutBrowserContainer(): void {885if (this._model) {886this.checkOverlays();887888const containerRect = this._browserContainer.getBoundingClientRect();889void this._model.layout({890windowId: this.group.windowId,891x: containerRect.left,892y: containerRect.top,893width: containerRect.width,894height: containerRect.height,895zoomFactor: getZoomFactor(this.window)896});897}898}899900override clearInput(): void {901this._inputDisposables.clear();902903// Cancel any active element selection904if (this._elementSelectionCts) {905this._elementSelectionCts.dispose(true);906this._elementSelectionCts = undefined;907}908909// Cancel any scheduled screenshots910this.cancelScheduledScreenshot();911912// Clear find widget model913this._findWidget.rawValue?.setModel(undefined);914this._findWidget.rawValue?.hide();915916void this._model?.setVisible(false);917this._model = undefined;918919this._canGoBackContext.reset();920this._canGoForwardContext.reset();921this._hasUrlContext.reset();922this._storageScopeContext.reset();923this._devToolsOpenContext.reset();924this._elementSelectionActiveContext.reset();925926this._navigationBar.clear();927this.setBackgroundImage(undefined);928929super.clearInput();930}931}932933934