Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/browserView/electron-browser/tools/browserToolHelpers.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
7
import { URI } from '../../../../../base/common/uri.js';
8
import { localize } from '../../../../../nls.js';
9
import { BrowserViewUri } from '../../../../../platform/browserView/common/browserViewUri.js';
10
import { IInvokeFunctionResult, IPlaywrightService } from '../../../../../platform/browserView/common/playwrightService.js';
11
import { IAgentNetworkFilterService } from '../../../../../platform/networkFilter/common/networkFilterService.js';
12
import { IEditorService } from '../../../../services/editor/common/editorService.js';
13
import { IToolResult } from '../../../chat/common/tools/languageModelToolsService.js';
14
import { BrowserEditorInput } from '../../common/browserEditorInput.js';
15
import { BrowserViewSharingState, IBrowserViewWorkbenchService } from '../../common/browserView.js';
16
17
// eslint-disable-next-line local/code-import-patterns
18
import type { Page } from 'playwright-core';
19
20
export const DEFAULT_ELEMENT_LABEL = localize('browser.element', 'element');
21
22
export interface FormatBrowserEditorLinesOptions {
23
indent?: string;
24
numbered?: boolean;
25
excludeIds?: boolean;
26
agentNetworkFilterService?: IAgentNetworkFilterService;
27
}
28
29
/**
30
* Formats a list of browser editors as summary lines such as
31
* `- [pageId] Title (url) (active)`. Active/visible hints are
32
* derived from the editor service automatically.
33
*
34
* When {@link FormatBrowserEditorLinesOptions.agentNetworkFilterService} is
35
* provided, pages whose URL is blocked by network policy are masked to avoid
36
* leaking title or URL to the model.
37
*/
38
export function formatBrowserEditorList(editorService: IEditorService, editors: readonly BrowserEditorInput[], options?: FormatBrowserEditorLinesOptions): string {
39
const activeEditor = editorService.activeEditor;
40
const visibleEditors = new Set(editorService.visibleEditors);
41
const indent = options?.indent ?? '';
42
const filterService = options?.agentNetworkFilterService;
43
return editors.map((editor, index) => {
44
const url = editor.url || 'about:blank';
45
46
// If the page URL is blocked by network policy, mask its details.
47
let blocked = false;
48
if (filterService && url !== 'about:blank') {
49
try { blocked = !filterService.isUriAllowed(URI.parse(url)); } catch { }
50
}
51
52
const title = blocked ? localize('browser.blockedByPolicy', "Blocked by network domain policy") : (editor.title || 'Untitled');
53
const displayUrl = blocked ? '' : ` (${url})`;
54
const hint = editor === activeEditor ? ' (active)' : visibleEditors.has(editor) ? ' (visible)' : ' (not visible)';
55
const id = options?.excludeIds ? '' : `[${editor.id}] `;
56
57
// By default, use numbers only if we're excluding IDs, so models don't get confused about which ID to use.
58
const bullet = (options?.numbered ?? options?.excludeIds) ? `${index + 1}. ` : '- ';
59
return `${indent}${bullet}${id}${title}${displayUrl}${hint}`;
60
}).join('\n');
61
}
62
63
/**
64
* Creates a markdown link to a browser page.
65
*/
66
export function createBrowserPageLink(pageId: string | URI): string {
67
if (typeof pageId === 'string') {
68
pageId = BrowserViewUri.forId(pageId);
69
}
70
return `[${BrowserEditorInput.DEFAULT_LABEL}](${pageId.toString()}?vscodeLinkType=browser)`;
71
}
72
73
/**
74
* Shared helper for running a Playwright function against a page and returning its result.
75
*/
76
export async function playwrightInvokeRaw<TArgs extends unknown[], TReturn>(
77
playwrightService: IPlaywrightService,
78
pageId: string,
79
fn: (page: Page, ...args: TArgs) => Promise<TReturn>,
80
...args: TArgs
81
): Promise<TReturn> {
82
return playwrightService.invokeFunctionRaw(pageId, fn.toString(), ...args);
83
}
84
85
/**
86
* Shared helper for running a Playwright function against a page and returning
87
* a tool result. Handles success/error formatting.
88
*
89
* Calls {@link IPlaywrightService.invokeFunction} without a timeout so the
90
* action runs to completion — no deferred results are ever produced.
91
*/
92
export async function playwrightInvoke<TArgs extends unknown[], TReturn>(
93
playwrightService: IPlaywrightService,
94
pageId: string,
95
fn: (page: Page, ...args: TArgs) => Promise<TReturn>,
96
...args: TArgs
97
): Promise<IToolResult> {
98
try {
99
const result = await playwrightService.invokeFunction(pageId, fn.toString(), args);
100
return invokeFunctionResultToToolResult(result);
101
} catch (e) {
102
return errorResult(e instanceof Error ? e.message : String(e));
103
}
104
}
105
106
/**
107
* Convert an {@link IInvokeFunctionResult} to an {@link IToolResult},
108
* including any {@link IInvokeFunctionResult.deferredResultId}.
109
*/
110
export function invokeFunctionResultToToolResult(result: IInvokeFunctionResult, code?: string): IToolResult {
111
const content: IToolResult['content'] = [];
112
if (result.result !== undefined) {
113
content.push({ kind: 'text', value: `Result: ${JSON.stringify(result.result)}` });
114
}
115
if (result.error) {
116
content.push({ kind: 'text', value: result.error });
117
}
118
if (result.deferredResultId) {
119
content.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.` });
120
}
121
content.push({ kind: 'text', value: result.summary });
122
return {
123
content,
124
...(code ? {
125
toolResultDetails: {
126
input: code,
127
inputLanguage: 'javascript',
128
output: result.result || result.error
129
? [{ type: 'embed' as const, isText: true, value: JSON.stringify(result.result ?? result.error, null, 2) }]
130
: [],
131
isError: !!result.error,
132
},
133
} : {}),
134
};
135
}
136
137
export function errorResult(message: string): IToolResult {
138
return {
139
content: [{ kind: 'text', value: message }],
140
toolResultError: message,
141
};
142
}
143
144
/**
145
* Checks whether a browser editor with the same host (hostname + port) already exists.
146
*
147
* @returns All matching {@link BrowserEditorInput}s.
148
*/
149
export function findExistingPagesByHost(
150
browserViewService: IBrowserViewWorkbenchService,
151
url: string,
152
options?: {
153
includeBlank?: boolean;
154
sharingState?: BrowserViewSharingState;
155
}
156
): BrowserEditorInput[] {
157
const parsed = URL.parse(url);
158
if (!parsed || (parsed.protocol !== 'file:' && !parsed.host)) {
159
return [];
160
}
161
162
const results: BrowserEditorInput[] = [];
163
for (const editor of browserViewService.getKnownBrowserViews().values()) {
164
if (!(editor instanceof BrowserEditorInput)) {
165
continue;
166
}
167
if (options?.sharingState && editor.model?.sharingState !== options.sharingState) {
168
continue;
169
}
170
const editorUrl = URL.parse(editor.url || '');
171
if (
172
options?.includeBlank && (!editor.url || editor.url === 'about:blank') ||
173
editorUrl?.host === parsed.host ||
174
(parsed.protocol === 'file:' && editorUrl?.protocol === 'file:') ||
175
(editorUrl?.host && parsed.host && (
176
editorUrl.host.endsWith('.' + parsed.host) ||
177
parsed.host.endsWith('.' + editorUrl.host)
178
))
179
) {
180
results.push(editor);
181
}
182
}
183
return results;
184
}
185
186
/**
187
* Builds the "already open" tool result returned when an existing page with the
188
* same host is found by {@link findExistingPagesByHost}.
189
*/
190
export async function getExistingPagesResult(
191
editorService: IEditorService,
192
existing: BrowserEditorInput[],
193
formatOptions?: FormatBrowserEditorLinesOptions
194
): Promise<IToolResult | undefined> {
195
if (existing.length === 0) {
196
return undefined;
197
}
198
199
const list = formatBrowserEditorList(editorService, existing, { indent: ' ', ...formatOptions });
200
const links = existing.map(e => createBrowserPageLink(e.id));
201
return {
202
content: [{
203
kind: 'text',
204
value: `At least one similar page is already open:\n${list}\n\nUse an existing page or pass \`forceNew: true\` to open a new one.`
205
}],
206
toolResultMessage: new MarkdownString(localize('browser.open.alreadyOpen', "Already open: {0}", links.join(', '))),
207
};
208
}
209
210