Path: blob/main/src/vs/workbench/contrib/browserView/electron-browser/tools/browserToolHelpers.ts
13405 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 { MarkdownString } from '../../../../../base/common/htmlContent.js';6import { URI } from '../../../../../base/common/uri.js';7import { localize } from '../../../../../nls.js';8import { BrowserViewUri } from '../../../../../platform/browserView/common/browserViewUri.js';9import { IInvokeFunctionResult, IPlaywrightService } from '../../../../../platform/browserView/common/playwrightService.js';10import { IAgentNetworkFilterService } from '../../../../../platform/networkFilter/common/networkFilterService.js';11import { IEditorService } from '../../../../services/editor/common/editorService.js';12import { IToolResult } from '../../../chat/common/tools/languageModelToolsService.js';13import { BrowserEditorInput } from '../../common/browserEditorInput.js';14import { BrowserViewSharingState, IBrowserViewWorkbenchService } from '../../common/browserView.js';1516// eslint-disable-next-line local/code-import-patterns17import type { Page } from 'playwright-core';1819export const DEFAULT_ELEMENT_LABEL = localize('browser.element', 'element');2021export interface FormatBrowserEditorLinesOptions {22indent?: string;23numbered?: boolean;24excludeIds?: boolean;25agentNetworkFilterService?: IAgentNetworkFilterService;26}2728/**29* Formats a list of browser editors as summary lines such as30* `- [pageId] Title (url) (active)`. Active/visible hints are31* derived from the editor service automatically.32*33* When {@link FormatBrowserEditorLinesOptions.agentNetworkFilterService} is34* provided, pages whose URL is blocked by network policy are masked to avoid35* leaking title or URL to the model.36*/37export function formatBrowserEditorList(editorService: IEditorService, editors: readonly BrowserEditorInput[], options?: FormatBrowserEditorLinesOptions): string {38const activeEditor = editorService.activeEditor;39const visibleEditors = new Set(editorService.visibleEditors);40const indent = options?.indent ?? '';41const filterService = options?.agentNetworkFilterService;42return editors.map((editor, index) => {43const url = editor.url || 'about:blank';4445// If the page URL is blocked by network policy, mask its details.46let blocked = false;47if (filterService && url !== 'about:blank') {48try { blocked = !filterService.isUriAllowed(URI.parse(url)); } catch { }49}5051const title = blocked ? localize('browser.blockedByPolicy', "Blocked by network domain policy") : (editor.title || 'Untitled');52const displayUrl = blocked ? '' : ` (${url})`;53const hint = editor === activeEditor ? ' (active)' : visibleEditors.has(editor) ? ' (visible)' : ' (not visible)';54const id = options?.excludeIds ? '' : `[${editor.id}] `;5556// By default, use numbers only if we're excluding IDs, so models don't get confused about which ID to use.57const bullet = (options?.numbered ?? options?.excludeIds) ? `${index + 1}. ` : '- ';58return `${indent}${bullet}${id}${title}${displayUrl}${hint}`;59}).join('\n');60}6162/**63* Creates a markdown link to a browser page.64*/65export function createBrowserPageLink(pageId: string | URI): string {66if (typeof pageId === 'string') {67pageId = BrowserViewUri.forId(pageId);68}69return `[${BrowserEditorInput.DEFAULT_LABEL}](${pageId.toString()}?vscodeLinkType=browser)`;70}7172/**73* Shared helper for running a Playwright function against a page and returning its result.74*/75export async function playwrightInvokeRaw<TArgs extends unknown[], TReturn>(76playwrightService: IPlaywrightService,77pageId: string,78fn: (page: Page, ...args: TArgs) => Promise<TReturn>,79...args: TArgs80): Promise<TReturn> {81return playwrightService.invokeFunctionRaw(pageId, fn.toString(), ...args);82}8384/**85* Shared helper for running a Playwright function against a page and returning86* a tool result. Handles success/error formatting.87*88* Calls {@link IPlaywrightService.invokeFunction} without a timeout so the89* action runs to completion — no deferred results are ever produced.90*/91export async function playwrightInvoke<TArgs extends unknown[], TReturn>(92playwrightService: IPlaywrightService,93pageId: string,94fn: (page: Page, ...args: TArgs) => Promise<TReturn>,95...args: TArgs96): Promise<IToolResult> {97try {98const result = await playwrightService.invokeFunction(pageId, fn.toString(), args);99return invokeFunctionResultToToolResult(result);100} catch (e) {101return errorResult(e instanceof Error ? e.message : String(e));102}103}104105/**106* Convert an {@link IInvokeFunctionResult} to an {@link IToolResult},107* including any {@link IInvokeFunctionResult.deferredResultId}.108*/109export function invokeFunctionResultToToolResult(result: IInvokeFunctionResult, code?: string): IToolResult {110const content: IToolResult['content'] = [];111if (result.result !== undefined) {112content.push({ kind: 'text', value: `Result: ${JSON.stringify(result.result)}` });113}114if (result.error) {115content.push({ kind: 'text', value: result.error });116}117if (result.deferredResultId) {118content.push({ kind: 'text', value: `[deferredResultId=${result.deferredResultId}] The code has not finished executing yet. Call run_playwright_code again with this deferredResultId and the same pageId (no code) to continue waiting.` });119}120content.push({ kind: 'text', value: result.summary });121return {122content,123...(code ? {124toolResultDetails: {125input: code,126inputLanguage: 'javascript',127output: result.result || result.error128? [{ type: 'embed' as const, isText: true, value: JSON.stringify(result.result ?? result.error, null, 2) }]129: [],130isError: !!result.error,131},132} : {}),133};134}135136export function errorResult(message: string): IToolResult {137return {138content: [{ kind: 'text', value: message }],139toolResultError: message,140};141}142143/**144* Checks whether a browser editor with the same host (hostname + port) already exists.145*146* @returns All matching {@link BrowserEditorInput}s.147*/148export function findExistingPagesByHost(149browserViewService: IBrowserViewWorkbenchService,150url: string,151options?: {152includeBlank?: boolean;153sharingState?: BrowserViewSharingState;154}155): BrowserEditorInput[] {156const parsed = URL.parse(url);157if (!parsed || (parsed.protocol !== 'file:' && !parsed.host)) {158return [];159}160161const results: BrowserEditorInput[] = [];162for (const editor of browserViewService.getKnownBrowserViews().values()) {163if (!(editor instanceof BrowserEditorInput)) {164continue;165}166if (options?.sharingState && editor.model?.sharingState !== options.sharingState) {167continue;168}169const editorUrl = URL.parse(editor.url || '');170if (171options?.includeBlank && (!editor.url || editor.url === 'about:blank') ||172editorUrl?.host === parsed.host ||173(parsed.protocol === 'file:' && editorUrl?.protocol === 'file:') ||174(editorUrl?.host && parsed.host && (175editorUrl.host.endsWith('.' + parsed.host) ||176parsed.host.endsWith('.' + editorUrl.host)177))178) {179results.push(editor);180}181}182return results;183}184185/**186* Builds the "already open" tool result returned when an existing page with the187* same host is found by {@link findExistingPagesByHost}.188*/189export async function getExistingPagesResult(190editorService: IEditorService,191existing: BrowserEditorInput[],192formatOptions?: FormatBrowserEditorLinesOptions193): Promise<IToolResult | undefined> {194if (existing.length === 0) {195return undefined;196}197198const list = formatBrowserEditorList(editorService, existing, { indent: ' ', ...formatOptions });199const links = existing.map(e => createBrowserPageLink(e.id));200return {201content: [{202kind: 'text',203value: `At least one similar page is already open:\n${list}\n\nUse an existing page or pass \`forceNew: true\` to open a new one.`204}],205toolResultMessage: new MarkdownString(localize('browser.open.alreadyOpen', "Already open: {0}", links.join(', '))),206};207}208209210