Skip to content

Commit f24ef26

Browse files
kycutlerjruales
andauthored
"Add Element to Chat" in integrated browser (#286649)
* "Add element to chat" in integrated browser * Support disabling AI features, PR feedback * PR feedback --------- Co-authored-by: Joaquín Ruales <1588988+jruales@users.noreply.github.com>
1 parent 008621a commit f24ef26

File tree

10 files changed

+295
-131
lines changed

10 files changed

+295
-131
lines changed

src/vs/platform/browserElements/common/browserElements.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,52 @@ export interface IElementData {
1515
readonly bounds: IRectangle;
1616
}
1717

18-
export enum BrowserType {
19-
SimpleBrowser = 'simpleBrowser',
20-
LiveServer = 'liveServer',
18+
/**
19+
* Locator for identifying a browser target/webview.
20+
* Uses either the parent webview or browser view id to uniquely identify the target.
21+
*/
22+
export interface IBrowserTargetLocator {
23+
/**
24+
* Identifier of the parent webview hosting the target.
25+
*
26+
* Exactly one of {@link webviewId} or {@link browserViewId} should be provided.
27+
* Use this when the target is rendered inside a webview.
28+
*/
29+
readonly webviewId?: string;
30+
/**
31+
* Identifier of the browser view hosting the target.
32+
*
33+
* Exactly one of {@link webviewId} or {@link browserViewId} should be provided.
34+
* Use this when the target is rendered inside a browser view rather than a webview.
35+
*/
36+
readonly browserViewId?: string;
2137
}
2238

23-
2439
export interface INativeBrowserElementsService {
2540

2641
readonly _serviceBrand: undefined;
2742

2843
// Properties
2944
readonly windowId: number;
3045

31-
getElementData(rect: IRectangle, token: CancellationToken, browserType: BrowserType, cancellationId?: number): Promise<IElementData | undefined>;
46+
getElementData(rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator, cancellationId?: number): Promise<IElementData | undefined>;
47+
48+
startDebugSession(token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise<void>;
49+
}
50+
51+
/**
52+
* Extract a display name from outer HTML (e.g., "div#myId.myClass1.myClass2")
53+
*/
54+
export function getDisplayNameFromOuterHTML(outerHTML: string): string {
55+
const firstElementMatch = outerHTML.match(/^<([^ >]+)([^>]*?)>/);
56+
if (!firstElementMatch) {
57+
throw new Error('No outer element found');
58+
}
3259

33-
startDebugSession(token: CancellationToken, browserType: BrowserType, cancelAndDetachId?: number): Promise<void>;
60+
const tagName = firstElementMatch[1];
61+
const idMatch = firstElementMatch[2].match(/\s+id\s*=\s*["']([^"']+)["']/i);
62+
const id = idMatch ? `#${idMatch[1]}` : '';
63+
const classMatch = firstElementMatch[2].match(/\s+class\s*=\s*["']([^"']+)["']/i);
64+
const className = classMatch ? `.${classMatch[1].replace(/\s+/g, '.')}` : '';
65+
return `${tagName}${id}${className}`;
3466
}

src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { BrowserType, IElementData, INativeBrowserElementsService } from '../common/browserElements.js';
6+
import { IElementData, INativeBrowserElementsService, IBrowserTargetLocator } from '../common/browserElements.js';
77
import { CancellationToken } from '../../../base/common/cancellation.js';
88
import { IRectangle } from '../../window/common/window.js';
99
import { BrowserWindow, webContents } from 'electron';
@@ -14,6 +14,7 @@ import { IWindowsMainService } from '../../windows/electron-main/windows.js';
1414
import { createDecorator } from '../../instantiation/common/instantiation.js';
1515
import { Disposable } from '../../../base/common/lifecycle.js';
1616
import { AddFirstParameterToFunctions } from '../../../base/common/types.js';
17+
import { IBrowserViewMainService } from '../../browserView/electron-main/browserViewMainService.js';
1718

1819
export const INativeBrowserElementsMainService = createDecorator<INativeBrowserElementsMainService>('browserElementsMainService');
1920
export interface INativeBrowserElementsMainService extends AddFirstParameterToFunctions<INativeBrowserElementsService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
@@ -27,89 +28,78 @@ interface NodeDataResponse {
2728
export class NativeBrowserElementsMainService extends Disposable implements INativeBrowserElementsMainService {
2829
_serviceBrand: undefined;
2930

30-
currentLocalAddress: string | undefined;
31-
3231
constructor(
3332
@IWindowsMainService private readonly windowsMainService: IWindowsMainService,
3433
@IAuxiliaryWindowsMainService private readonly auxiliaryWindowsMainService: IAuxiliaryWindowsMainService,
35-
34+
@IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService
3635
) {
3736
super();
3837
}
3938

4039
get windowId(): never { throw new Error('Not implemented in electron-main'); }
4140

42-
async findWebviewTarget(debuggers: Electron.Debugger, windowId: number, browserType: BrowserType): Promise<string | undefined> {
41+
/**
42+
* Find the webview target that matches the given locator.
43+
* Checks either webviewId or browserViewId depending on what's provided.
44+
*/
45+
async findWebviewTarget(debuggers: Electron.Debugger, locator: IBrowserTargetLocator): Promise<string | undefined> {
4346
const { targetInfos } = await debuggers.sendCommand('Target.getTargets');
44-
let target: typeof targetInfos[number] | undefined = undefined;
45-
const matchingTarget = targetInfos.find((targetInfo: { url: string }) => {
46-
try {
47-
const url = new URL(targetInfo.url);
48-
if (browserType === BrowserType.LiveServer) {
49-
return url.searchParams.get('id') && url.searchParams.get('extensionId') === 'ms-vscode.live-server';
50-
} else if (browserType === BrowserType.SimpleBrowser) {
51-
return url.searchParams.get('parentId') === windowId.toString() && url.searchParams.get('extensionId') === 'vscode.simple-browser';
47+
48+
if (locator.webviewId) {
49+
let extensionId = '';
50+
for (const targetInfo of targetInfos) {
51+
try {
52+
const url = new URL(targetInfo.url);
53+
if (url.searchParams.get('id') === locator.webviewId) {
54+
extensionId = url.searchParams.get('extensionId') || '';
55+
break;
56+
}
57+
} catch (err) {
58+
// ignore
5259
}
53-
return false;
54-
} catch (err) {
55-
return false;
5660
}
57-
});
58-
59-
// search for webview via search parameters
60-
if (matchingTarget) {
61-
let resultId: string | undefined;
62-
let url: URL | undefined;
63-
try {
64-
url = new URL(matchingTarget.url);
65-
resultId = url.searchParams.get('id')!;
66-
} catch (e) {
61+
if (!extensionId) {
6762
return undefined;
6863
}
6964

70-
target = targetInfos.find((targetInfo: { url: string }) => {
65+
// search for webview via search parameters
66+
const target = targetInfos.find((targetInfo: { url: string }) => {
7167
try {
7268
const url = new URL(targetInfo.url);
73-
const isLiveServer = browserType === BrowserType.LiveServer && url.searchParams.get('serverWindowId') === resultId;
74-
const isSimpleBrowser = browserType === BrowserType.SimpleBrowser && url.searchParams.get('id') === resultId && url.searchParams.has('vscodeBrowserReqId');
69+
const isLiveServer = extensionId === 'ms-vscode.live-server' && url.searchParams.get('serverWindowId') === locator.webviewId;
70+
const isSimpleBrowser = extensionId === 'vscode.simple-browser' && url.searchParams.get('id') === locator.webviewId && url.searchParams.has('vscodeBrowserReqId');
7571
if (isLiveServer || isSimpleBrowser) {
76-
this.currentLocalAddress = url.origin;
7772
return true;
7873
}
7974
return false;
8075
} catch (e) {
8176
return false;
8277
}
8378
});
84-
85-
if (target) {
86-
return target.targetId;
87-
}
79+
return target?.targetId;
8880
}
8981

90-
// fallback: search for webview without parameters based on current origin
91-
target = targetInfos.find((targetInfo: { url: string }) => {
92-
try {
93-
const url = new URL(targetInfo.url);
94-
return (this.currentLocalAddress === url.origin);
95-
} catch (e) {
96-
return false;
97-
}
98-
});
82+
if (locator.browserViewId) {
83+
const webContentsInstance = this.browserViewMainService.tryGetBrowserView(locator.browserViewId)?.webContents;
84+
const target = targetInfos.find((targetInfo: { targetId: string; type: string }) => {
85+
if (targetInfo.type !== 'page') {
86+
return false;
87+
}
9988

100-
if (!target) {
101-
return undefined;
89+
return webContents.fromDevToolsTargetId(targetInfo.targetId) === webContentsInstance;
90+
});
91+
return target?.targetId;
10292
}
10393

104-
return target.targetId;
94+
return undefined;
10595
}
10696

107-
async waitForWebviewTargets(debuggers: Electron.Debugger, windowId: number, browserType: BrowserType): Promise<string | undefined> {
97+
async waitForWebviewTargets(debuggers: Electron.Debugger, locator: IBrowserTargetLocator): Promise<string | undefined> {
10898
const start = Date.now();
10999
const timeout = 10000;
110100

111101
while (Date.now() - start < timeout) {
112-
const targetId = await this.findWebviewTarget(debuggers, windowId, browserType);
102+
const targetId = await this.findWebviewTarget(debuggers, locator);
113103
if (targetId) {
114104
return targetId;
115105
}
@@ -122,7 +112,7 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
122112
return undefined;
123113
}
124114

125-
async startDebugSession(windowId: number | undefined, token: CancellationToken, browserType: BrowserType, cancelAndDetachId?: number): Promise<void> {
115+
async startDebugSession(windowId: number | undefined, token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise<void> {
126116
const window = this.windowById(windowId);
127117
if (!window?.win) {
128118
return undefined;
@@ -142,7 +132,7 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
142132
}
143133

144134
try {
145-
const matchingTargetId = await this.waitForWebviewTargets(debuggers, windowId!, browserType);
135+
const matchingTargetId = await this.waitForWebviewTargets(debuggers, locator);
146136
if (!matchingTargetId) {
147137
if (debuggers.isAttached()) {
148138
debuggers.detach();
@@ -187,7 +177,7 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
187177
}
188178
}
189179

190-
async getElementData(windowId: number | undefined, rect: IRectangle, token: CancellationToken, browserType: BrowserType, cancellationId?: number): Promise<IElementData | undefined> {
180+
async getElementData(windowId: number | undefined, rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator, cancellationId?: number): Promise<IElementData | undefined> {
191181
const window = this.windowById(windowId);
192182
if (!window?.win) {
193183
return undefined;
@@ -208,7 +198,7 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
208198

209199
let targetSessionId: string | undefined = undefined;
210200
try {
211-
const targetId = await this.findWebviewTarget(debuggers, windowId!, browserType);
201+
const targetId = await this.findWebviewTarget(debuggers, locator);
212202
const { sessionId } = await debuggers.sendCommand('Target.attachToTarget', {
213203
targetId: targetId,
214204
flatten: true,
@@ -373,7 +363,7 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
373363
const content = model.content;
374364
const margin = model.margin;
375365
const x = Math.min(margin[0], content[0]);
376-
const y = Math.min(margin[1], content[1]) + 32.4; // 32.4 is height of the title bar
366+
const y = Math.min(margin[1], content[1]);
377367
const width = Math.max(margin[2] - margin[0], content[2] - content[0]);
378368
const height = Math.max(margin[5] - margin[1], content[5] - content[1]);
379369

@@ -416,7 +406,7 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
416406
});
417407
}
418408

419-
formatMatchedStyles(matched: { inlineStyle?: { cssProperties?: Array<{ name: string; value: string }> }; matchedCSSRules?: Array<{ rule: { selectorList: { selectors: Array<{ text: string }> }; origin: string; style: { cssProperties: Array<{ name: string; value: string }> } } }>; inherited?: Array<{ matchedCSSRules?: Array<{ rule: { selectorList: { selectors: Array<{ text: string }> }; origin: string; style: { cssProperties: Array<{ name: string; value: string }> } } }> }> }): string {
409+
formatMatchedStyles(matched: { inlineStyle?: { cssProperties?: Array<{ name: string; value: string }> }; matchedCSSRules?: Array<{ rule: { selectorList: { selectors: Array<{ text: string }> }; origin: string; style: { cssProperties: Array<{ name: string; value: string }> } } }>; inherited?: Array<{ inlineStyle?: { cssText: string }; matchedCSSRules?: Array<{ rule: { selectorList: { selectors: Array<{ text: string }> }; origin: string; style: { cssProperties: Array<{ name: string; value: string }> } } }> }> }): string {
420410
const lines: string[] = [];
421411

422412
// inline
@@ -451,6 +441,14 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
451441
if (matched.inherited?.length) {
452442
let level = 1;
453443
for (const inherited of matched.inherited) {
444+
const inline = inherited.inlineStyle;
445+
if (inline) {
446+
lines.push(`/* Inherited from ancestor level ${level} (inline) */`);
447+
lines.push('element {');
448+
lines.push(inline.cssText);
449+
lines.push('}\n');
450+
}
451+
454452
const rules = inherited.matchedCSSRules || [];
455453
for (const ruleEntry of rules) {
456454
const rule = ruleEntry.rule;

src/vs/platform/browserView/electron-main/browserView.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ export class BrowserView extends Disposable {
271271
});
272272
}
273273

274+
get webContents(): Electron.WebContents {
275+
return this._view.webContents;
276+
}
277+
274278
/**
275279
* Get the current state of this browser view
276280
*/

src/vs/platform/browserView/electron-main/browserViewMainService.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { generateUuid } from '../../../base/common/uuid.js';
1616
export const IBrowserViewMainService = createDecorator<IBrowserViewMainService>('browserViewMainService');
1717

1818
export interface IBrowserViewMainService extends IBrowserViewService {
19-
// Additional electron-specific methods can be added here if needed in the future
19+
tryGetBrowserView(id: string): BrowserView | undefined;
2020
}
2121

2222
// Same as webviews
@@ -96,6 +96,10 @@ export class BrowserViewMainService extends Disposable implements IBrowserViewMa
9696
return view.getState();
9797
}
9898

99+
tryGetBrowserView(id: string): BrowserView | undefined {
100+
return this.browserViews.get(id);
101+
}
102+
99103
/**
100104
* Get a browser view or throw if not found
101105
*/

0 commit comments

Comments
 (0)