Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/vs/platform/browserElements/common/browserElements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ export interface INativeBrowserElementsService {
getElementData(rect: IRectangle, token: CancellationToken, browserType: BrowserType, cancellationId?: number): Promise<IElementData | undefined>;

startDebugSession(token: CancellationToken, browserType: BrowserType, cancelAndDetachId?: number): Promise<void>;

getConsoleLogs(): Promise<string | undefined>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ interface NodeDataResponse {
bounds: IRectangle;
}

interface LogEntry {
type: 'console' | 'exception' | 'log';
level?: string;
message?: string;
args?: any[];
exceptionDetails?: any;
timestamp: number;
rawData: string;
}

const allConsole = new Map<number, LogEntry[]>();

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

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

The global module-level Map (allConsole) storing console logs has no cleanup mechanism. As users open/close windows, this Map will continue to grow indefinitely, leading to a memory leak. Consider adding cleanup logic when windows are closed (e.g., in a window close event handler) to remove entries from the Map.

Copilot uses AI. Check for mistakes.

export class NativeBrowserElementsMainService extends Disposable implements INativeBrowserElementsMainService {
_serviceBrand: undefined;

Expand Down Expand Up @@ -141,6 +153,75 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
debuggers.attach();
}

let sessionId: string | undefined;
const onMessage = (event: any, method: string, params: any, sessionIdFromMessage?: string) => {

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

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

Using any type for debuggers parameter lacks type safety. Consider defining a proper interface or type for the debugger object to improve code maintainability and catch potential errors at compile time.

Suggested change
const onMessage = (event: any, method: string, params: any, sessionIdFromMessage?: string) => {
/**
* Types for debugger protocol messages
*/
interface ConsoleAPICalledParams {
args?: Array<{
type: string;
value?: unknown;
preview?: {
description?: string;
};
}>;
type: string;
}
interface ExceptionThrownParams {
exceptionDetails?: {
text?: string;
exception?: {
description?: string;
};
};
}
interface LogEntryAddedParams {
level: string;
text: string;
}
type DebuggerParams = ConsoleAPICalledParams | ExceptionThrownParams | LogEntryAddedParams;
const onMessage = (event: unknown, method: string, params: DebuggerParams, sessionIdFromMessage?: string) => {

Copilot uses AI. Check for mistakes.
if (sessionIdFromMessage === sessionId && (method === 'Runtime.consoleAPICalled' || method === 'Runtime.exceptionThrown' || method === 'Log.entryAdded')) {
const current = allConsole.get(windowId!) ?? [];

let logEntry: LogEntry;

if (method === 'Runtime.consoleAPICalled') {
// Extract console message from args
const args = params.args?.map((arg: any) => {
if (arg.type === 'string') {
return arg.value;
}
if (arg.type === 'object') {
// For objects, try to extract a readable representation
if (arg.preview?.description) {
return arg.preview.description;
}
return JSON.stringify(arg, null, 2);
}
if (arg.value !== undefined) {
return arg.value;
}
return JSON.stringify(arg, null, 2);
}) ?? [];
Comment on lines +165 to +180

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

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

Multiple any types used (arg: any, frame: any) reduce type safety. Consider defining proper interfaces for the Chrome DevTools Protocol message structure (e.g., RemoteObject, CallFrame) to improve code maintainability.

Copilot uses AI. Check for mistakes.

logEntry = {
type: 'console',
level: params.type, // 'log', 'debug', 'info', 'error', 'warning', 'dir', 'dirxml', 'table', 'trace', 'clear', 'startGroup', 'startGroupCollapsed', 'endGroup'
message: args.join(' '),
args: params.args,
timestamp: Date.now(),
rawData: JSON.stringify(params)
};
} else if (method === 'Runtime.exceptionThrown') {
const exceptionDetails = params.exceptionDetails;
let errorMessage = exceptionDetails?.text || 'Unknown error';
if (exceptionDetails?.exception?.description) {
errorMessage = exceptionDetails.exception.description;
}

logEntry = {
type: 'exception',
level: 'error',
message: errorMessage,
exceptionDetails: params.exceptionDetails,
timestamp: Date.now(),
rawData: JSON.stringify(params)
};
} else { // Log.entryAdded
logEntry = {
type: 'log',
level: params.level, // 'verbose', 'info', 'warning', 'error'
message: params.text,
timestamp: Date.now(),
rawData: JSON.stringify(params)
};
}

if (!current.some(entry => entry.rawData === logEntry.rawData)) {
current.push(logEntry);
if (current.length > 5) {
current.shift();
}
allConsole.set(windowId!, current);
}
}
};

try {
Comment on lines +157 to 225

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

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

The onMessage event handler captures windowId from the outer scope, but windowId could potentially be undefined (as the parameter type indicates). While it's checked at line 140, using the non-null assertion (windowId!) at lines 160, 226 could fail if the check at 140 doesn't return. Consider checking windowId is defined before setting up the message handler or using a local constant to store the validated windowId.

Copilot uses AI. Check for mistakes.
const matchingTargetId = await this.waitForWebviewTargets(debuggers, windowId!, browserType);
if (!matchingTargetId) {
Expand All @@ -150,6 +231,20 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
throw new Error('No target found');
}

// starts console logging as well.
const attachResult = await debuggers.sendCommand('Target.attachToTarget', {
targetId: matchingTargetId,
flatten: true,
});
sessionId = attachResult.sessionId;

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

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

This comment does not accurately describe what's happening. The code already attached to the target and enabled console logging above (lines 241-250). This section is for handling detachment on IPC messages, not "starts console logging".

Copilot uses AI. Check for mistakes.

allConsole.set(windowId!, []);

await debuggers.sendCommand('Debugger.enable', {}, sessionId);
await debuggers.sendCommand('Runtime.enable', {}, sessionId);
await debuggers.sendCommand('Log.enable', {}, sessionId);
debuggers.on('message', onMessage);

} catch (e) {
if (debuggers.isAttached()) {
debuggers.detach();
Expand All @@ -170,6 +265,53 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
}
}
});

window.win.webContents.on('ipc-message', async (event, channel, closedCancelAndDetachId) => {
if (channel === `vscode:changeElementSelection${cancelAndDetachId}`) {
if (cancelAndDetachId !== closedCancelAndDetachId) {
return;
}
if (debuggers.isAttached()) {
debuggers.detach();
}
debuggers.off('message', onMessage);
if (window.win) {
window.win.webContents.removeAllListeners('ipc-message');
}
}
});
}

async getConsoleLogs(windowId: number | undefined): Promise<string | undefined> {
const window = this.windowById(windowId);
if (!window?.win) {
Comment on lines +274 to +287

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

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

Two separate event listeners are registered for 'ipc-message' events that handle different channels (vscode:cancelCurrentSession${cancelAndDetachId} and vscode:changeElementSelection${cancelAndDetachId}). Both listeners call removeAllListeners('ipc-message'), which will remove both listeners even if only one should be removed. Consider using a single listener that handles both channels, or use named listener functions that can be removed individually.

Copilot uses AI. Check for mistakes.
return undefined;
}

const logs = allConsole.get(window.id) ?? [];
if (logs.length === 0) {
return undefined;
}

const formatted = logs.map(log => {
let line = '';
if (log.type === 'console') {
line = `[${log.level?.toUpperCase()}] ${log.message}`;
} else if (log.type === 'exception') {
line = `[ERROR] Exception: ${log.message}`;
if (log.exceptionDetails?.stackTrace?.callFrames?.length) {
line += '\nStack Trace:';
log.exceptionDetails.stackTrace.callFrames.forEach((frame: any) => {
line += `\n at ${frame.functionName} (${frame.url}:${frame.lineNumber}:${frame.columnNumber})`;
});
}
} else if (log.type === 'log') {
line = `[${log.level?.toUpperCase()}] ${log.message}`;
}
return line;
}).join('\n');

return formatted;
}

async finishOverlay(debuggers: any, sessionId: string | undefined): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ class SimpleBrowserOverlayWidget {
const cancelButtonLabel = localize('cancelSelectionLabel', 'Cancel');
cancelButton.label = cancelButtonLabel;

const attachLogs = this._showStore.add(new Button(mainContent, { ...defaultButtonStyles, supportIcons: true, title: localize('chat.attachLogs', "Attach Logs") }));
attachLogs.icon = Codicon.bug;

const configure = this._showStore.add(new Button(mainContent, { supportIcons: true, title: localize('chat.configureElements', "Configure Attachments Sent") }));
configure.icon = Codicon.gear;

Expand Down Expand Up @@ -228,6 +231,10 @@ class SimpleBrowserOverlayWidget {
this._showStore.add(addDisposableListener(configure.element, 'click', () => {
this._preferencesService.openSettings({ jsonEditor: false, query: '@id:chat.sendElementsToChat.enabled,chat.sendElementsToChat.attachCSS,chat.sendElementsToChat.attachImages' });
}));

this._showStore.add(addDisposableListener(attachLogs.element, 'click', async () => {
await this.addConsolesToChat();
}));
}

setActiveBrowserType(type: BrowserType | undefined) {
Expand All @@ -248,6 +255,23 @@ class SimpleBrowserOverlayWidget {
element.classList.remove('hidden');
}

async addConsolesToChat() {
const logs = await this._browserElementsService.getConsoleLogs();
const toAttach: IChatRequestVariableEntry[] = [];

toAttach.push({
id: 'element-' + Date.now(),
name: localize('consoleLogs', 'Console Logs'),
fullName: localize('consoleLogs', 'Console Logs'),
value: logs ?? localize('noConsoleLogs', 'No console logs captured.'),
kind: 'element',
icon: ThemeIcon.fromId(Codicon.bug.id),
});

const widget = await showChatView(this._viewService, this._layoutService) ?? this._chatWidgetService.lastFocusedWidget;
widget?.attachmentModel?.addContext(...toAttach);
}
Comment on lines +258 to +273

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

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

The addConsolesToChat method doesn't handle the case where logs could be undefined. If no logs are available, the method will attach a context entry with value: undefined, which may not be the desired behavior. Consider adding a check and either showing a message to the user or returning early if no logs are available.

Copilot uses AI. Check for mistakes.

async addElementToChat(cts: CancellationTokenSource) {
// eslint-disable-next-line no-restricted-syntax
const editorContainer = this._container.querySelector('.editor-container') as HTMLDivElement;
Expand Down Expand Up @@ -378,6 +402,7 @@ class SimpleBrowserOverlayController {
if (activeBrowserType) {
try {
await this._browserElementsService.startDebugSession(cts.token, activeBrowserType);

Copilot AI Nov 13, 2025

Copy link

Choose a reason for hiding this comment

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

There's an extra blank line after the startDebugSession call. Remove this blank line to maintain consistency with the codebase style.

Suggested change

Copilot uses AI. Check for mistakes.
} catch (error) {
connectingWebviewElement.textContent = localize('reopenErrorWebviewElement', 'Please reopen the preview.');
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
.element-selection-main-content .monaco-button.codicon.codicon-close,
.element-expand-container .monaco-button.codicon.codicon-layout,
.element-selection-main-content .monaco-button.codicon.codicon-chevron-right,
.element-selection-main-content .monaco-button.codicon.codicon-gear {
.element-selection-main-content .monaco-button.codicon.codicon-gear,
.element-selection-main-content .monaco-button.codicon.codicon-bug {
width: 17px;
height: 17px;
padding: 2px 2px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ export interface IBrowserElementsService {
getElementData(rect: IRectangle, token: CancellationToken, browserType: BrowserType | undefined): Promise<IElementData | undefined>;

startDebugSession(token: CancellationToken, browserType: BrowserType): Promise<void>;

getConsoleLogs(): Promise<string | undefined>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class WebBrowserElementsService implements IBrowserElementsService {

constructor() { }

async getConsoleLogs(): Promise<string | undefined> {
throw new Error('Not implemented');
}

async getElementData(rect: IRectangle, token: CancellationToken): Promise<IElementData | undefined> {
throw new Error('Not implemented');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ class WorkbenchBrowserElementsService implements IBrowserElementsService {
disposable.dispose();
}
}

async getConsoleLogs(): Promise<string | undefined> {
return this.simpleBrowser.getConsoleLogs();
}
}

registerSingleton(IBrowserElementsService, WorkbenchBrowserElementsService, InstantiationType.Delayed);
Expand Down
Loading