stably 4.12.8 → 4.12.9
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.
- package/dist/index.mjs +1 -1
- package/dist/node_modules/playwright/lib/cli/daemon/daemon.js +111 -0
- package/dist/node_modules/playwright/lib/index.js +32 -1
- package/dist/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +27 -0
- package/dist/node_modules/playwright/lib/mcp/browser/config.js.rej +11 -0
- package/dist/node_modules/playwright/lib/mcp/browser/context.js +52 -0
- package/dist/node_modules/playwright/lib/mcp/browser/sessionLog.js +50 -0
- package/dist/node_modules/playwright/lib/mcp/test/browserBackend.js +6 -1
- package/package.json +1 -1
|
@@ -75,6 +75,117 @@ async function startMcpDaemonServer(config, contextFactory) {
|
|
|
75
75
|
timestamp: Date.now()
|
|
76
76
|
};
|
|
77
77
|
const { browserContext, close } = await contextFactory.createContext(clientInfo, new AbortController().signal, {});
|
|
78
|
+
// For CDP connections: resize the browser window to match the user's configured viewport.
|
|
79
|
+
// We only use Browser.setWindowBounds (no setViewportSize) so the page viewport is
|
|
80
|
+
// determined naturally by the window's content area. Using setViewportSize would trigger
|
|
81
|
+
// Emulation.setDeviceMetricsOverride which creates a device emulation frame with white
|
|
82
|
+
// borders inside the window — the page renders at the viewport size but doesn't fill
|
|
83
|
+
// the window's content area.
|
|
84
|
+
if (config.browser.cdpEndpoint) {
|
|
85
|
+
try {
|
|
86
|
+
// Pages may not be immediately available after CDP connection — wait briefly for discovery.
|
|
87
|
+
let page = browserContext.pages()[0];
|
|
88
|
+
if (!page) {
|
|
89
|
+
daemonDebug("no pages found, waiting for page discovery...");
|
|
90
|
+
page = await Promise.race([
|
|
91
|
+
new Promise(resolve => browserContext.once("page", resolve)),
|
|
92
|
+
new Promise(resolve => setTimeout(() => resolve(null), 5000))
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
if (page) {
|
|
96
|
+
daemonDebug("found page for viewport resize:", page.url());
|
|
97
|
+
// Clear any stale device emulation from a prior session's setViewportSize() calls.
|
|
98
|
+
try {
|
|
99
|
+
const clearSession = await browserContext.newCDPSession(page);
|
|
100
|
+
await clearSession.send("Emulation.clearDeviceMetricsOverride").catch(() => {});
|
|
101
|
+
await clearSession.detach();
|
|
102
|
+
daemonDebug("cleared stale device emulation");
|
|
103
|
+
} catch (_) {}
|
|
104
|
+
// Try to load viewport from the user's playwright.config (most authoritative source),
|
|
105
|
+
// then fall back to config.browser.contextOptions.viewport (which may just be the 1280x720 default).
|
|
106
|
+
let viewport = null;
|
|
107
|
+
if (config.playwrightConfigPath) {
|
|
108
|
+
try {
|
|
109
|
+
const { requireOrImport } = require("../../transform/transform");
|
|
110
|
+
let userConfig = await requireOrImport(config.playwrightConfigPath);
|
|
111
|
+
if (userConfig && typeof userConfig === "object" && "default" in userConfig)
|
|
112
|
+
userConfig = userConfig.default;
|
|
113
|
+
viewport = userConfig?.use?.viewport || userConfig?.projects?.[0]?.use?.viewport;
|
|
114
|
+
daemonDebug("extracted viewport from playwright config:", viewport);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
daemonDebug("failed to load playwright config for viewport", e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Fall back to config viewport if playwright.config didn't provide one
|
|
120
|
+
if (!viewport) {
|
|
121
|
+
viewport = config.browser.contextOptions?.viewport;
|
|
122
|
+
}
|
|
123
|
+
const cdpSession = await browserContext.newCDPSession(page);
|
|
124
|
+
const { windowId } = await cdpSession.send("Browser.getWindowForTarget");
|
|
125
|
+
const vp = viewport || { width: 1280, height: 720 };
|
|
126
|
+
// Get current window state and measure chrome decoration overhead.
|
|
127
|
+
const { bounds: currentBounds } = await cdpSession.send("Browser.getWindowBounds", { windowId });
|
|
128
|
+
daemonDebug("current window state:", currentBounds.windowState, "bounds:", currentBounds.width, "x", currentBounds.height);
|
|
129
|
+
// Get screen size to know if the desired viewport can fit.
|
|
130
|
+
const { result: screenSize } = await cdpSession.send("Runtime.evaluate", {
|
|
131
|
+
expression: "JSON.stringify({ width: screen.width, height: screen.height })",
|
|
132
|
+
returnByValue: true
|
|
133
|
+
});
|
|
134
|
+
const screen = JSON.parse(screenSize.value);
|
|
135
|
+
daemonDebug("screen size:", screen.width, "x", screen.height);
|
|
136
|
+
// Quick check: if viewport won't fit with decorations (~100px width, ~200px height),
|
|
137
|
+
// skip measurement and go straight to maximize + device emulation.
|
|
138
|
+
if (vp.width + 100 > screen.width || vp.height + 200 > screen.height) {
|
|
139
|
+
daemonDebug("viewport", vp.width, "x", vp.height, "exceeds screen, using maximize + device emulation");
|
|
140
|
+
// Chrome requires fullscreen → normal → maximized (can't go directly fullscreen → maximized).
|
|
141
|
+
if (currentBounds.windowState === "fullscreen") {
|
|
142
|
+
await cdpSession.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "normal" } });
|
|
143
|
+
}
|
|
144
|
+
await cdpSession.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "maximized" } });
|
|
145
|
+
// Use Emulation.setDeviceMetricsOverride directly instead of page.setViewportSize()
|
|
146
|
+
// because setViewportSize also resizes the window, pushing it off-screen and hiding
|
|
147
|
+
// the tab bar and address bar.
|
|
148
|
+
await cdpSession.send("Emulation.setDeviceMetricsOverride", {
|
|
149
|
+
width: vp.width, height: vp.height, deviceScaleFactor: 0, mobile: false
|
|
150
|
+
});
|
|
151
|
+
daemonDebug("device emulation applied:", vp.width, "x", vp.height);
|
|
152
|
+
} else {
|
|
153
|
+
// Viewport fits — un-maximize, measure decorations, resize window exactly.
|
|
154
|
+
if (currentBounds.windowState === "maximized" || currentBounds.windowState === "fullscreen") {
|
|
155
|
+
await cdpSession.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "normal" } });
|
|
156
|
+
daemonDebug("un-maximized window for measurement");
|
|
157
|
+
}
|
|
158
|
+
const { bounds: measuredBounds } = await cdpSession.send("Browser.getWindowBounds", { windowId });
|
|
159
|
+
const { result: innerSize } = await cdpSession.send("Runtime.evaluate", {
|
|
160
|
+
expression: "JSON.stringify({ width: window.innerWidth, height: window.innerHeight })",
|
|
161
|
+
returnByValue: true
|
|
162
|
+
});
|
|
163
|
+
const inner = JSON.parse(innerSize.value);
|
|
164
|
+
const decorationWidth = measuredBounds.width - inner.width;
|
|
165
|
+
const decorationHeight = measuredBounds.height - inner.height;
|
|
166
|
+
daemonDebug("measured chrome decorations:", decorationWidth, "x", decorationHeight, "(window:", measuredBounds.width, "x", measuredBounds.height, "inner:", inner.width, "x", inner.height, ")");
|
|
167
|
+
// Sanity check: if decorations are negative, measurement is corrupted — skip resize.
|
|
168
|
+
if (decorationWidth < 0 || decorationHeight < 0) {
|
|
169
|
+
daemonDebug("negative decorations detected, skipping resize to avoid progressive shrink");
|
|
170
|
+
} else {
|
|
171
|
+
const targetWidth = vp.width + decorationWidth;
|
|
172
|
+
const targetHeight = vp.height + decorationHeight;
|
|
173
|
+
daemonDebug("resizing window to:", targetWidth, "x", targetHeight, "for viewport:", vp.width, "x", vp.height);
|
|
174
|
+
await cdpSession.send("Browser.setWindowBounds", {
|
|
175
|
+
windowId,
|
|
176
|
+
bounds: { width: targetWidth, height: targetHeight }
|
|
177
|
+
});
|
|
178
|
+
daemonDebug("window resize succeeded");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
await cdpSession.detach();
|
|
182
|
+
} else {
|
|
183
|
+
daemonDebug("no page available for viewport resize after waiting");
|
|
184
|
+
}
|
|
185
|
+
} catch (e) {
|
|
186
|
+
daemonDebug("failed to resize window for CDP connection (best-effort)", e);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
78
189
|
browserContext.on("close", () => {
|
|
79
190
|
daemonDebug("browser closed, shutting down daemon");
|
|
80
191
|
shutdown(0);
|
|
@@ -92,6 +92,21 @@ const playwrightFixtures = {
|
|
|
92
92
|
browser: [async ({ playwright, browserName, _browserOptions, connectOptions }, use) => {
|
|
93
93
|
if (!["chromium", "firefox", "webkit"].includes(browserName))
|
|
94
94
|
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
|
|
95
|
+
// CDP endpoint support via environment variable or connectOptions with /browser-cdp/ path
|
|
96
|
+
const cdpEndpoint = process.env.PLAYWRIGHT_CDP_ENDPOINT;
|
|
97
|
+
const connectWsEndpoint = connectOptions == null ? void 0 : connectOptions.wsEndpoint;
|
|
98
|
+
const isCdpWsEndpoint = typeof connectWsEndpoint === "string" && (connectWsEndpoint.includes("/browser-cdp/") || connectWsEndpoint.includes("/browser/cdp"));
|
|
99
|
+
if (cdpEndpoint || isCdpWsEndpoint) {
|
|
100
|
+
if (browserName !== "chromium")
|
|
101
|
+
throw new Error("CDP endpoint is only supported for Chromium browsers");
|
|
102
|
+
const endpoint = cdpEndpoint || connectWsEndpoint;
|
|
103
|
+
const cdpHeadersFromEnv = process.env.PLAYWRIGHT_CDP_HEADERS ? JSON.parse(process.env.PLAYWRIGHT_CDP_HEADERS) : void 0;
|
|
104
|
+
const browser2 = await playwright.chromium.connectOverCDP(endpoint, {
|
|
105
|
+
headers: cdpHeadersFromEnv || (connectOptions == null ? void 0 : connectOptions.headers)
|
|
106
|
+
});
|
|
107
|
+
await use(browser2);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
95
110
|
if (connectOptions) {
|
|
96
111
|
const browser2 = await playwright[browserName].connect({
|
|
97
112
|
...connectOptions,
|
|
@@ -335,7 +350,13 @@ const playwrightFixtures = {
|
|
|
335
350
|
size: typeof video === "string" ? void 0 : video.size
|
|
336
351
|
}
|
|
337
352
|
} : {};
|
|
338
|
-
|
|
353
|
+
// When connected via CDP (run-test), reuse the default context instead of
|
|
354
|
+
// creating a new one. This preserves browser state (pages, cookies, etc.)
|
|
355
|
+
// after the test completes, allowing the daemon to interact with the result.
|
|
356
|
+
const isCdpSession = !!process.env.PLAYWRIGHT_CDP_ENDPOINT;
|
|
357
|
+
const context = isCdpSession && browser.contexts().length > 0
|
|
358
|
+
? browser.contexts()[0]
|
|
359
|
+
: await browser.newContext({ ...videoOptions, ...options });
|
|
339
360
|
if (process.env.PW_CLOCK === "frozen") {
|
|
340
361
|
await context._wrapApiCall(async () => {
|
|
341
362
|
await context.clock.install({ time: 0 });
|
|
@@ -351,6 +372,10 @@ const playwrightFixtures = {
|
|
|
351
372
|
if (closed)
|
|
352
373
|
return;
|
|
353
374
|
closed = true;
|
|
375
|
+
// Skip context.close() when connected via CDP — preserve browser state
|
|
376
|
+
// so the daemon can continue interacting with the test's pages.
|
|
377
|
+
if (isCdpSession)
|
|
378
|
+
return;
|
|
354
379
|
const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
|
|
355
380
|
await context.close({ reason: closeReason });
|
|
356
381
|
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
|
@@ -403,6 +428,12 @@ const playwrightFixtures = {
|
|
|
403
428
|
await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true });
|
|
404
429
|
},
|
|
405
430
|
page: async ({ context, _reuseContext }, use) => {
|
|
431
|
+
// When connected via CDP, reuse the existing page (the daemon's page)
|
|
432
|
+
// instead of creating a new one, so the daemon sees the test's navigation.
|
|
433
|
+
if (process.env.PLAYWRIGHT_CDP_ENDPOINT && context.pages().length > 0) {
|
|
434
|
+
await use(context.pages()[0]);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
406
437
|
if (!_reuseContext) {
|
|
407
438
|
await use(await context.newPage());
|
|
408
439
|
return;
|
|
@@ -27,6 +27,22 @@ var import_response = require("./response");
|
|
|
27
27
|
var import_sessionLog = require("./sessionLog");
|
|
28
28
|
var import_tools = require("./tools");
|
|
29
29
|
var import_tool = require("../sdk/tool");
|
|
30
|
+
var import_net = require("net");
|
|
31
|
+
function sendRecorderEvent(event) {
|
|
32
|
+
const port = process.env.CLAUDE_RECORDER_PORT;
|
|
33
|
+
if (!port)
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
const socket = import_net.createConnection({
|
|
37
|
+
port: Number(port),
|
|
38
|
+
host: process.env.CLAUDE_RECORDER_HOST || "127.0.0.1"
|
|
39
|
+
});
|
|
40
|
+
const payload = JSON.stringify(event) + "\n";
|
|
41
|
+
socket.on("error", () => socket.destroy());
|
|
42
|
+
socket.write(payload, () => socket.end());
|
|
43
|
+
} catch (error) {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
30
46
|
class BrowserServerBackend {
|
|
31
47
|
constructor(config, factory, options = {}) {
|
|
32
48
|
this._config = config;
|
|
@@ -64,6 +80,17 @@ Tool "${name}" not found` }],
|
|
|
64
80
|
await tool.handle(context, parsedArguments, response);
|
|
65
81
|
responseObject = await response.serialize();
|
|
66
82
|
this._sessionLog?.logResponse(name, parsedArguments, responseObject);
|
|
83
|
+
// Send recorder event for user action tracking
|
|
84
|
+
const parsed = (0, import_response.parseResponse)(responseObject);
|
|
85
|
+
if (parsed && parsed.code) {
|
|
86
|
+
sendRecorderEvent({
|
|
87
|
+
type: "user-action",
|
|
88
|
+
action: { name: name, args: parsedArguments },
|
|
89
|
+
code: parsed.code,
|
|
90
|
+
url: this._context?.currentTab()?.page?.url() || "",
|
|
91
|
+
timestamp: Date.now()
|
|
92
|
+
});
|
|
93
|
+
}
|
|
67
94
|
} catch (error) {
|
|
68
95
|
return {
|
|
69
96
|
content: [{ type: "text", text: `### Error
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
--- lib/mcp/browser/config.js
|
|
2
|
+
+++ lib/mcp/browser/config.js
|
|
3
|
+
@@ -305,7 +305,7 @@
|
|
4
|
+
function outputDir(config, clientInfo) {
|
|
5
|
+
if (config.outputDir)
|
|
6
|
+
return import_path.default.resolve(config.outputDir);
|
|
7
|
+
const rootPath = (0, import_server2.firstRootPath)(clientInfo);
|
|
8
|
+
if (rootPath)
|
|
9
|
+
- return import_path.default.resolve(rootPath, config.skillMode ? ".playwright-cli" : ".playwright-mcp");
|
|
10
|
+
+ return import_path.default.resolve(rootPath, config.skillMode ? ".stably-browser" : ".playwright-mcp");
|
|
11
|
+
const tmpDir = process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir();
|
|
@@ -40,6 +40,55 @@ var import_log = require("../log");
|
|
|
40
40
|
var import_tab = require("./tab");
|
|
41
41
|
var import_config = require("./config");
|
|
42
42
|
const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
|
|
43
|
+
class InputRecorder {
|
|
44
|
+
constructor(context, browserContext) {
|
|
45
|
+
this._context = context;
|
|
46
|
+
this._browserContext = browserContext;
|
|
47
|
+
}
|
|
48
|
+
static async create(context, browserContext) {
|
|
49
|
+
const recorder = new InputRecorder(context, browserContext);
|
|
50
|
+
await recorder._initialize();
|
|
51
|
+
return recorder;
|
|
52
|
+
}
|
|
53
|
+
async _initialize() {
|
|
54
|
+
const sessionLog = this._context.sessionLog;
|
|
55
|
+
if (!sessionLog) return;
|
|
56
|
+
await this._browserContext._enableRecorder({
|
|
57
|
+
mode: "recording",
|
|
58
|
+
recorderMode: "api"
|
|
59
|
+
}, {
|
|
60
|
+
actionAdded: (page, data, code) => {
|
|
61
|
+
if (this._context.isRunningTool())
|
|
62
|
+
return;
|
|
63
|
+
const tab = import_tab.Tab.forPage(page);
|
|
64
|
+
if (tab)
|
|
65
|
+
sessionLog.logUserAction(data.action, tab, code, false);
|
|
66
|
+
},
|
|
67
|
+
actionUpdated: (page, data, code) => {
|
|
68
|
+
if (this._context.isRunningTool())
|
|
69
|
+
return;
|
|
70
|
+
const tab = import_tab.Tab.forPage(page);
|
|
71
|
+
if (tab)
|
|
72
|
+
sessionLog.logUserAction(data.action, tab, code, true);
|
|
73
|
+
},
|
|
74
|
+
signalAdded: (page, data) => {
|
|
75
|
+
if (this._context.isRunningTool())
|
|
76
|
+
return;
|
|
77
|
+
if (data.signal.name !== "navigation")
|
|
78
|
+
return;
|
|
79
|
+
const tab = import_tab.Tab.forPage(page);
|
|
80
|
+
const navigateAction = {
|
|
81
|
+
name: "navigate",
|
|
82
|
+
url: data.signal.url,
|
|
83
|
+
signals: []
|
|
84
|
+
};
|
|
85
|
+
if (tab)
|
|
86
|
+
sessionLog.logUserAction(navigateAction, tab, `await page.goto('${data.signal.url}');`, false);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
43
92
|
class Context {
|
|
44
93
|
constructor(options) {
|
|
45
94
|
this._tabs = [];
|
|
@@ -251,6 +300,9 @@ class Context {
|
|
|
251
300
|
_live: true
|
|
252
301
|
});
|
|
253
302
|
}
|
|
303
|
+
// Initialize InputRecorder for user action capture
|
|
304
|
+
if (this.sessionLog)
|
|
305
|
+
await InputRecorder.create(this, browserContext);
|
|
254
306
|
return result;
|
|
255
307
|
}
|
|
256
308
|
lookupSecret(secretName) {
|
|
@@ -33,8 +33,24 @@ __export(sessionLog_exports, {
|
|
|
33
33
|
module.exports = __toCommonJS(sessionLog_exports);
|
|
34
34
|
var import_fs = __toESM(require("fs"));
|
|
35
35
|
var import_path = __toESM(require("path"));
|
|
36
|
+
var import_net = require("net");
|
|
36
37
|
var import_config = require("./config");
|
|
37
38
|
var import_response = require("./response");
|
|
39
|
+
function sendRecorderEvent(event) {
|
|
40
|
+
const port = process.env.CLAUDE_RECORDER_PORT;
|
|
41
|
+
if (!port)
|
|
42
|
+
return;
|
|
43
|
+
try {
|
|
44
|
+
const socket = import_net.createConnection({
|
|
45
|
+
port: Number(port),
|
|
46
|
+
host: process.env.CLAUDE_RECORDER_HOST || "127.0.0.1"
|
|
47
|
+
});
|
|
48
|
+
const payload = JSON.stringify(event) + "\n";
|
|
49
|
+
socket.on("error", () => socket.destroy());
|
|
50
|
+
socket.write(payload, () => socket.end());
|
|
51
|
+
} catch (error) {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
38
54
|
class SessionLog {
|
|
39
55
|
constructor(sessionFolder) {
|
|
40
56
|
this._sessionFileQueue = Promise.resolve();
|
|
@@ -68,6 +84,40 @@ class SessionLog {
|
|
|
68
84
|
lines.push("");
|
|
69
85
|
this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
|
|
70
86
|
}
|
|
87
|
+
logUserAction(action, tab, code, isUpdate) {
|
|
88
|
+
code = code.trim();
|
|
89
|
+
// Send recorder event for user action tracking
|
|
90
|
+
const actionForLog = { ...action };
|
|
91
|
+
delete actionForLog.ariaSnapshot;
|
|
92
|
+
delete actionForLog.selector;
|
|
93
|
+
actionForLog.isUpdate = !!isUpdate;
|
|
94
|
+
sendRecorderEvent({
|
|
95
|
+
type: "user-action",
|
|
96
|
+
action: actionForLog,
|
|
97
|
+
code,
|
|
98
|
+
url: tab.page.url(),
|
|
99
|
+
timestamp: Date.now()
|
|
100
|
+
});
|
|
101
|
+
// Also log to session file
|
|
102
|
+
const lines = [""];
|
|
103
|
+
lines.push(
|
|
104
|
+
`### User action: ${action.name}`,
|
|
105
|
+
"- Args",
|
|
106
|
+
"```json",
|
|
107
|
+
JSON.stringify(actionForLog, null, 2),
|
|
108
|
+
"```"
|
|
109
|
+
);
|
|
110
|
+
if (code) {
|
|
111
|
+
lines.push(
|
|
112
|
+
"- Code",
|
|
113
|
+
"```js",
|
|
114
|
+
code,
|
|
115
|
+
"```"
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
lines.push("");
|
|
119
|
+
this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
|
|
120
|
+
}
|
|
71
121
|
}
|
|
72
122
|
// Annotate the CommonJS export names for ESM import in node:
|
|
73
123
|
0 && (module.exports = {
|
|
@@ -32,9 +32,14 @@ function createCustomMessageHandler(testInfo, context) {
|
|
|
32
32
|
if (data.initialize) {
|
|
33
33
|
if (backend)
|
|
34
34
|
throw new Error("MCP backend is already initialized");
|
|
35
|
-
backend = new import_browserServerBackend.BrowserServerBackend({ ...import_config.defaultConfig, capabilities: ["testing"] }, (0, import_browserContextFactory.identityBrowserContextFactory)(context));
|
|
35
|
+
backend = new import_browserServerBackend.BrowserServerBackend({ ...import_config.defaultConfig, capabilities: ["testing"], saveSession: true }, (0, import_browserContextFactory.identityBrowserContextFactory)(context));
|
|
36
36
|
await backend.initialize(data.initialize.clientInfo);
|
|
37
37
|
const pausedMessage = await generatePausedMessage(testInfo, context);
|
|
38
|
+
// Ensure recorder UI appears immediately by triggering a benign action that doesn't change page state.
|
|
39
|
+
try {
|
|
40
|
+
await backend.callTool("browser_tabs", { action: "list" });
|
|
41
|
+
} catch (e) {
|
|
42
|
+
}
|
|
38
43
|
return { initialize: { pausedMessage } };
|
|
39
44
|
}
|
|
40
45
|
if (data.listTools) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stably",
|
|
3
|
-
"version": "4.12.
|
|
3
|
+
"version": "4.12.9",
|
|
4
4
|
"packageManager": "pnpm@10.24.0",
|
|
5
5
|
"description": "AI-powered E2E Playwright testing CLI. Stably can understand your codebase, edit/run tests, and handle complex test scenarios for you.",
|
|
6
6
|
"main": "dist/index.mjs",
|