diff --git a/packages/playwright-core/src/tools/cli-client/program.ts b/packages/playwright-core/src/tools/cli-client/program.ts index 0f8d900c05ede..e15a0e3654c94 100644 --- a/packages/playwright-core/src/tools/cli-client/program.ts +++ b/packages/playwright-core/src/tools/cli-client/program.ts @@ -218,10 +218,12 @@ export async function program(options?: { embedderVersion?: string}) { return; } if (args.annotate) { - const dashboard = spawn(process.execPath, daemonArgs, { detached: true, stdio: 'ignore' }); - dashboard.unref(); - const annotate = spawn(process.execPath, [...daemonArgs, '--annotate'], { stdio: 'inherit' }); - await new Promise(resolve => annotate.on('exit', () => resolve())); + const entry = registry.entry(clientInfo, sessionName); + if (!entry) + output.errorBrowserNotOpenForTool(sessionName); + args.raw = true; + const text = await runInSession(entry, clientInfo, args, output); + output.toolResult(text); return; } const foreground = args.port !== undefined; diff --git a/packages/playwright-core/src/tools/cli-client/skill/SKILL.md b/packages/playwright-core/src/tools/cli-client/skill/SKILL.md index f9bc753e0682a..f034c324bd242 100644 --- a/packages/playwright-core/src/tools/cli-client/skill/SKILL.md +++ b/packages/playwright-core/src/tools/cli-client/skill/SKILL.md @@ -163,7 +163,7 @@ playwright-cli video-start video.webm playwright-cli video-chapter "Chapter Title" --description="Details" --duration=2000 playwright-cli video-stop -# launch the dashboard with annotation prompt to ask the user for input +# launch the dashboard for UI review / design feedback — user annotates the page, you receive the annotated screenshot, snapshot, and notes playwright-cli show --annotate # generate a Playwright locator for an element from its ref or selector @@ -367,7 +367,7 @@ playwright-cli close ## Example: Interactive session -Ask the user to annotate the UI. User can provide contextual tasks or ask contextual questions using annotations: +Ask the user for UI review or design feedback. The user draws boxes on the live page and types comments; you receive the annotated screenshot, the snapshot of the marked region, and the user's notes. Use this whenever the user asks for "UI review", "design feedback", or to "ask the user what they think / want / mean": ```bash playwright-cli open https://example.com diff --git a/packages/playwright-core/src/tools/cli-daemon/commands.ts b/packages/playwright-core/src/tools/cli-daemon/commands.ts index b3dff96875493..188136f1eef9c 100644 --- a/packages/playwright-core/src/tools/cli-daemon/commands.ts +++ b/packages/playwright-core/src/tools/cli-daemon/commands.ts @@ -973,6 +973,7 @@ const dashboardShow = declareCommand({ name: 'show', description: 'Show Playwright Dashboard', category: 'devtools', + raw: true, args: z.object({}), options: z.object({ port: numberArg.optional().describe('Start as a blocking HTTP server on this port (use 0 for a random port)'), @@ -980,7 +981,7 @@ const dashboardShow = declareCommand({ annotate: z.boolean().optional().describe('Switch the dashboard into annotation mode.'), kill: z.boolean().optional().describe('Kill the dashboard daemon.'), }), - toolName: '', + toolName: ({ annotate }) => annotate ? 'browser_annotate' : '', toolParams: () => ({}), }); diff --git a/packages/playwright-core/src/tools/cli-daemon/daemon.ts b/packages/playwright-core/src/tools/cli-daemon/daemon.ts index 84c61546106ee..61f576a02fc95 100644 --- a/packages/playwright-core/src/tools/cli-daemon/daemon.ts +++ b/packages/playwright-core/src/tools/cli-daemon/daemon.ts @@ -78,6 +78,8 @@ export async function startCliDaemonServer( const server = net.createServer(socket => { const connection = new SocketConnection(socket); + const abortController = new AbortController(); + connection.onclose = () => abortController.abort(); connection.onmessage = async message => { const { id, method, params } = message; try { @@ -91,7 +93,7 @@ export async function startCliDaemonServer( } else if (method === 'run') { const { toolName, toolParams } = parseCliCommand(params.args); toolParams._meta = { cwd: params.cwd, raw: params.raw || params.json, json: !!params.json }; - const response = await backend.callTool(toolName, toolParams); + const response = await backend.callTool(toolName, toolParams, abortController.signal); await connection.send({ id, result: formatResult(response) }); } else { throw new Error(`Unknown method: ${method}`); diff --git a/tests/mcp/dashboard.spec.ts b/tests/mcp/dashboard.spec.ts index 9016a6d3341c0..4f3887a5e5ec0 100644 --- a/tests/mcp/dashboard.spec.ts +++ b/tests/mcp/dashboard.spec.ts @@ -153,11 +153,10 @@ async function drawAndSubmitAnnotation(dashboard: import('playwright-core').Page } function verifyAnnotateOutput(output: string, expectedText: string, outputDir: string) { - const lines = output.trim().split('\n'); - expect(lines[0]).toMatch(new RegExp(`^\\{ x: \\d+, y: \\d+, width: \\d+, height: \\d+ \\}: ${expectedText}$`)); - expect(lines[lines.length - 1]).toMatch(/^image: \.playwright-cli[\\/]annotations-.*\.png$/); - const pngRel = lines[lines.length - 1].replace(/^image: /, ''); - const pngPath = path.resolve(outputDir, pngRel); + expect(output).toMatch(new RegExp(`\\{ x: \\d+, y: \\d+, width: \\d+, height: \\d+ \\}: ${expectedText}`)); + const imageMatch = output.match(/- \[Annotation image\]\((\.playwright-cli[\\/]annotations-.*\.png)\)/); + expect(imageMatch).not.toBeNull(); + const pngPath = path.resolve(outputDir, imageMatch![1]); expect(fs.existsSync(pngPath)).toBe(true); expect(fs.statSync(pngPath).size).toBeGreaterThan(0); } @@ -184,10 +183,10 @@ test('should capture annotations via show --annotate', async ({ connectToDashboa }); test('should start dashboard and annotate when no dashboard is running', async ({ connectToDashboard, cli, server }) => { - await cli('open', server.EMPTY_PAGE); - const bindTitle = `--playwright-internal--${crypto.randomUUID()}`; - const annotatePromise = cli('show', '--annotate', { bindTitle }); + await cli('open', server.EMPTY_PAGE, { bindTitle }); + + const annotatePromise = cli('show', '--annotate'); let done = false; void annotatePromise.finally(() => { done = true; }); @@ -205,12 +204,12 @@ test('should start dashboard and annotate when no dashboard is running', async ( verifyAnnotateOutput(output, 'hi', test.info().outputDir); }); -test('should enter annotate mode on fresh dashboard.tsx mount with annotate', async ({ connectToDashboard, cli, server }) => { - await cli('-s=first', 'open', server.EMPTY_PAGE); - await cli('-s=second', 'open', server.EMPTY_PAGE); - +test('should enter annotate mode on fresh dashboard.tsx mount with -s --annotate', async ({ connectToDashboard, cli, server }) => { const bindTitle = `--playwright-internal--${crypto.randomUUID()}`; - const annotatePromise = cli('-s=second', 'show', '--annotate', { bindTitle }); + await cli('-s=first', 'open', server.EMPTY_PAGE, { bindTitle }); + await cli('-s=second', 'open', server.EMPTY_PAGE, { bindTitle }); + + const annotatePromise = cli('-s=second', 'show', '--annotate'); let done = false; void annotatePromise.finally(() => { done = true; });