feat: add screenshot output config, clipboard CLI commands, and fix wait --text native path (#749)
* feat: add screenshot output config, clipboard CLI commands, and fix wait --text native path
## Summary
- Add `--screenshot-dir`, `--screenshot-quality`, and `--screenshot-format` CLI flags (with corresponding `AGENT_BROWSER_SCREENSHOT_DIR`, `AGENT_BROWSER_SCREENSHOT_QUALITY`, `AGENT_BROWSER_SCREENSHOT_FORMAT` env vars) so users can configure where and how screenshots are saved without specifying a full path every time
- Add `clipboard read`, `clipboard write <text>`, `clipboard copy`, and `clipboard paste` CLI commands, exposing the existing protocol-level clipboard handlers that were previously only accessible via JSON-RPC
- Fix `wait --text` in native mode: the CLI was emitting `selector: "text=..."` (a Playwright-style locator) which native's `querySelector` can't handle. Now emits a `text` field that correctly hits the native `wait_for_text` polling path
- Add native clipboard `copy` and `paste` support via CDP `Input.dispatchKeyEvent`, and a `write` operation to the Node.js handler
* fix: resolve CI failures in Rust formatting and TypeScript typecheck
Use string-based page.evaluate for clipboard writeText to avoid
referencing `navigator` in Node.js compilation context. Run cargo fmt
to fix formatting in commands.rs and screenshot.rs.
* fix: clipboard write captures full multi-word text
Use rest[1..].join(" ") instead of rest.get(1) so unquoted multi-word
input like `clipboard write hello world` sends the full string rather
than silently dropping everything after the first word.
* improvements
* fixes
* improvements
* improvements
This commit is contained in:
+19
-6
@@ -741,7 +741,7 @@ async function handleScreenshot(
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
const filename = `screenshot-${timestamp}-${random}.${ext}`;
|
||||
const screenshotDir = path.join(getAppDir(), 'tmp', 'screenshots');
|
||||
const screenshotDir = command.screenshotDir ?? path.join(getAppDir(), 'tmp', 'screenshots');
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
savePath = path.join(screenshotDir, filename);
|
||||
}
|
||||
@@ -954,7 +954,13 @@ async function handleEvaluate(
|
||||
async function handleWait(command: WaitCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
|
||||
if (command.selector) {
|
||||
if (command.text) {
|
||||
await page.waitForFunction(
|
||||
(t: string) => (document.body.innerText || '').includes(t),
|
||||
command.text,
|
||||
{ timeout: command.timeout }
|
||||
);
|
||||
} else if (command.selector) {
|
||||
await page.waitForSelector(command.selector, {
|
||||
state: command.state ?? 'visible',
|
||||
timeout: command.timeout,
|
||||
@@ -962,7 +968,6 @@ async function handleWait(command: WaitCommand, browser: BrowserManager): Promis
|
||||
} else if (command.timeout) {
|
||||
await page.waitForTimeout(command.timeout);
|
||||
} else {
|
||||
// Default: wait for load state
|
||||
await page.waitForLoadState('load');
|
||||
}
|
||||
|
||||
@@ -2119,14 +2124,22 @@ async function handleClipboard(
|
||||
|
||||
switch (command.operation) {
|
||||
case 'copy':
|
||||
await page.keyboard.press('Control+c');
|
||||
await page.keyboard.press('ControlOrMeta+c');
|
||||
return successResponse(command.id, { copied: true });
|
||||
case 'paste':
|
||||
await page.keyboard.press('Control+v');
|
||||
await page.keyboard.press('ControlOrMeta+v');
|
||||
return successResponse(command.id, { pasted: true });
|
||||
case 'read':
|
||||
case 'read': {
|
||||
const text = await page.evaluate('navigator.clipboard.readText()');
|
||||
return successResponse(command.id, { text });
|
||||
}
|
||||
case 'write': {
|
||||
if (!command.text) {
|
||||
return errorResponse(command.id, "Missing 'text' parameter for clipboard write");
|
||||
}
|
||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(command.text)})`);
|
||||
return successResponse(command.id, { written: command.text });
|
||||
}
|
||||
default:
|
||||
return errorResponse(command.id, 'Unknown clipboard operation');
|
||||
}
|
||||
|
||||
+3
-1
@@ -468,7 +468,7 @@ const tapSchema = baseCommandSchema.extend({
|
||||
|
||||
const clipboardSchema = baseCommandSchema.extend({
|
||||
action: z.literal('clipboard'),
|
||||
operation: z.enum(['copy', 'paste', 'read']),
|
||||
operation: z.enum(['copy', 'paste', 'read', 'write']),
|
||||
text: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -794,6 +794,7 @@ const screenshotSchema = baseCommandSchema.extend({
|
||||
format: z.enum(['png', 'jpeg']).optional(),
|
||||
quality: z.number().min(0).max(100).optional(),
|
||||
annotate: z.boolean().optional(),
|
||||
screenshotDir: z.string().optional(),
|
||||
});
|
||||
|
||||
const snapshotSchema = baseCommandSchema.extend({
|
||||
@@ -814,6 +815,7 @@ const evaluateSchema = baseCommandSchema.extend({
|
||||
const waitSchema = baseCommandSchema.extend({
|
||||
action: z.literal('wait'),
|
||||
selector: z.string().min(1).optional(),
|
||||
text: z.string().min(1).optional(),
|
||||
timeout: z.number().positive().optional(),
|
||||
state: z.enum(['attached', 'detached', 'visible', 'hidden']).optional(),
|
||||
});
|
||||
|
||||
+3
-1
@@ -707,7 +707,7 @@ export interface TapCommand extends BaseCommand {
|
||||
// Clipboard
|
||||
export interface ClipboardCommand extends BaseCommand {
|
||||
action: 'clipboard';
|
||||
operation: 'copy' | 'paste' | 'read';
|
||||
operation: 'copy' | 'paste' | 'read' | 'write';
|
||||
text?: string;
|
||||
}
|
||||
|
||||
@@ -826,6 +826,7 @@ export interface ScreenshotCommand extends BaseCommand {
|
||||
format?: 'png' | 'jpeg';
|
||||
quality?: number;
|
||||
annotate?: boolean;
|
||||
screenshotDir?: string;
|
||||
}
|
||||
|
||||
export interface SnapshotCommand extends BaseCommand {
|
||||
@@ -841,6 +842,7 @@ export interface EvaluateCommand extends BaseCommand {
|
||||
export interface WaitCommand extends BaseCommand {
|
||||
action: 'wait';
|
||||
selector?: string;
|
||||
text?: string;
|
||||
timeout?: number;
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user