mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-13 23:44:09 +01:00
add console to chat from integrated browser (#295839)
* add console to chat from integrated browser * address some comments and change icon
This commit is contained in:
@@ -46,6 +46,10 @@ export interface INativeBrowserElementsService {
|
||||
getElementData(rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator, cancellationId?: number): Promise<IElementData | undefined>;
|
||||
|
||||
startDebugSession(token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise<void>;
|
||||
|
||||
startConsoleSession(token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise<void>;
|
||||
|
||||
getConsoleLogs(locator: IBrowserTargetLocator): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,17 @@ interface NodeDataResponse {
|
||||
bounds: IRectangle;
|
||||
}
|
||||
|
||||
const MAX_CONSOLE_LOG_ENTRIES = 1000;
|
||||
const consoleLogStore = new Map<string, string[]>();
|
||||
|
||||
function locatorKey(locator: IBrowserTargetLocator): string {
|
||||
const key = locator.browserViewId ?? locator.webviewId;
|
||||
if (!key) {
|
||||
return 'unknown';
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
export class NativeBrowserElementsMainService extends Disposable implements INativeBrowserElementsMainService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
@@ -38,11 +49,82 @@ export class NativeBrowserElementsMainService extends Disposable implements INat
|
||||
|
||||
get windowId(): never { throw new Error('Not implemented in electron-main'); }
|
||||
|
||||
async getConsoleLogs(windowId: number | undefined, locator: IBrowserTargetLocator): Promise<string | undefined> {
|
||||
const key = locatorKey(locator);
|
||||
const entries = consoleLogStore.get(key);
|
||||
if (!entries || entries.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return entries.join('\n');
|
||||
}
|
||||
|
||||
async startConsoleSession(windowId: number | undefined, token: CancellationToken, locator: IBrowserTargetLocator, cancelAndDetachId?: number): Promise<void> {
|
||||
const window = this.windowById(windowId);
|
||||
if (!window?.win) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let targetWebContents: Electron.WebContents | undefined;
|
||||
if (locator.browserViewId) {
|
||||
targetWebContents = this.browserViewMainService.tryGetBrowserView(locator.browserViewId)?.webContents;
|
||||
}
|
||||
|
||||
if (!targetWebContents) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const key = locatorKey(locator);
|
||||
if (!consoleLogStore.has(key)) {
|
||||
consoleLogStore.set(key, []);
|
||||
}
|
||||
|
||||
const levelMap: Record<number, string> = { 0: 'log', 1: 'warning', 2: 'error' };
|
||||
const onConsoleMessage = (_event: Electron.Event, level: number, message: string, _line: number, _sourceId: string) => {
|
||||
const levelName = levelMap[level] ?? 'log';
|
||||
const formatted = `[${levelName}] ${message}`;
|
||||
const current = consoleLogStore.get(key) ?? [];
|
||||
current.push(formatted);
|
||||
if (current.length > MAX_CONSOLE_LOG_ENTRIES) {
|
||||
current.splice(0, current.length - MAX_CONSOLE_LOG_ENTRIES);
|
||||
}
|
||||
consoleLogStore.set(key, current);
|
||||
};
|
||||
|
||||
const cleanupListeners = () => {
|
||||
targetWebContents?.off('console-message', onConsoleMessage);
|
||||
window.win?.webContents.off('ipc-message', onIpcMessage);
|
||||
};
|
||||
|
||||
const onIpcMessage = async (_event: Electron.Event, channel: string, closedCancelAndDetachId: number) => {
|
||||
if (channel === `vscode:cancelConsoleSession${cancelAndDetachId}`) {
|
||||
if (cancelAndDetachId !== closedCancelAndDetachId) {
|
||||
return;
|
||||
}
|
||||
cleanupListeners();
|
||||
consoleLogStore.delete(key);
|
||||
}
|
||||
};
|
||||
|
||||
targetWebContents.on('console-message', onConsoleMessage);
|
||||
|
||||
targetWebContents.once('destroyed', () => {
|
||||
cleanupListeners();
|
||||
consoleLogStore.delete(key);
|
||||
});
|
||||
|
||||
token.onCancellationRequested(() => {
|
||||
cleanupListeners();
|
||||
consoleLogStore.delete(key);
|
||||
});
|
||||
|
||||
window.win.webContents.on('ipc-message', onIpcMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the webview target that matches the given locator.
|
||||
* Checks either webviewId or browserViewId depending on what's provided.
|
||||
*/
|
||||
async findWebviewTarget(debuggers: Electron.Debugger, locator: IBrowserTargetLocator): Promise<string | undefined> {
|
||||
private async findWebviewTarget(debuggers: Electron.Debugger, locator: IBrowserTargetLocator): Promise<string | undefined> {
|
||||
const { targetInfos } = await debuggers.sendCommand('Target.getTargets');
|
||||
|
||||
if (locator.webviewId) {
|
||||
|
||||
@@ -195,6 +195,7 @@ export class BrowserEditor extends EditorPane {
|
||||
private readonly _inputDisposables = this._register(new DisposableStore());
|
||||
private overlayManager: BrowserOverlayManager | undefined;
|
||||
private _elementSelectionCts: CancellationTokenSource | undefined;
|
||||
private _consoleSessionCts: CancellationTokenSource | undefined;
|
||||
private _screenshotTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
constructor(
|
||||
@@ -367,6 +368,12 @@ export class BrowserEditor extends EditorPane {
|
||||
|
||||
// Update navigation bar and context keys from model
|
||||
this.updateNavigationState(navEvent);
|
||||
|
||||
if (navEvent.url) {
|
||||
this.startConsoleSession();
|
||||
} else {
|
||||
this.stopConsoleSession();
|
||||
}
|
||||
}));
|
||||
|
||||
this._inputDisposables.add(this._model.onDidChangeLoadingState(() => {
|
||||
@@ -759,6 +766,66 @@ export class BrowserEditor extends EditorPane {
|
||||
}
|
||||
}
|
||||
|
||||
async addConsoleLogsToChat(): Promise<void> {
|
||||
const resourceUri = this.input?.resource;
|
||||
if (!resourceUri) {
|
||||
return;
|
||||
}
|
||||
|
||||
const locator: IBrowserTargetLocator = { browserViewId: BrowserViewUri.getId(resourceUri) };
|
||||
|
||||
try {
|
||||
const logs = await this.browserElementsService.getConsoleLogs(locator);
|
||||
if (!logs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toAttach: IChatRequestVariableEntry[] = [];
|
||||
toAttach.push({
|
||||
id: 'console-logs-' + Date.now(),
|
||||
name: localize('consoleLogs', 'Console Logs'),
|
||||
fullName: localize('consoleLogs', 'Console Logs'),
|
||||
value: logs,
|
||||
kind: 'element',
|
||||
icon: ThemeIcon.fromId(Codicon.output.id),
|
||||
});
|
||||
|
||||
const widget = await this.chatWidgetService.revealWidget() ?? this.chatWidgetService.lastFocusedWidget;
|
||||
widget?.attachmentModel?.addContext(...toAttach);
|
||||
} catch (error) {
|
||||
this.logService.error('BrowserEditor.addConsoleLogsToChat: Failed to get console logs', error);
|
||||
}
|
||||
}
|
||||
|
||||
private startConsoleSession(): void {
|
||||
// don't restart if already running
|
||||
if (this._consoleSessionCts) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resourceUri = this.input?.resource;
|
||||
if (!resourceUri || !this._model?.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
this._consoleSessionCts = cts;
|
||||
const locator: IBrowserTargetLocator = { browserViewId: BrowserViewUri.getId(resourceUri) };
|
||||
|
||||
this.browserElementsService.startConsoleSession(cts.token, locator).catch(error => {
|
||||
if (!cts.token.isCancellationRequested) {
|
||||
this.logService.error('BrowserEditor: Failed to start console session', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private stopConsoleSession(): void {
|
||||
if (this._consoleSessionCts) {
|
||||
this._consoleSessionCts.dispose(true);
|
||||
this._consoleSessionCts = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update navigation state and context keys
|
||||
*/
|
||||
@@ -907,6 +974,9 @@ export class BrowserEditor extends EditorPane {
|
||||
this._elementSelectionCts = undefined;
|
||||
}
|
||||
|
||||
// Cancel any active console session
|
||||
this.stopConsoleSession();
|
||||
|
||||
// Cancel any scheduled screenshots
|
||||
this.cancelScheduledScreenshot();
|
||||
|
||||
|
||||
@@ -254,6 +254,34 @@ class AddElementToChatAction extends Action2 {
|
||||
}
|
||||
}
|
||||
|
||||
class AddConsoleLogsToChatAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.browser.addConsoleLogsToChat';
|
||||
|
||||
constructor() {
|
||||
const enabled = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('config.chat.sendElementsToChat.enabled', true));
|
||||
super({
|
||||
id: AddConsoleLogsToChatAction.ID,
|
||||
title: localize2('browser.addConsoleLogsToChatAction', 'Add Console Logs to Chat'),
|
||||
category: BrowserCategory,
|
||||
icon: Codicon.output,
|
||||
f1: true,
|
||||
precondition: ContextKeyExpr.and(BROWSER_EDITOR_ACTIVE, CONTEXT_BROWSER_HAS_URL, enabled),
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
group: 'actions',
|
||||
order: 2,
|
||||
when: enabled
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, browserEditor = accessor.get(IEditorService).activeEditorPane): Promise<void> {
|
||||
if (browserEditor instanceof BrowserEditor) {
|
||||
await browserEditor.addConsoleLogsToChat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ToggleDevToolsAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.browser.toggleDevTools';
|
||||
|
||||
@@ -269,7 +297,7 @@ class ToggleDevToolsAction extends Action2 {
|
||||
menu: {
|
||||
id: MenuId.BrowserActionsToolbar,
|
||||
group: 'actions',
|
||||
order: 2,
|
||||
order: 3,
|
||||
},
|
||||
keybinding: {
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
@@ -542,6 +570,7 @@ registerAction2(GoForwardAction);
|
||||
registerAction2(ReloadAction);
|
||||
registerAction2(FocusUrlInputAction);
|
||||
registerAction2(AddElementToChatAction);
|
||||
registerAction2(AddConsoleLogsToChatAction);
|
||||
registerAction2(ToggleDevToolsAction);
|
||||
registerAction2(OpenInExternalBrowserAction);
|
||||
registerAction2(ClearGlobalBrowserStorageAction);
|
||||
|
||||
@@ -17,4 +17,8 @@ export interface IBrowserElementsService {
|
||||
getElementData(rect: IRectangle, token: CancellationToken, locator: IBrowserTargetLocator | undefined): Promise<IElementData | undefined>;
|
||||
|
||||
startDebugSession(token: CancellationToken, locator: IBrowserTargetLocator): Promise<void>;
|
||||
|
||||
startConsoleSession(token: CancellationToken, locator: IBrowserTargetLocator): Promise<void>;
|
||||
|
||||
getConsoleLogs(locator: IBrowserTargetLocator): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,14 @@ class WebBrowserElementsService implements IBrowserElementsService {
|
||||
async startDebugSession(token: CancellationToken, locator: IBrowserTargetLocator): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async startConsoleSession(token: CancellationToken, locator: IBrowserTargetLocator): Promise<void> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async getConsoleLogs(locator: IBrowserTargetLocator): Promise<string | undefined> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IBrowserElementsService, WebBrowserElementsService, InstantiationType.Delayed);
|
||||
|
||||
+23
-1
@@ -33,6 +33,27 @@ class WorkbenchBrowserElementsService implements IBrowserElementsService {
|
||||
@INativeBrowserElementsService private readonly simpleBrowser: INativeBrowserElementsService
|
||||
) { }
|
||||
|
||||
async getConsoleLogs(locator: IBrowserTargetLocator): Promise<string | undefined> {
|
||||
return await this.simpleBrowser.getConsoleLogs(locator);
|
||||
}
|
||||
|
||||
async startConsoleSession(token: CancellationToken, locator: IBrowserTargetLocator): Promise<void> {
|
||||
const cancelAndDetachId = cancelAndDetachIdPool++;
|
||||
const onCancelChannel = `vscode:cancelConsoleSession${cancelAndDetachId}`;
|
||||
|
||||
const disposable = token.onCancellationRequested(() => {
|
||||
ipcRenderer.send(onCancelChannel, cancelAndDetachId);
|
||||
disposable.dispose();
|
||||
});
|
||||
try {
|
||||
await this.simpleBrowser.startConsoleSession(token, locator, cancelAndDetachId);
|
||||
} catch (error) {
|
||||
throw new Error('Failed to start console session', { cause: error });
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async startDebugSession(token: CancellationToken, locator: IBrowserTargetLocator): Promise<void> {
|
||||
const cancelAndDetachId = cancelAndDetachIdPool++;
|
||||
const onCancelChannel = `vscode:cancelCurrentSession${cancelAndDetachId}`;
|
||||
@@ -44,8 +65,9 @@ class WorkbenchBrowserElementsService implements IBrowserElementsService {
|
||||
try {
|
||||
await this.simpleBrowser.startDebugSession(token, locator, cancelAndDetachId);
|
||||
} catch (error) {
|
||||
throw new Error('No debug session target found', { cause: error });
|
||||
} finally {
|
||||
disposable.dispose();
|
||||
throw new Error('No debug session target found', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user