v0.14.0 changeset (#534)

* v0.14.0 changeset

* fixes

* improvements
This commit is contained in:
Chris Tate
2026-02-23 10:48:07 -06:00
committed by GitHub
parent 16c4ef2da6
commit b7665e52b6
10 changed files with 193 additions and 12 deletions
+10
View File
@@ -0,0 +1,10 @@
---
"agent-browser": minor
---
- Added `keyboard` command for raw keyboard input -- type with real keystrokes, insert text, and press shortcuts at the currently focused element without needing a selector.
- Added `--color-scheme` flag and `AGENT_BROWSER_COLOR_SCHEME` env var for persistent dark/light mode preference across browser sessions.
- Fixed IPC EAGAIN errors (os error 35/11) by adding backpressure-aware socket writes, command serialization, and lowering the default Playwright timeout to 25s (configurable via `AGENT_BROWSER_DEFAULT_TIMEOUT`).
- Fixed remote debugging (CDP) reconnection.
- Fixed state load failing when no browser is running.
- Fixed `--annotate` flag warning appearing when not explicitly passed via CLI.
+2
View File
@@ -24,6 +24,8 @@ When adding or changing user-facing features (new flags, commands, behaviors, en
This applies to changes that either human users or AI agents would need to know about. Do not skip any of these locations.
In the `docs/src/app/` MDX files, always use HTML `<table>` syntax for tables (not markdown pipe tables). This matches the existing convention across the docs site.
<!-- opensrc:start -->
## Source Code Reference
+17
View File
@@ -507,6 +507,23 @@ Auto-discovered config files that are missing are silently ignored. If `--config
> **Tip:** If your project-level `agent-browser.json` contains environment-specific values (paths, proxies), consider adding it to `.gitignore`.
## Default Timeout
The default Playwright timeout for standard operations (clicks, waits, fills, etc.) is 25 seconds. This is intentionally below the CLI's 30-second IPC read timeout so that Playwright returns a proper error instead of the CLI timing out with EAGAIN.
Override the default timeout via environment variable:
```bash
# Set a longer timeout for slow pages (in milliseconds)
export AGENT_BROWSER_DEFAULT_TIMEOUT=45000
```
> **Note:** Setting this above 30000 (30s) may cause EAGAIN errors on slow operations because the CLI's read timeout will expire before Playwright responds. The CLI retries transient errors automatically, but response times will increase.
| Variable | Description |
|----------|-------------|
| `AGENT_BROWSER_DEFAULT_TIMEOUT` | Default Playwright timeout in ms (default: 25000) |
## Selectors
### Refs (Recommended for AI)
+4
View File
@@ -2148,6 +2148,10 @@ Environment:
AGENT_BROWSER_AUTO_CONNECT Auto-discover and connect to running Chrome
AGENT_BROWSER_ALLOW_FILE_ACCESS Allow file:// URLs to access local files
AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference)
AGENT_BROWSER_DEFAULT_TIMEOUT Default Playwright timeout in ms (default: 25000)
AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name
AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete saved states older than N days (default: 30)
AGENT_BROWSER_ENCRYPTION_KEY 64-char hex key for AES-256-GCM session encryption
AGENT_BROWSER_STREAM_PORT Enable WebSocket streaming on port (e.g., 9223)
AGENT_BROWSER_IOS_DEVICE Default iOS device name
AGENT_BROWSER_IOS_UDID Default iOS device UDID
+23
View File
@@ -139,6 +139,29 @@ Extensions from user-level and project-level configs are **concatenated**, not r
The `AGENT_BROWSER_EXTENSIONS` environment variable and CLI `--extension` flags follow the standard priority rules (env replaces config, CLI appends).
## Environment Variables
These environment variables configure additional daemon and runtime behavior:
<table>
<thead>
<tr><th>Variable</th><th>Description</th><th>Default</th></tr>
</thead>
<tbody>
<tr><td><code>AGENT_BROWSER_AUTO_CONNECT</code></td><td>Auto-discover and connect to a running Chrome instance.</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_ALLOW_FILE_ACCESS</code></td><td>Allow <code>file://</code> URLs to access local files.</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_COLOR_SCHEME</code></td><td>Color scheme preference (<code>dark</code>, <code>light</code>, <code>no-preference</code>).</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_DEFAULT_TIMEOUT</code></td><td>Default Playwright timeout in ms. Keep below 30000 to avoid IPC timeouts.</td><td><code>25000</code></td></tr>
<tr><td><code>AGENT_BROWSER_SESSION_NAME</code></td><td>Auto-save/load state persistence name.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_STATE_EXPIRE_DAYS</code></td><td>Auto-delete saved session states older than N days.</td><td><code>30</code></td></tr>
<tr><td><code>AGENT_BROWSER_ENCRYPTION_KEY</code></td><td>64-char hex key for AES-256-GCM session encryption.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_STREAM_PORT</code></td><td>Enable WebSocket streaming on the specified port (e.g., <code>9223</code>).</td><td>(disabled)</td></tr>
<tr><td><code>AGENT_BROWSER_IOS_DEVICE</code></td><td>Default iOS device name for the <code>ios</code> provider.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_IOS_UDID</code></td><td>Default iOS device UDID for the <code>ios</code> provider.</td><td>(none)</td></tr>
<tr><td><code>AGENT_BROWSER_DEBUG</code></td><td>Enable debug output (<code>1</code> to enable).</td><td>(disabled)</td></tr>
</tbody>
</table>
## Error Handling
- **Auto-discovered config files** (`~/.agent-browser/config.json`, `./agent-browser.json`) that are missing are silently ignored.
+1 -1
View File
@@ -269,7 +269,7 @@ agent-browser diff url https://staging.example.com https://prod.example.com --sc
## Timeouts and Slow Pages
The default Playwright timeout is 60 seconds for local browsers. For slow websites or large pages, use explicit waits instead of relying on the default timeout:
The default Playwright timeout is 25 seconds for local browsers. This can be overridden with the `AGENT_BROWSER_DEFAULT_TIMEOUT` environment variable (value in milliseconds). For slow websites or large pages, use explicit waits instead of relying on the default timeout:
```bash
# Wait for network activity to settle (best for slow pages)
+55 -2
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
import { BrowserManager } from './browser.js';
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest';
import { BrowserManager, getDefaultTimeout } from './browser.js';
import { executeCommand } from './actions.js';
import { chromium } from 'playwright-core';
@@ -1196,3 +1196,56 @@ describe('BrowserManager', () => {
});
});
});
describe('getDefaultTimeout', () => {
const originalEnv = { ...process.env };
afterEach(() => {
process.env = { ...originalEnv };
});
it('should return 25000 when env var is not set', () => {
delete process.env.AGENT_BROWSER_DEFAULT_TIMEOUT;
expect(getDefaultTimeout()).toBe(25000);
});
it('should return parsed value when env var is a valid positive integer', () => {
process.env.AGENT_BROWSER_DEFAULT_TIMEOUT = '10000';
expect(getDefaultTimeout()).toBe(10000);
});
it('should return 25000 for negative values', () => {
process.env.AGENT_BROWSER_DEFAULT_TIMEOUT = '-1';
expect(getDefaultTimeout()).toBe(25000);
});
it('should return 25000 for zero', () => {
process.env.AGENT_BROWSER_DEFAULT_TIMEOUT = '0';
expect(getDefaultTimeout()).toBe(25000);
});
it('should return 25000 for values below 1000ms floor', () => {
process.env.AGENT_BROWSER_DEFAULT_TIMEOUT = '500';
expect(getDefaultTimeout()).toBe(25000);
});
it('should accept exactly 1000ms as the minimum', () => {
process.env.AGENT_BROWSER_DEFAULT_TIMEOUT = '1000';
expect(getDefaultTimeout()).toBe(1000);
});
it('should return 25000 for non-numeric strings', () => {
process.env.AGENT_BROWSER_DEFAULT_TIMEOUT = 'abc';
expect(getDefaultTimeout()).toBe(25000);
});
it('should return 25000 for empty string', () => {
process.env.AGENT_BROWSER_DEFAULT_TIMEOUT = '';
expect(getDefaultTimeout()).toBe(25000);
});
it('should allow overriding above 25s for users who need longer timeouts', () => {
process.env.AGENT_BROWSER_DEFAULT_TIMEOUT = '60000';
expect(getDefaultTimeout()).toBe(60000);
});
});
+6 -4
View File
@@ -29,19 +29,21 @@ import {
} from './state-utils.js';
/**
* Returns the default Playwright timeout for standard operations.
* Returns the default Playwright timeout in milliseconds for standard operations.
* Can be overridden via the AGENT_BROWSER_DEFAULT_TIMEOUT environment variable.
* Default is 25s, which is below the CLI's 30s IPC read timeout to ensure
* Playwright errors are returned before the CLI gives up with EAGAIN.
* CDP and recording contexts use a shorter fixed timeout (10s) and are not affected.
*/
function getDefaultTimeout(): number {
export function getDefaultTimeout(): number {
const envValue = process.env.AGENT_BROWSER_DEFAULT_TIMEOUT;
if (envValue) {
const parsed = parseInt(envValue, 10);
if (!isNaN(parsed) && parsed > 0) {
if (!isNaN(parsed) && parsed >= 1000) {
return parsed;
}
}
return 60000;
return 25000;
}
// Screencast frame data from CDP
+67 -2
View File
@@ -1,7 +1,9 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as os from 'os';
import * as path from 'path';
import { getSocketDir } from './daemon.js';
import * as net from 'net';
import { EventEmitter } from 'events';
import { getSocketDir, safeWrite } from './daemon.js';
/**
* HTTP request detection pattern used in daemon.ts to prevent cross-origin attacks.
@@ -94,3 +96,66 @@ describe('getSocketDir', () => {
});
});
});
function createMockSocket(opts: { destroyed?: boolean; writeReturns?: boolean } = {}) {
const emitter = new EventEmitter();
const socket = Object.assign(emitter, {
destroyed: opts.destroyed ?? false,
write: vi.fn().mockReturnValue(opts.writeReturns ?? true),
removeListener: emitter.removeListener.bind(emitter),
});
return socket as unknown as net.Socket;
}
describe('safeWrite', () => {
it('should resolve immediately when socket.write returns true', async () => {
const socket = createMockSocket({ writeReturns: true });
await safeWrite(socket, 'hello\n');
expect(socket.write).toHaveBeenCalledWith('hello\n');
});
it('should resolve immediately when socket is already destroyed', async () => {
const socket = createMockSocket({ destroyed: true });
await safeWrite(socket, 'hello\n');
expect(socket.write).not.toHaveBeenCalled();
});
it('should wait for drain event when socket.write returns false', async () => {
const socket = createMockSocket({ writeReturns: false });
const promise = safeWrite(socket, 'big payload');
// Simulate drain after a tick
setTimeout(() => socket.emit('drain'), 0);
await promise;
expect(socket.write).toHaveBeenCalledWith('big payload');
});
it('should reject on socket error while waiting for drain', async () => {
const socket = createMockSocket({ writeReturns: false });
const promise = safeWrite(socket, 'data');
setTimeout(() => socket.emit('error', new Error('connection reset')), 0);
await expect(promise).rejects.toThrow('connection reset');
});
it('should resolve on socket close while waiting for drain', async () => {
const socket = createMockSocket({ writeReturns: false });
const promise = safeWrite(socket, 'data');
setTimeout(() => socket.emit('close'), 0);
await promise;
});
it('should clean up listeners after drain resolves', async () => {
const socket = createMockSocket({ writeReturns: false });
const promise = safeWrite(socket, 'data');
setTimeout(() => socket.emit('drain'), 0);
await promise;
expect(socket.listenerCount('drain')).toBe(0);
expect(socket.listenerCount('error')).toBe(0);
expect(socket.listenerCount('close')).toBe(0);
});
});
+8 -3
View File
@@ -26,7 +26,7 @@ type Manager = BrowserManager | IOSManager;
* If the kernel buffer is full (socket.write returns false),
* waits for the 'drain' event before resolving.
*/
function safeWrite(socket: net.Socket, payload: string): Promise<void> {
export function safeWrite(socket: net.Socket, payload: string): Promise<void> {
return new Promise((resolve, reject) => {
if (socket.destroyed) {
resolve();
@@ -592,8 +592,13 @@ export async function startDaemon(options?: {
commandQueue.push(line);
}
processQueue().catch(() => {
// Socket write failures during queue processing are non-fatal
processQueue().catch((err) => {
// Socket write failures during queue processing are non-fatal;
// the client has likely disconnected.
console.warn('[warn] processQueue error:', err?.message ?? err);
if (process.env.AGENT_BROWSER_DEBUG === '1') {
console.error('[DEBUG] processQueue error (full):', err);
}
});
});