playwright 1.58.0-alpha-2025-12-03 → 1.58.0-alpha-2025-12-05

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 (44) hide show
  1. package/README.md +2 -2
  2. package/ThirdPartyNotices.txt +288 -2773
  3. package/lib/agents/agent.js +2 -2
  4. package/lib/agents/performTask.js +121 -8
  5. package/lib/index.js +6 -3
  6. package/lib/mcp/browser/browserContextFactory.js +2 -0
  7. package/lib/mcp/browser/browserServerBackend.js +2 -1
  8. package/lib/mcp/browser/config.js +21 -2
  9. package/lib/mcp/browser/response.js +129 -53
  10. package/lib/mcp/browser/tab.js +39 -8
  11. package/lib/mcp/browser/tools/common.js +5 -5
  12. package/lib/mcp/browser/tools/console.js +4 -4
  13. package/lib/mcp/browser/tools/dialogs.js +4 -4
  14. package/lib/mcp/browser/tools/evaluate.js +5 -5
  15. package/lib/mcp/browser/tools/files.js +3 -3
  16. package/lib/mcp/browser/tools/form.js +7 -7
  17. package/lib/mcp/browser/tools/install.js +2 -2
  18. package/lib/mcp/browser/tools/keyboard.js +6 -6
  19. package/lib/mcp/browser/tools/mouse.js +11 -11
  20. package/lib/mcp/browser/tools/navigate.js +4 -4
  21. package/lib/mcp/browser/tools/network.js +17 -11
  22. package/lib/mcp/browser/tools/pdf.js +3 -3
  23. package/lib/mcp/browser/tools/runCode.js +3 -3
  24. package/lib/mcp/browser/tools/screenshot.js +7 -7
  25. package/lib/mcp/browser/tools/snapshot.js +14 -14
  26. package/lib/mcp/browser/tools/tabs.js +4 -4
  27. package/lib/mcp/browser/tools/tool.js +8 -7
  28. package/lib/mcp/browser/tools/tracing.js +3 -3
  29. package/lib/mcp/browser/tools/verify.js +15 -15
  30. package/lib/mcp/browser/tools/wait.js +5 -5
  31. package/lib/mcp/program.js +1 -1
  32. package/lib/mcp/sdk/http.js +1 -1
  33. package/lib/mcp/sdk/proxyBackend.js +1 -1
  34. package/lib/mcp/sdk/server.js +1 -1
  35. package/lib/mcp/sdk/tool.js +2 -2
  36. package/lib/mcp/test/browserBackend.js +1 -1
  37. package/lib/mcp/test/generatorTools.js +9 -9
  38. package/lib/mcp/test/plannerTools.js +16 -16
  39. package/lib/mcp/test/testBackend.js +2 -2
  40. package/lib/mcp/test/testTools.js +9 -9
  41. package/package.json +2 -2
  42. package/types/test.d.ts +1 -3
  43. package/lib/mcp/sdk/bundle.js +0 -84
  44. package/lib/mcpBundleImpl.js +0 -96
@@ -21,10 +21,10 @@ __export(agent_exports, {
21
21
  Agent: () => Agent
22
22
  });
23
23
  module.exports = __toCommonJS(agent_exports);
24
- var import_bundle = require("../mcp/sdk/bundle");
24
+ var import_mcpBundle = require("playwright-core/lib/mcpBundle");
25
25
  class Agent {
26
26
  constructor(loopName, spec, clients, resultSchema) {
27
- this.loop = new import_bundle.Loop(loopName, { model: spec.model });
27
+ this.loop = new import_mcpBundle.Loop(loopName, { model: spec.model });
28
28
  this.spec = spec;
29
29
  this.clients = clients;
30
30
  this.resultSchema = resultSchema;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,36 +17,147 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
  var performTask_exports = {};
20
30
  __export(performTask_exports, {
31
+ performCache: () => performCache,
21
32
  performTask: () => performTask
22
33
  });
23
34
  module.exports = __toCommonJS(performTask_exports);
35
+ var import_fs = __toESM(require("fs"));
36
+ var import_path = __toESM(require("path"));
24
37
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
38
+ var import_mcpBundle = require("playwright-core/lib/mcpBundle");
25
39
  var import_browserContextFactory = require("../mcp/browser/browserContextFactory");
26
40
  var import_browserServerBackend = require("../mcp/browser/browserServerBackend");
27
41
  var import_config = require("../mcp/browser/config");
28
- var import_bundle = require("../mcp/sdk/bundle");
29
42
  var import_server = require("../mcp/sdk/server");
30
- async function performTask(context, task) {
43
+ const resultSchema = import_mcpBundle.z.object({
44
+ code: import_mcpBundle.z.string().optional().describe(`
45
+ Generated code to perform the task using Playwright API.
46
+ Check out the <code> blocks and combine them. Should be presented in the following form:
47
+
48
+ perform(async ({ page }) => {
49
+ // generated code here.
50
+ });
51
+ `),
52
+ error: import_mcpBundle.z.string().optional().describe("The error that occurred if execution failed.").optional()
53
+ });
54
+ async function performTask(testInfo, context, userTask, options) {
55
+ const cacheStatus = await performTaskFromCache(testInfo, context, userTask);
56
+ if (cacheStatus === "success")
57
+ return;
31
58
  const backend = new import_browserServerBackend.BrowserServerBackend(import_config.defaultConfig, (0, import_browserContextFactory.identityBrowserContextFactory)(context));
32
59
  const client = await (0, import_server.wrapInClient)(backend, { name: "Internal", version: "0.0.0" });
33
- const loop = new import_bundle.Loop("github", { model: "claude-sonnet-4.5" });
34
60
  const callTool = async (params) => {
35
61
  return await client.callTool(params);
36
62
  };
63
+ const loop = new import_mcpBundle.Loop(options.provider ?? "github", {
64
+ model: options.model ?? "claude-sonnet-4.5",
65
+ reasoning: options.reasoning,
66
+ temperature: options.temperature,
67
+ maxTokens: options.maxTokens,
68
+ summarize: true,
69
+ debug: import_utilsBundle.debug,
70
+ callTool,
71
+ tools: await backend.listTools()
72
+ });
37
73
  try {
38
- return await loop.run(task, {
39
- tools: await backend.listTools(),
40
- callTool,
41
- debug: import_utilsBundle.debug
42
- });
74
+ const result = await loop.run(userTask, { resultSchema: (0, import_mcpBundle.zodToJsonSchema)(resultSchema) });
75
+ if (result.code)
76
+ await updatePerformFile(testInfo, userTask, result.code, options);
43
77
  } finally {
44
78
  await client.close();
45
79
  }
46
80
  }
81
+ async function updatePerformFile(testInfo, userTask, taskCode, options) {
82
+ const relativeFile = import_path.default.relative(testInfo.project.testDir, testInfo.file);
83
+ const promptCacheFile = testInfo.file.replace(".spec.ts", ".cache.ts");
84
+ const testTitle = testInfo.title;
85
+ const loop = new import_mcpBundle.Loop(options.provider ?? "github", {
86
+ model: options.model ?? "claude-sonnet-4.5",
87
+ reasoning: options.reasoning,
88
+ temperature: options.temperature,
89
+ maxTokens: options.maxTokens,
90
+ summarize: true,
91
+ debug: import_utilsBundle.debug,
92
+ callTool: async () => ({ content: [] }),
93
+ tools: []
94
+ });
95
+ const resultSchema2 = import_mcpBundle.z.object({
96
+ code: import_mcpBundle.z.string().optional().describe(`
97
+ Generated code with all the perofrm routines combined or updated into the following format:
98
+
99
+ import { performCache } from '@playwright/test';
100
+
101
+ performCache({
102
+ file: 'tests/page/perform-task.spec.ts',
103
+ test: 'perform task',
104
+ task: 'Click the learn more button',
105
+ code: async ({ page }) => {
106
+ await page.getByRole('link', { name: 'Learn more' }).click();
107
+ },
108
+ });
109
+ `)
110
+ });
111
+ const existingCode = await import_fs.default.promises.readFile(promptCacheFile, "utf8").catch(() => "");
112
+ const task = `
113
+ - Create or update a perform file to include performCache block for the given task and code.
114
+ - Dedupe items with the same file, test, and task.
115
+ - Should produce code in the following format
116
+
117
+ import { performCache } from '@playwright/test';
118
+
119
+ performCache({
120
+ file: '<file>',
121
+ test: '<test>',
122
+ task: '<task>',
123
+ code: async ({ page }) => {
124
+ <code>
125
+ },
126
+ });
127
+
128
+ performCache({
129
+ ...
130
+
131
+ ## Params for the new or updated performCache block
132
+ <file-content>${existingCode}</file-content>
133
+ <file>${relativeFile}</file>
134
+ <test>${testTitle}</test>
135
+ <task>${userTask}</task>
136
+ <code>${taskCode}</code>
137
+ `;
138
+ const result = await loop.run(task, { resultSchema: (0, import_mcpBundle.zodToJsonSchema)(resultSchema2) });
139
+ if (result.code)
140
+ await import_fs.default.promises.writeFile(promptCacheFile, result.code);
141
+ }
142
+ const performCacheMap = /* @__PURE__ */ new Map();
143
+ function performCache(entry) {
144
+ performCacheMap.set(JSON.stringify({ ...entry, code: void 0 }), entry);
145
+ }
146
+ async function performTaskFromCache(testInfo, context, userTask) {
147
+ const relativeFile = import_path.default.relative(testInfo.project.testDir, testInfo.file);
148
+ const key = JSON.stringify({ file: relativeFile, test: testInfo.title, task: userTask });
149
+ const entry = performCacheMap.get(key);
150
+ if (!entry)
151
+ return "cache-miss";
152
+ try {
153
+ await entry.code({ page: context.pages()[0] });
154
+ return "success";
155
+ } catch (error) {
156
+ return error;
157
+ }
158
+ }
47
159
  // Annotate the CommonJS export names for ESM import in node:
48
160
  0 && (module.exports = {
161
+ performCache,
49
162
  performTask
50
163
  });
package/lib/index.js CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  expect: () => import_expect.expect,
34
34
  mergeExpects: () => import_expect2.mergeExpects,
35
35
  mergeTests: () => import_testType2.mergeTests,
36
+ performCache: () => import_performTask2.performCache,
36
37
  test: () => test
37
38
  });
38
39
  module.exports = __toCommonJS(index_exports);
@@ -48,6 +49,7 @@ var import_expect = require("./matchers/expect");
48
49
  var import_configLoader = require("./common/configLoader");
49
50
  var import_testType2 = require("./common/testType");
50
51
  var import_expect2 = require("./matchers/expect");
52
+ var import_performTask2 = require("./agents/performTask");
51
53
  const _baseTest = import_testType.rootTestType.test;
52
54
  (0, import_utils.setBoxedStackPrefixes)([import_path.default.dirname(require.resolve("../package.json"))]);
53
55
  if (process["__pw_initiator__"]) {
@@ -417,9 +419,9 @@ const playwrightFixtures = {
417
419
  await request.dispose();
418
420
  }
419
421
  },
420
- _perform: async ({ context }, use) => {
421
- await use(async (task) => {
422
- await (0, import_performTask.performTask)(context, task);
422
+ _perform: async ({ context }, use, testInfo) => {
423
+ await use(async (task, options) => {
424
+ await (0, import_performTask.performTask)(testInfo, context, task, options ?? {});
423
425
  });
424
426
  }
425
427
  };
@@ -684,5 +686,6 @@ const test = _baseTest.extend(playwrightFixtures);
684
686
  expect,
685
687
  mergeExpects,
686
688
  mergeTests,
689
+ performCache,
687
690
  test
688
691
  });
@@ -216,6 +216,8 @@ class PersistentContextFactory {
216
216
  });
217
217
  await afterClose();
218
218
  this._userDataDirs.delete(userDataDir);
219
+ if (process.env.PWMCP_PROFILES_DIR_FOR_TEST && userDataDir.startsWith(process.env.PWMCP_PROFILES_DIR_FOR_TEST))
220
+ await import_fs.default.promises.rm(userDataDir, { recursive: true }).catch(import_log.logUnhandledError);
219
221
  (0, import_log.testDebug)("close browser context complete (persistent)");
220
222
  }
221
223
  async _createUserDataDir(clientInfo) {
@@ -64,7 +64,8 @@ class BrowserServerBackend {
64
64
  context.setRunningTool(void 0);
65
65
  }
66
66
  response.logEnd();
67
- return response.serialize();
67
+ const _meta = rawArguments?._meta;
68
+ return response.serialize({ _meta });
68
69
  }
69
70
  serverClosed() {
70
71
  void this._context?.dispose().catch(import_log.logUnhandledError);
@@ -32,6 +32,7 @@ __export(config_exports, {
32
32
  configFromCLIOptions: () => configFromCLIOptions,
33
33
  defaultConfig: () => defaultConfig,
34
34
  dotenvFileLoader: () => dotenvFileLoader,
35
+ enumParser: () => enumParser,
35
36
  headerParser: () => headerParser,
36
37
  numberParser: () => numberParser,
37
38
  outputDir: () => outputDir,
@@ -61,6 +62,9 @@ const defaultConfig = {
61
62
  viewport: null
62
63
  }
63
64
  },
65
+ console: {
66
+ level: "info"
67
+ },
64
68
  network: {
65
69
  allowedOrigins: void 0,
66
70
  blockedOrigins: void 0
@@ -182,6 +186,9 @@ function configFromCLIOptions(cliOptions) {
182
186
  allowedHosts: cliOptions.allowedHosts
183
187
  },
184
188
  capabilities: cliOptions.caps,
189
+ console: {
190
+ level: cliOptions.consoleLevel
191
+ },
185
192
  network: {
186
193
  allowedOrigins: cliOptions.allowedOrigins,
187
194
  blockedOrigins: cliOptions.blockedOrigins
@@ -213,6 +220,8 @@ function configFromEnv() {
213
220
  options.cdpEndpoint = envToString(process.env.PLAYWRIGHT_MCP_CDP_ENDPOINT);
214
221
  options.cdpHeader = headerParser(process.env.PLAYWRIGHT_MCP_CDP_HEADERS, {});
215
222
  options.config = envToString(process.env.PLAYWRIGHT_MCP_CONFIG);
223
+ if (process.env.PLAYWRIGHT_MCP_CONSOLE_LEVEL)
224
+ options.consoleLevel = enumParser("--console-level", ["error", "warning", "info", "debug"], process.env.PLAYWRIGHT_MCP_CONSOLE_LEVEL);
216
225
  options.device = envToString(process.env.PLAYWRIGHT_MCP_DEVICE);
217
226
  options.executablePath = envToString(process.env.PLAYWRIGHT_MCP_EXECUTABLE_PATH);
218
227
  options.grantPermissions = commaSeparatedList(process.env.PLAYWRIGHT_MCP_GRANT_PERMISSIONS);
@@ -226,8 +235,8 @@ function configFromEnv() {
226
235
  if (initScript)
227
236
  options.initScript = [initScript];
228
237
  options.isolated = envToBoolean(process.env.PLAYWRIGHT_MCP_ISOLATED);
229
- if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES === "omit")
230
- options.imageResponses = "omit";
238
+ if (process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES)
239
+ options.imageResponses = enumParser("--image-responses", ["allow", "omit"], process.env.PLAYWRIGHT_MCP_IMAGE_RESPONSES);
231
240
  options.sandbox = envToBoolean(process.env.PLAYWRIGHT_MCP_SANDBOX);
232
241
  options.outputDir = envToString(process.env.PLAYWRIGHT_MCP_OUTPUT_DIR);
233
242
  options.port = numberParser(process.env.PLAYWRIGHT_MCP_PORT);
@@ -306,6 +315,10 @@ function mergeConfig(base, overrides) {
306
315
  ...pickDefined(base),
307
316
  ...pickDefined(overrides),
308
317
  browser,
318
+ console: {
319
+ ...pickDefined(base.console),
320
+ ...pickDefined(overrides.console)
321
+ },
309
322
  network: {
310
323
  ...pickDefined(base.network),
311
324
  ...pickDefined(overrides.network)
@@ -369,6 +382,11 @@ function headerParser(arg, previous) {
369
382
  result[name] = value;
370
383
  return result;
371
384
  }
385
+ function enumParser(name, options, value) {
386
+ if (!options.includes(value))
387
+ throw new Error(`Invalid ${name}: ${value}. Valid values are: ${options.join(", ")}`);
388
+ return value;
389
+ }
372
390
  function envToBoolean(value) {
373
391
  if (value === "true" || value === "1")
374
392
  return true;
@@ -392,6 +410,7 @@ function sanitizeForFilePath(s) {
392
410
  configFromCLIOptions,
393
411
  defaultConfig,
394
412
  dotenvFileLoader,
413
+ enumParser,
395
414
  headerParser,
396
415
  numberParser,
397
416
  outputDir,
@@ -18,6 +18,7 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var response_exports = {};
20
20
  __export(response_exports, {
21
+ RenderedResponse: () => RenderedResponse,
21
22
  Response: () => Response,
22
23
  parseResponse: () => parseResponse,
23
24
  requestDebug: () => requestDebug
@@ -71,6 +72,9 @@ class Response {
71
72
  setIncludeTabs() {
72
73
  this._includeTabs = true;
73
74
  }
75
+ setIncludeModalStates(modalStates) {
76
+ this._includeModalStates = modalStates;
77
+ }
74
78
  async finish() {
75
79
  if (this._includeSnapshot !== "none" && this._context.currentTab())
76
80
  this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
@@ -89,72 +93,64 @@ class Response {
89
93
  requestDebug(this.serialize({ omitSnapshot: true, omitBlobs: true }));
90
94
  }
91
95
  serialize(options = {}) {
92
- const response = [];
93
- if (this._result.length) {
94
- response.push("### Result");
95
- response.push(this._result.join("\n"));
96
- response.push("");
97
- }
98
- if (this._code.length) {
99
- response.push(`### Ran Playwright code
100
- \`\`\`js
101
- ${this._code.join("\n")}
102
- \`\`\``);
103
- response.push("");
96
+ const renderedResponse = new RenderedResponse();
97
+ if (this._result.length)
98
+ renderedResponse.results.push(...this._result);
99
+ if (this._code.length)
100
+ renderedResponse.code.push(...this._code);
101
+ if (this._includeSnapshot !== "none" || this._includeTabs) {
102
+ const tabsMarkdown = renderTabsMarkdown(this._context.tabs(), this._includeTabs);
103
+ if (tabsMarkdown.length)
104
+ renderedResponse.states.tabs = tabsMarkdown.join("\n");
104
105
  }
105
- if (this._includeSnapshot !== "none" || this._includeTabs)
106
- response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs));
107
106
  if (this._tabSnapshot?.modalStates.length) {
108
- response.push(...(0, import_tab.renderModalStates)(this._context, this._tabSnapshot.modalStates));
109
- response.push("");
107
+ const modalStatesMarkdown = (0, import_tab.renderModalStates)(this._tabSnapshot.modalStates);
108
+ renderedResponse.states.modal = modalStatesMarkdown.join("\n");
110
109
  } else if (this._tabSnapshot) {
111
110
  const includeSnapshot = options.omitSnapshot ? "none" : this._includeSnapshot;
112
- response.push(renderTabSnapshot(this._tabSnapshot, includeSnapshot));
113
- response.push("");
111
+ renderTabSnapshot(this._tabSnapshot, includeSnapshot, renderedResponse);
112
+ } else if (this._includeModalStates) {
113
+ const modalStatesMarkdown = (0, import_tab.renderModalStates)(this._includeModalStates);
114
+ renderedResponse.states.modal = modalStatesMarkdown.join("\n");
114
115
  }
116
+ const redactedResponse = this._context.config.secrets ? renderedResponse.redact(this._context.config.secrets) : renderedResponse;
117
+ const includeMeta = options._meta && "dev.lowire/history" in options._meta && "dev.lowire/state" in options._meta;
118
+ const _meta = includeMeta ? redactedResponse.asMeta() : void 0;
115
119
  const content = [
116
- { type: "text", text: response.join("\n") }
120
+ { type: "text", text: redactedResponse.asText() }
117
121
  ];
118
122
  if (this._context.config.imageResponses !== "omit") {
119
123
  for (const image of this._images)
120
124
  content.push({ type: "image", data: options.omitBlobs ? "<blob>" : image.data.toString("base64"), mimeType: image.contentType });
121
125
  }
122
- this._redactSecrets(content);
123
- return { content, isError: this._isError };
124
- }
125
- _redactSecrets(content) {
126
- if (!this._context.config.secrets)
127
- return;
128
- for (const item of content) {
129
- if (item.type !== "text")
130
- continue;
131
- for (const [secretName, secretValue] of Object.entries(this._context.config.secrets))
132
- item.text = item.text.replaceAll(secretValue, `<secret>${secretName}</secret>`);
133
- }
126
+ return {
127
+ _meta,
128
+ content,
129
+ isError: this._isError
130
+ };
134
131
  }
135
132
  }
136
- function renderTabSnapshot(tabSnapshot, includeSnapshot) {
137
- const lines = [];
133
+ function renderTabSnapshot(tabSnapshot, includeSnapshot, response) {
138
134
  if (tabSnapshot.consoleMessages.length) {
139
- lines.push(`### New console messages`);
135
+ const lines2 = [];
140
136
  for (const message of tabSnapshot.consoleMessages)
141
- lines.push(`- ${trim(message.toString(), 100)}`);
142
- lines.push("");
137
+ lines2.push(`- ${trim(message.toString(), 100)}`);
138
+ response.updates.push({ category: "console", content: lines2.join("\n") });
143
139
  }
144
140
  if (tabSnapshot.downloads.length) {
145
- lines.push(`### Downloads`);
141
+ const lines2 = [];
146
142
  for (const entry of tabSnapshot.downloads) {
147
143
  if (entry.finished)
148
- lines.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
144
+ lines2.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
149
145
  else
150
- lines.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
146
+ lines2.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
151
147
  }
152
- lines.push("");
148
+ response.updates.push({ category: "downloads", content: lines2.join("\n") });
153
149
  }
154
150
  if (includeSnapshot === "incremental" && tabSnapshot.ariaSnapshotDiff === "") {
155
- return lines.join("\n");
151
+ return;
156
152
  }
157
- lines.push(`### Page state`);
153
+ const lines = [];
158
154
  lines.push(`- Page URL: ${tabSnapshot.url}`);
159
155
  lines.push(`- Page Title: ${tabSnapshot.title}`);
160
156
  if (includeSnapshot !== "none") {
@@ -166,25 +162,19 @@ function renderTabSnapshot(tabSnapshot, includeSnapshot) {
166
162
  lines.push(tabSnapshot.ariaSnapshot);
167
163
  lines.push("```");
168
164
  }
169
- return lines.join("\n");
165
+ response.states.page = lines.join("\n");
170
166
  }
171
167
  function renderTabsMarkdown(tabs, force = false) {
172
168
  if (tabs.length === 1 && !force)
173
169
  return [];
174
- if (!tabs.length) {
175
- return [
176
- "### Open tabs",
177
- 'No open tabs. Use the "browser_navigate" tool to navigate to a page first.',
178
- ""
179
- ];
180
- }
181
- const lines = ["### Open tabs"];
170
+ if (!tabs.length)
171
+ return ['No open tabs. Use the "browser_navigate" tool to navigate to a page first.'];
172
+ const lines = [];
182
173
  for (let i = 0; i < tabs.length; i++) {
183
174
  const tab = tabs[i];
184
175
  const current = tab.isCurrentTab() ? " (current)" : "";
185
176
  lines.push(`- ${i}:${current} [${tab.lastTitle()}] (${tab.page.url()})`);
186
177
  }
187
- lines.push("");
188
178
  return lines;
189
179
  }
190
180
  function trim(text, maxLength) {
@@ -192,6 +182,90 @@ function trim(text, maxLength) {
192
182
  return text;
193
183
  return text.slice(0, maxLength) + "...";
194
184
  }
185
+ class RenderedResponse {
186
+ constructor(copy) {
187
+ this.states = {};
188
+ this.updates = [];
189
+ this.results = [];
190
+ this.code = [];
191
+ if (copy) {
192
+ this.states = copy.states;
193
+ this.updates = copy.updates;
194
+ this.results = copy.results;
195
+ this.code = copy.code;
196
+ }
197
+ }
198
+ asText() {
199
+ const text = [];
200
+ if (this.results.length)
201
+ text.push(`### Result
202
+ ${this.results.join("\n")}
203
+ `);
204
+ if (this.code.length)
205
+ text.push(`### Ran Playwright code
206
+ ${this.code.join("\n")}
207
+ `);
208
+ for (const { category, content } of this.updates) {
209
+ if (!content.trim())
210
+ continue;
211
+ switch (category) {
212
+ case "console":
213
+ text.push(`### New console messages
214
+ ${content}
215
+ `);
216
+ break;
217
+ case "downloads":
218
+ text.push(`### Downloads
219
+ ${content}
220
+ `);
221
+ break;
222
+ }
223
+ }
224
+ for (const [category, value] of Object.entries(this.states)) {
225
+ if (!value.trim())
226
+ continue;
227
+ switch (category) {
228
+ case "page":
229
+ text.push(`### Page state
230
+ ${value}
231
+ `);
232
+ break;
233
+ case "tabs":
234
+ text.push(`### Open tabs
235
+ ${value}
236
+ `);
237
+ break;
238
+ case "modal":
239
+ text.push(`### Modal state
240
+ ${value}
241
+ `);
242
+ break;
243
+ }
244
+ }
245
+ return text.join("\n");
246
+ }
247
+ asMeta() {
248
+ const codeUpdate = this.code.length ? { category: "code", content: this.code.join("\n") } : void 0;
249
+ const resultUpdate = this.results.length ? { category: "result", content: this.results.join("\n") } : void 0;
250
+ const updates = [resultUpdate, codeUpdate, ...this.updates].filter(Boolean);
251
+ return {
252
+ "dev.lowire/history": updates,
253
+ "dev.lowire/state": { ...this.states }
254
+ };
255
+ }
256
+ redact(secrets) {
257
+ const redactText = (text) => {
258
+ for (const [secretName, secretValue] of Object.entries(secrets))
259
+ text = text.replaceAll(secretValue, `<secret>${secretName}</secret>`);
260
+ return text;
261
+ };
262
+ const updates = this.updates.map((update) => ({ ...update, content: redactText(update.content) }));
263
+ const results = this.results.map((result) => redactText(result));
264
+ const code = this.code.map((code2) => redactText(code2));
265
+ const states = Object.fromEntries(Object.entries(this.states).map(([key, value]) => [key, redactText(value)]));
266
+ return new RenderedResponse({ states, updates, results, code });
267
+ }
268
+ }
195
269
  function parseSections(text) {
196
270
  const sections = /* @__PURE__ */ new Map();
197
271
  const sectionHeaders = text.split(/^### /m).slice(1);
@@ -229,11 +303,13 @@ function parseResponse(response) {
229
303
  modalState,
230
304
  downloads,
231
305
  isError,
232
- attachments
306
+ attachments,
307
+ _meta: response._meta
233
308
  };
234
309
  }
235
310
  // Annotate the CommonJS export names for ESM import in node:
236
311
  0 && (module.exports = {
312
+ RenderedResponse,
237
313
  Response,
238
314
  parseResponse,
239
315
  requestDebug
@@ -105,9 +105,6 @@ class Tab extends import_events.EventEmitter {
105
105
  clearModalState(modalState) {
106
106
  this._modalStates = this._modalStates.filter((state) => state !== modalState);
107
107
  }
108
- modalStatesMarkdown() {
109
- return renderModalStates(this.context, this.modalStates());
110
- }
111
108
  _dialogShown(dialog) {
112
109
  this.setModalState({
113
110
  type: "dialog",
@@ -176,9 +173,9 @@ class Tab extends import_events.EventEmitter {
176
173
  }
177
174
  await this.waitForLoadState("load", { timeout: 5e3 });
178
175
  }
179
- async consoleMessages(type) {
176
+ async consoleMessages(level) {
180
177
  await this._initializedPromise;
181
- return this._consoleMessages.filter((message) => type ? message.type === type : true);
178
+ return this._consoleMessages.filter((message) => shouldIncludeMessage(level, message.type));
182
179
  }
183
180
  async requests() {
184
181
  await this._initializedPromise;
@@ -200,7 +197,7 @@ class Tab extends import_events.EventEmitter {
200
197
  };
201
198
  });
202
199
  if (tabSnapshot) {
203
- tabSnapshot.consoleMessages = this._recentConsoleMessages;
200
+ tabSnapshot.consoleMessages = this._recentConsoleMessages.filter((message) => shouldIncludeMessage(this.context.config.console.level, message.type));
204
201
  this._recentConsoleMessages = [];
205
202
  }
206
203
  this._needsFullSnapshot = !tabSnapshot;
@@ -282,14 +279,48 @@ function pageErrorToConsoleMessage(errorOrValue) {
282
279
  toString: () => String(errorOrValue)
283
280
  };
284
281
  }
285
- function renderModalStates(context, modalStates) {
286
- const result = ["### Modal state"];
282
+ function renderModalStates(modalStates) {
283
+ const result = [];
287
284
  if (modalStates.length === 0)
288
285
  result.push("- There is no modal state present");
289
286
  for (const state of modalStates)
290
287
  result.push(`- [${state.description}]: can be handled by the "${state.clearedBy}" tool`);
291
288
  return result;
292
289
  }
290
+ const consoleMessageLevels = ["error", "warning", "info", "debug"];
291
+ function shouldIncludeMessage(thresholdLevel, type) {
292
+ const messageLevel = consoleLevelForMessageType(type);
293
+ return consoleMessageLevels.indexOf(messageLevel) <= consoleMessageLevels.indexOf(thresholdLevel);
294
+ }
295
+ function consoleLevelForMessageType(type) {
296
+ switch (type) {
297
+ case "assert":
298
+ case "error":
299
+ return "error";
300
+ case "warning":
301
+ return "warning";
302
+ case "count":
303
+ case "dir":
304
+ case "dirxml":
305
+ case "info":
306
+ case "log":
307
+ case "table":
308
+ case "time":
309
+ case "timeEnd":
310
+ return "info";
311
+ case "clear":
312
+ case "debug":
313
+ case "endGroup":
314
+ case "profile":
315
+ case "profileEnd":
316
+ case "startGroup":
317
+ case "startGroupCollapsed":
318
+ case "trace":
319
+ return "debug";
320
+ default:
321
+ return "info";
322
+ }
323
+ }
293
324
  const tabSymbol = Symbol("tabSymbol");
294
325
  // Annotate the CommonJS export names for ESM import in node:
295
326
  0 && (module.exports = {