@tontoko/fast-playwright-mcp 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +1047 -0
  3. package/cli.js +18 -0
  4. package/config.d.ts +124 -0
  5. package/index.d.ts +25 -0
  6. package/index.js +18 -0
  7. package/lib/actions.d.js +0 -0
  8. package/lib/batch/batch-executor.js +137 -0
  9. package/lib/browser-context-factory.js +252 -0
  10. package/lib/browser-server-backend.js +139 -0
  11. package/lib/config/constants.js +80 -0
  12. package/lib/config.js +405 -0
  13. package/lib/context.js +274 -0
  14. package/lib/diagnostics/common/diagnostic-base.js +63 -0
  15. package/lib/diagnostics/common/error-enrichment-utils.js +212 -0
  16. package/lib/diagnostics/common/index.js +56 -0
  17. package/lib/diagnostics/common/initialization-manager.js +210 -0
  18. package/lib/diagnostics/common/performance-tracker.js +132 -0
  19. package/lib/diagnostics/diagnostic-error.js +140 -0
  20. package/lib/diagnostics/diagnostic-level.js +123 -0
  21. package/lib/diagnostics/diagnostic-thresholds.js +347 -0
  22. package/lib/diagnostics/element-discovery.js +441 -0
  23. package/lib/diagnostics/enhanced-error-handler.js +376 -0
  24. package/lib/diagnostics/error-enrichment.js +157 -0
  25. package/lib/diagnostics/frame-reference-manager.js +179 -0
  26. package/lib/diagnostics/page-analyzer.js +639 -0
  27. package/lib/diagnostics/parallel-page-analyzer.js +129 -0
  28. package/lib/diagnostics/resource-manager.js +134 -0
  29. package/lib/diagnostics/smart-config.js +482 -0
  30. package/lib/diagnostics/smart-handle.js +118 -0
  31. package/lib/diagnostics/unified-system.js +717 -0
  32. package/lib/extension/cdp-relay.js +486 -0
  33. package/lib/extension/extension-context-factory.js +74 -0
  34. package/lib/extension/main.js +41 -0
  35. package/lib/file-utils.js +42 -0
  36. package/lib/generate-keys.js +75 -0
  37. package/lib/http-server.js +50 -0
  38. package/lib/in-process-client.js +64 -0
  39. package/lib/index.js +48 -0
  40. package/lib/javascript.js +90 -0
  41. package/lib/log.js +33 -0
  42. package/lib/loop/loop-claude.js +247 -0
  43. package/lib/loop/loop-open-ai.js +222 -0
  44. package/lib/loop/loop.js +174 -0
  45. package/lib/loop/main.js +46 -0
  46. package/lib/loopTools/context.js +76 -0
  47. package/lib/loopTools/main.js +65 -0
  48. package/lib/loopTools/perform.js +40 -0
  49. package/lib/loopTools/snapshot.js +37 -0
  50. package/lib/loopTools/tool.js +26 -0
  51. package/lib/manual-promise.js +125 -0
  52. package/lib/mcp/in-process-transport.js +91 -0
  53. package/lib/mcp/proxy-backend.js +127 -0
  54. package/lib/mcp/server.js +123 -0
  55. package/lib/mcp/transport.js +159 -0
  56. package/lib/package.js +28 -0
  57. package/lib/program.js +82 -0
  58. package/lib/response.js +493 -0
  59. package/lib/schemas/expectation.js +152 -0
  60. package/lib/session-log.js +210 -0
  61. package/lib/tab.js +417 -0
  62. package/lib/tools/base-tool-handler.js +141 -0
  63. package/lib/tools/batch-execute.js +150 -0
  64. package/lib/tools/common.js +65 -0
  65. package/lib/tools/console.js +60 -0
  66. package/lib/tools/diagnose/diagnose-analysis-runner.js +101 -0
  67. package/lib/tools/diagnose/diagnose-config-handler.js +130 -0
  68. package/lib/tools/diagnose/diagnose-report-builder.js +394 -0
  69. package/lib/tools/diagnose.js +147 -0
  70. package/lib/tools/dialogs.js +57 -0
  71. package/lib/tools/evaluate.js +67 -0
  72. package/lib/tools/files.js +53 -0
  73. package/lib/tools/find-elements.js +307 -0
  74. package/lib/tools/install.js +60 -0
  75. package/lib/tools/keyboard.js +93 -0
  76. package/lib/tools/mouse.js +110 -0
  77. package/lib/tools/navigate.js +82 -0
  78. package/lib/tools/network.js +50 -0
  79. package/lib/tools/pdf.js +46 -0
  80. package/lib/tools/screenshot.js +113 -0
  81. package/lib/tools/snapshot.js +158 -0
  82. package/lib/tools/tabs.js +97 -0
  83. package/lib/tools/tool.js +47 -0
  84. package/lib/tools/utils.js +131 -0
  85. package/lib/tools/wait.js +64 -0
  86. package/lib/tools.js +65 -0
  87. package/lib/types/batch.js +47 -0
  88. package/lib/types/diff.js +0 -0
  89. package/lib/types/performance.js +0 -0
  90. package/lib/types/threshold-base.js +0 -0
  91. package/lib/utils/array-utils.js +44 -0
  92. package/lib/utils/code-deduplication-utils.js +141 -0
  93. package/lib/utils/common-formatters.js +252 -0
  94. package/lib/utils/console-filter.js +64 -0
  95. package/lib/utils/diagnostic-report-utils.js +178 -0
  96. package/lib/utils/diff-formatter.js +126 -0
  97. package/lib/utils/disposable-manager.js +135 -0
  98. package/lib/utils/error-handler-middleware.js +77 -0
  99. package/lib/utils/image-processor.js +137 -0
  100. package/lib/utils/index.js +92 -0
  101. package/lib/utils/report-builder.js +189 -0
  102. package/lib/utils/request-logger.js +82 -0
  103. package/lib/utils/response-diff-detector.js +150 -0
  104. package/lib/utils/section-builder.js +62 -0
  105. package/lib/utils/tool-patterns.js +153 -0
  106. package/lib/utils.js +46 -0
  107. package/package.json +77 -0
package/lib/program.js ADDED
@@ -0,0 +1,82 @@
1
+ // src/program.ts
2
+ import { Option, program } from "commander";
3
+ import debug from "debug";
4
+ import { startTraceViewerServer } from "playwright-core/lib/server";
5
+ import { contextFactory } from "./browser-context-factory.js";
6
+ import {
7
+ BrowserServerBackend
8
+ } from "./browser-server-backend.js";
9
+ import {
10
+ commaSeparatedList,
11
+ resolveCLIConfig,
12
+ semicolonSeparatedList
13
+ } from "./config.js";
14
+ import { Context } from "./context.js";
15
+ import {
16
+ createExtensionContextFactory,
17
+ runWithExtension
18
+ } from "./extension/main.js";
19
+ import { runLoopTools } from "./loopTools/main.js";
20
+ import { start } from "./mcp/transport.js";
21
+ import { packageJSON } from "./package.js";
22
+ import { logServerStart } from "./utils/request-logger.js";
23
+ var programDebug = debug("pw:mcp:program");
24
+ program.version(`Version ${packageJSON.version}`).name(packageJSON.name).option("--allowed-origins <origins>", "semicolon-separated list of origins to allow the browser to request. Default is to allow all.", semicolonSeparatedList).option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.", semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--config <path>", "path to the configuration file.").option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".').option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.").option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280, 720"').addOption(new Option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').hideHelp()).addOption(new Option("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp()).addOption(new Option("--loop-tools", "Run loop tools").hideHelp()).addOption(new Option("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
25
+ setupExitWatchdog();
26
+ if (options.vision) {
27
+ options.caps = "vision";
28
+ }
29
+ try {
30
+ const config = await resolveCLIConfig(options);
31
+ if (options.extension) {
32
+ await runWithExtension(config);
33
+ return;
34
+ }
35
+ if (options.loopTools) {
36
+ await runLoopTools(config);
37
+ return;
38
+ }
39
+ const browserContextFactory = contextFactory(config);
40
+ let serverBackendFactory;
41
+ if (options.connectTool) {
42
+ const factories = [
43
+ browserContextFactory,
44
+ createExtensionContextFactory(config)
45
+ ];
46
+ serverBackendFactory = () => new BrowserServerBackend(config, factories);
47
+ } else {
48
+ const factories = [browserContextFactory];
49
+ serverBackendFactory = () => new BrowserServerBackend(config, factories);
50
+ }
51
+ logServerStart();
52
+ await start(serverBackendFactory, config.server);
53
+ if (config.saveTrace) {
54
+ const server = await startTraceViewerServer();
55
+ const urlPrefix = server.urlPrefix("human-readable");
56
+ const url = urlPrefix + "/trace/index.html?trace=" + config.browser.launchOptions.tracesDir + "/trace.json";
57
+ programDebug(`Trace viewer available at: ${url}`);
58
+ }
59
+ } catch (error) {
60
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
61
+ `);
62
+ process.exit(1);
63
+ }
64
+ });
65
+ function setupExitWatchdog() {
66
+ let isExiting = false;
67
+ const handleExit = async () => {
68
+ if (isExiting) {
69
+ return;
70
+ }
71
+ isExiting = true;
72
+ setTimeout(() => process.exit(0), 15000);
73
+ await Context.disposeAll();
74
+ process.exit(0);
75
+ };
76
+ process.stdin.on("close", handleExit);
77
+ process.on("SIGINT", handleExit);
78
+ process.on("SIGTERM", handleExit);
79
+ }
80
+ program.parseAsync(process.argv).catch(() => {
81
+ process.exit(1);
82
+ });
@@ -0,0 +1,493 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
+
20
+ // src/response.ts
21
+ import debug from "debug";
22
+ import { TIMEOUTS } from "./config/constants.js";
23
+ import { mergeExpectations } from "./schemas/expectation.js";
24
+ import { renderModalStates } from "./tab.js";
25
+ import { filterConsoleMessages } from "./utils/console-filter.js";
26
+ import { processImage } from "./utils/image-processor.js";
27
+ import { TextReportBuilder } from "./utils/report-builder.js";
28
+ import { ResponseDiffDetector } from "./utils/response-diff-detector.js";
29
+ var responseDebug = debug("pw:mcp:response");
30
+
31
+ class Response {
32
+ _result = [];
33
+ _code = [];
34
+ _images = [];
35
+ _context;
36
+ _includeSnapshot = false;
37
+ _includeTabs = false;
38
+ _tabSnapshot;
39
+ _expectation;
40
+ _diffResult;
41
+ toolName;
42
+ toolArgs;
43
+ _isError;
44
+ static diffDetector = new ResponseDiffDetector;
45
+ constructor(context, toolName, toolArgs, expectation) {
46
+ this._context = context;
47
+ this.toolName = toolName;
48
+ this.toolArgs = toolArgs;
49
+ const actualExpectation = expectation || toolArgs.expectation;
50
+ this._expectation = mergeExpectations(toolName, actualExpectation);
51
+ }
52
+ addResult(result) {
53
+ this._result.push(result);
54
+ }
55
+ addError(error) {
56
+ this._result.push(error);
57
+ this._isError = true;
58
+ }
59
+ isError() {
60
+ return this._isError;
61
+ }
62
+ result() {
63
+ return this._result.join(`
64
+ `);
65
+ }
66
+ addCode(code) {
67
+ this._code.push(code);
68
+ }
69
+ code() {
70
+ return this._code.join(`
71
+ `);
72
+ }
73
+ addImage(image) {
74
+ this._images.push(image);
75
+ }
76
+ images() {
77
+ return this._images;
78
+ }
79
+ setIncludeSnapshot() {
80
+ this._includeSnapshot = true;
81
+ }
82
+ setIncludeTabs() {
83
+ this._includeTabs = true;
84
+ }
85
+ setTabSnapshot(snapshot) {
86
+ this._tabSnapshot = snapshot;
87
+ }
88
+ async finish() {
89
+ const shouldIncludeSnapshot = this._expectation.includeSnapshot || this._includeSnapshot;
90
+ if (shouldIncludeSnapshot && this._context.currentTab() && !this._tabSnapshot) {
91
+ await this._captureSnapshotWithNavigationHandling();
92
+ }
93
+ await Promise.all(this._context.tabs().map((tab) => tab.updateTitle()));
94
+ if (this._expectation.imageOptions && this._images.length > 0) {
95
+ const processedImages = await Promise.all(this._images.map(async (image) => {
96
+ try {
97
+ const processedResult = await processImage(image.data, image.contentType, this._expectation.imageOptions);
98
+ return {
99
+ contentType: processedResult.contentType,
100
+ data: processedResult.data
101
+ };
102
+ } catch (error) {
103
+ responseDebug("Image processing failed:", error);
104
+ return image;
105
+ }
106
+ }));
107
+ this._images.length = 0;
108
+ this._images.push(...processedImages);
109
+ }
110
+ if (this._expectation.diffOptions?.enabled) {
111
+ try {
112
+ const currentContent = this.buildContentForDiff();
113
+ const diffOptions = {
114
+ enabled: this._expectation.diffOptions.enabled,
115
+ threshold: this._expectation.diffOptions.threshold ?? 0.1,
116
+ format: this._expectation.diffOptions.format ?? "unified",
117
+ maxDiffLines: this._expectation.diffOptions.maxDiffLines ?? 50,
118
+ ignoreWhitespace: this._expectation.diffOptions.ignoreWhitespace ?? true,
119
+ context: this._expectation.diffOptions.context ?? 3
120
+ };
121
+ this._diffResult = Response.diffDetector.detectDiff(currentContent, this.toolName, diffOptions);
122
+ } catch (error) {
123
+ responseDebug("Diff detection failed:", error);
124
+ this._diffResult = undefined;
125
+ }
126
+ }
127
+ }
128
+ tabSnapshot() {
129
+ return this._tabSnapshot;
130
+ }
131
+ serialize() {
132
+ const response = [];
133
+ this._addDiffSectionToResponse(response);
134
+ this._addResultSectionToResponse(response);
135
+ this._addCodeSectionToResponse(response);
136
+ const { shouldIncludeTabs, shouldIncludeSnapshot } = this._getInclusionFlags();
137
+ if (shouldIncludeTabs) {
138
+ response.push(...renderTabsMarkdown(this._context.tabs(), true));
139
+ }
140
+ this._addSnapshotSectionToResponse(response, shouldIncludeSnapshot);
141
+ const content = [
142
+ { type: "text", text: response.join(`
143
+ `) }
144
+ ];
145
+ if (this._context.config.imageResponses !== "omit") {
146
+ for (const image of this._images) {
147
+ content.push({
148
+ type: "image",
149
+ data: image.data.toString("base64"),
150
+ mimeType: image.contentType
151
+ });
152
+ }
153
+ }
154
+ return { content, isError: this._isError };
155
+ }
156
+ renderFilteredTabSnapshot(tabSnapshot) {
157
+ const sections = [
158
+ this.buildConsoleSection(tabSnapshot),
159
+ this.buildDownloadsSection(tabSnapshot),
160
+ this.buildPageStateSection(tabSnapshot)
161
+ ].filter(Boolean);
162
+ return sections.join(`
163
+ `);
164
+ }
165
+ buildConsoleSection(tabSnapshot) {
166
+ if (!(this._expectation.includeConsole && tabSnapshot.consoleMessages.length)) {
167
+ return null;
168
+ }
169
+ const filteredMessages = filterConsoleMessages(tabSnapshot.consoleMessages, this._expectation.consoleOptions);
170
+ if (!filteredMessages.length) {
171
+ return null;
172
+ }
173
+ return this.buildSection("New console messages", (b) => {
174
+ for (const message of filteredMessages) {
175
+ b.addListItem(trim(message.toString(), 100));
176
+ }
177
+ }, 3);
178
+ }
179
+ buildDownloadsSection(tabSnapshot) {
180
+ if (!(this._expectation.includeDownloads && tabSnapshot.downloads.length)) {
181
+ return null;
182
+ }
183
+ return this.buildSection("Downloads", (b) => {
184
+ for (const entry of tabSnapshot.downloads) {
185
+ const filename = entry.download.suggestedFilename();
186
+ const status = entry.finished ? `Downloaded file ${filename} to ${entry.outputFile}` : `Downloading file ${filename} ...`;
187
+ b.addListItem(status);
188
+ }
189
+ }, 3);
190
+ }
191
+ buildPageStateSection(tabSnapshot) {
192
+ return this.buildSection("Page state", (b) => {
193
+ b.addKeyValue("Page URL", tabSnapshot.url);
194
+ b.addKeyValue("Page Title", tabSnapshot.title);
195
+ if (this._expectation.includeSnapshot) {
196
+ b.addLine("- Page Snapshot:");
197
+ b.addCodeBlock(tabSnapshot.ariaSnapshot, "yaml");
198
+ }
199
+ }, 3);
200
+ }
201
+ buildSection(title, contentFn, level = 2) {
202
+ const builder = new TextReportBuilder;
203
+ builder.addSection(title, contentFn, level);
204
+ return builder.getSections().join(`
205
+ `);
206
+ }
207
+ _addDiffSectionToResponse(response) {
208
+ if (!(this._diffResult?.hasDifference && this._diffResult.formattedDiff)) {
209
+ return;
210
+ }
211
+ response.push("### Changes from previous response", `Similarity: ${(this._diffResult.similarity * 100).toFixed(1)}%`, `Changes: ${this._diffResult.metadata.addedLines} additions, ${this._diffResult.metadata.removedLines} deletions`, "", "```diff", this._diffResult.formattedDiff, "```", "");
212
+ }
213
+ _addResultSectionToResponse(response) {
214
+ if (this._result.length) {
215
+ response.push("### Result", this._result.join(`
216
+ `), "");
217
+ }
218
+ }
219
+ _addCodeSectionToResponse(response) {
220
+ if (this._code.length && this._expectation.includeCode) {
221
+ response.push(`### Ran Playwright code
222
+ \`\`\`js
223
+ ${this._code.join(`
224
+ `)}
225
+ \`\`\``, "");
226
+ }
227
+ }
228
+ _getInclusionFlags() {
229
+ return {
230
+ shouldIncludeTabs: this._expectation.includeTabs || this._includeTabs,
231
+ shouldIncludeSnapshot: this._expectation.includeSnapshot || this._includeSnapshot
232
+ };
233
+ }
234
+ _addSnapshotSectionToResponse(response, shouldIncludeSnapshot) {
235
+ if (!(shouldIncludeSnapshot && this._tabSnapshot)) {
236
+ return;
237
+ }
238
+ if (this._tabSnapshot.modalStates.length) {
239
+ response.push(...renderModalStates(this._context, this._tabSnapshot.modalStates), "");
240
+ } else {
241
+ response.push(this.renderFilteredTabSnapshot(this._tabSnapshot), "");
242
+ }
243
+ }
244
+ buildContentForDiff() {
245
+ const sections = [
246
+ this.buildResultDiffSection(),
247
+ this.buildCodeDiffSection(),
248
+ this.buildSnapshotDiffSection(),
249
+ this.buildConsoleMessagesDiffSection()
250
+ ].filter(Boolean);
251
+ return sections.join(`
252
+ `);
253
+ }
254
+ buildResultDiffSection() {
255
+ return this._result.length ? ["### Result", this._result.join(`
256
+ `)].join(`
257
+ `) : null;
258
+ }
259
+ buildCodeDiffSection() {
260
+ return this._code.length ? ["### Code", this._code.join(`
261
+ `)].join(`
262
+ `) : null;
263
+ }
264
+ buildSnapshotDiffSection() {
265
+ if (!(this._tabSnapshot && this._expectation.includeSnapshot)) {
266
+ return null;
267
+ }
268
+ return [
269
+ "### Page State",
270
+ `URL: ${this._tabSnapshot.url}`,
271
+ `Title: ${this._tabSnapshot.title}`,
272
+ "Snapshot:",
273
+ this._tabSnapshot.ariaSnapshot
274
+ ].join(`
275
+ `);
276
+ }
277
+ buildConsoleMessagesDiffSection() {
278
+ if (!(this._tabSnapshot?.consoleMessages.length && this._expectation.includeConsole)) {
279
+ return null;
280
+ }
281
+ const filteredMessages = filterConsoleMessages(this._tabSnapshot.consoleMessages, this._expectation.consoleOptions);
282
+ if (!filteredMessages.length) {
283
+ return null;
284
+ }
285
+ const messageLines = filteredMessages.map((msg) => `- ${msg.toString()}`);
286
+ return ["### Console Messages", ...messageLines].join(`
287
+ `);
288
+ }
289
+ _getNavigationRetryConfig() {
290
+ return {
291
+ maxRetries: 3,
292
+ retryDelay: TIMEOUTS.RETRY_DELAY,
293
+ stabilityTimeout: TIMEOUTS.STABILITY_TIMEOUT,
294
+ evaluationTimeout: 200
295
+ };
296
+ }
297
+ async _captureSnapshotWithNavigationHandling() {
298
+ const currentTab = this._context.currentTabOrDie();
299
+ const { maxRetries } = this._getNavigationRetryConfig();
300
+ await this._executeWithRetry(() => this._performSnapshotCapture(currentTab), maxRetries);
301
+ }
302
+ async _executeWithRetry(operation, maxRetries) {
303
+ await this._executeWithRetryRecursive(operation, 1, maxRetries);
304
+ }
305
+ async _executeWithRetryRecursive(operation, attempt, maxRetries) {
306
+ if (attempt > maxRetries) {
307
+ return;
308
+ }
309
+ try {
310
+ await operation();
311
+ return;
312
+ } catch (error) {
313
+ const shouldRetry = await this._handleSnapshotError(error, attempt, maxRetries, this._getNavigationRetryConfig().retryDelay, this._context.currentTabOrDie());
314
+ if (!shouldRetry) {
315
+ return;
316
+ }
317
+ await this._executeWithRetryRecursive(operation, attempt + 1, maxRetries);
318
+ }
319
+ }
320
+ async _performSnapshotCapture(tab) {
321
+ await this._handleNavigationIfNeeded(tab);
322
+ await this._attemptSnapshotCapture(tab);
323
+ }
324
+ async _handleNavigationIfNeeded(tab) {
325
+ if (await this._isPageNavigating(tab)) {
326
+ await this._waitForNavigationStability(tab);
327
+ }
328
+ }
329
+ async _attemptSnapshotCapture(tab) {
330
+ const options = this._expectation.snapshotOptions;
331
+ if (options?.selector || options?.maxLength) {
332
+ this._tabSnapshot = await tab.capturePartialSnapshot(options.selector, options.maxLength);
333
+ } else {
334
+ this._tabSnapshot = await tab.captureSnapshot();
335
+ }
336
+ }
337
+ async _handleSnapshotError(error, attempt, maxRetries, retryDelay, tab) {
338
+ const errorMessage = error?.message ?? "";
339
+ if (this._isRecoverableError(errorMessage) && attempt < maxRetries) {
340
+ await this._delayRetry(retryDelay, attempt);
341
+ return true;
342
+ }
343
+ if (attempt === maxRetries) {
344
+ await this._handleFinalFailure(tab);
345
+ }
346
+ return false;
347
+ }
348
+ _isRecoverableError(errorMessage) {
349
+ const recoverableErrors = [
350
+ "Execution context was destroyed",
351
+ "Target closed",
352
+ "Session closed"
353
+ ];
354
+ return recoverableErrors.some((err) => errorMessage.includes(err));
355
+ }
356
+ async _delayRetry(retryDelay, attempt) {
357
+ await new Promise((resolve) => setTimeout(resolve, retryDelay * attempt));
358
+ }
359
+ async _handleFinalFailure(tab) {
360
+ try {
361
+ this._tabSnapshot = await this._captureBasicSnapshot(tab);
362
+ } catch (error) {
363
+ responseDebug("Failed to capture basic snapshot:", error);
364
+ this._tabSnapshot = undefined;
365
+ }
366
+ }
367
+ async _isPageNavigating(tab) {
368
+ try {
369
+ if (this._hasInternalNavigationState(tab)) {
370
+ return tab.isNavigating();
371
+ }
372
+ const navigationState = await this._evaluateNavigationState(tab);
373
+ return this._isNavigationInProgress(navigationState);
374
+ } catch (error) {
375
+ responseDebug("Navigation check failed (assuming in progress):", error);
376
+ return true;
377
+ }
378
+ }
379
+ _hasInternalNavigationState(tab) {
380
+ return "isNavigating" in tab && typeof tab.isNavigating === "function";
381
+ }
382
+ async _evaluateNavigationState(tab) {
383
+ return await Promise.race([
384
+ tab.page.evaluate(() => [
385
+ document.readyState,
386
+ window.performance?.timing?.loadEventEnd === 0
387
+ ]).catch(() => [null, null]),
388
+ new Promise((resolve) => setTimeout(() => resolve([null, null]), this._getNavigationRetryConfig().evaluationTimeout))
389
+ ]);
390
+ }
391
+ _isNavigationInProgress(state) {
392
+ const [readyState, isLoading] = state;
393
+ return readyState === "loading" || isLoading === true;
394
+ }
395
+ async _waitForNavigationStability(tab) {
396
+ if (await this._tryBuiltInNavigationCompletion(tab)) {
397
+ return;
398
+ }
399
+ await this._performManualStabilityCheck(tab);
400
+ }
401
+ async _tryBuiltInNavigationCompletion(tab) {
402
+ if (!this._hasNavigationCompletionMethod(tab)) {
403
+ return false;
404
+ }
405
+ try {
406
+ await tab.waitForNavigationComplete();
407
+ return true;
408
+ } catch (error) {
409
+ responseDebug("Tab navigation completion failed:", error);
410
+ return false;
411
+ }
412
+ }
413
+ _hasNavigationCompletionMethod(tab) {
414
+ return "waitForNavigationComplete" in tab && typeof tab.waitForNavigationComplete === "function";
415
+ }
416
+ async _performManualStabilityCheck(tab) {
417
+ const { stabilityTimeout } = this._getNavigationRetryConfig();
418
+ const startTime = Date.now();
419
+ await this._performStabilityCheckRecursive(tab, startTime, stabilityTimeout);
420
+ }
421
+ async _performStabilityCheckRecursive(tab, startTime, stabilityTimeout) {
422
+ if (Date.now() - startTime >= stabilityTimeout) {
423
+ return;
424
+ }
425
+ if (await this._checkPageStability(tab)) {
426
+ await new Promise((resolve) => setTimeout(resolve, 200));
427
+ return;
428
+ }
429
+ await new Promise((resolve) => setTimeout(resolve, 100));
430
+ await this._performStabilityCheckRecursive(tab, startTime, stabilityTimeout);
431
+ }
432
+ async _checkPageStability(tab) {
433
+ try {
434
+ await tab.page.waitForLoadState("load", {
435
+ timeout: TIMEOUTS.MEDIUM_DELAY
436
+ });
437
+ await tab.page.waitForLoadState("networkidle", { timeout: TIMEOUTS.SHORT_DELAY }).catch(() => {});
438
+ return await tab.page.evaluate(() => document.readyState === "complete").catch(() => false);
439
+ } catch (error) {
440
+ responseDebug("Page stability check failed (retrying):", error);
441
+ return false;
442
+ }
443
+ }
444
+ async _captureBasicSnapshot(tab) {
445
+ try {
446
+ return await tab.captureSnapshot();
447
+ } catch (error) {
448
+ responseDebug("Basic snapshot capture failed, creating minimal snapshot:", error);
449
+ return await this._createMinimalSnapshot(tab);
450
+ }
451
+ }
452
+ async _createMinimalSnapshot(tab) {
453
+ const url = tab.page.url();
454
+ const title = await tab.page.title().catch(() => "");
455
+ return {
456
+ url,
457
+ title,
458
+ ariaSnapshot: "// Snapshot unavailable due to navigation context issues",
459
+ modalStates: [],
460
+ consoleMessages: [],
461
+ downloads: []
462
+ };
463
+ }
464
+ }
465
+ function renderTabsMarkdown(tabs, force = false) {
466
+ if (tabs.length === 1 && !force) {
467
+ return [];
468
+ }
469
+ if (!tabs.length) {
470
+ return [
471
+ "### Open tabs",
472
+ 'No open tabs. Use the "browser_navigate" tool to navigate to a page first.',
473
+ ""
474
+ ];
475
+ }
476
+ const lines = ["### Open tabs"];
477
+ for (let i = 0;i < tabs.length; i++) {
478
+ const tab = tabs[i];
479
+ const current = tab.isCurrentTab() ? " (current)" : "";
480
+ lines.push(`- ${i}:${current} [${tab.lastTitle()}] (${tab.page.url()})`);
481
+ }
482
+ lines.push("");
483
+ return lines;
484
+ }
485
+ function trim(text, maxLength) {
486
+ if (text.length <= maxLength) {
487
+ return text;
488
+ }
489
+ return `${text.slice(0, maxLength)}...`;
490
+ }
491
+ export {
492
+ Response
493
+ };