Skip to content

Instantly share code, notes, and snippets.

@ivorpad
Last active March 26, 2026 18:26
Show Gist options
  • Select an option

  • Save ivorpad/9afbaf8967fb288f3f040b049f276595 to your computer and use it in GitHub Desktop.

Select an option

Save ivorpad/9afbaf8967fb288f3f040b049f276595 to your computer and use it in GitHub Desktop.
// ab-batch-type: Fast keyboard typing for canvas-based web apps via agent-browser daemon.
// Sends all keystrokes through the daemon's keyboard action (trusted CDP events, single event path).
//
// Usage:
// node <this-script> "text" Tab "text" Enter ... # args mode
// echo '"text" Tab "text" Enter ...' | node <this-script> # stdin mode (unlimited size)
//
// Stdin mode reads all input, splits on whitespace (respecting "quoted strings"),
// and processes identically to arg mode. Use for large payloads that exceed shell
// argument limits.
//
// Text strings typed char-by-char. Special keys: Tab Enter Escape Backspace Delete Arrow*.
//
// Use for: Google Sheets, Google Docs, Excel Online, Figma, Monaco/code-server,
// Airtable, Notion, terminal emulators (xterm.js) — any app where ab fill/type fails
// because targets are canvas-rendered or custom inputs, not DOM <input> elements.
//
// CRITICAL: all events must go through the daemon's keyboard action.
// Do NOT mix with input_keyboard (CDP direct) — they have separate focus tracking
// and mixing them breaks cell navigation (Enter won't move to next row).
//
// Performance: pipelines ALL commands over the socket in one write — zero artificial
// delays. The daemon processes them sequentially via CDP; Google Sheets handles the
// pace naturally. ~10x faster than send-one-await-one with pauses.
//
// Requires: agent-browser daemon running (any agent-browser command starts it).
const net = require('net');
const path = require('path');
const os = require('os');
const session = process.env.AGENT_BROWSER_SESSION || 'default';
const socketPath = path.join(os.homedir(), '.agent-browser', session + '.sock');
const SPECIAL = new Set([
'Tab', 'Enter', 'Escape', 'Backspace', 'Delete',
'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight',
]);
function parseTokens(input) {
const tokens = [];
const re = /"([^"]*)"|([\S]+)/g;
let m;
while ((m = re.exec(input)) !== null) {
tokens.push(m[1] !== undefined ? m[1] : m[2]);
}
return tokens;
}
function buildCommands(tokens) {
const commands = [];
let id = 0;
for (const token of tokens) {
if (SPECIAL.has(token)) {
commands.push({ id: String(++id), action: 'keyboard', keys: token });
} else {
for (const ch of token) {
commands.push({ id: String(++id), action: 'keyboard', keys: ch });
}
}
}
return commands;
}
function readStdin() {
return new Promise((resolve) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => { data += chunk; });
process.stdin.on('end', () => resolve(data));
});
}
async function getTokens() {
const args = process.argv.slice(2);
if (args.length > 0) return args;
// Stdin mode
const input = await readStdin();
return parseTokens(input);
}
async function main() {
const tokens = await getTokens();
const commands = buildCommands(tokens);
if (commands.length === 0) {
console.error('Usage: node ab-batch-type.js "text" Tab "text" Enter ...');
console.error(' or: echo \'"text" Tab "text" Enter\' | node ab-batch-type.js');
console.error('Special keys: Tab Enter Escape Backspace Delete ArrowDown ArrowUp ArrowLeft ArrowRight');
process.exit(1);
}
const sock = net.connect(socketPath);
await new Promise((resolve, reject) => {
sock.on('connect', resolve);
sock.on('error', reject);
});
const start = Date.now();
// Pipeline: write ALL commands in one shot
sock.write(commands.map(c => JSON.stringify(c)).join('\n') + '\n');
// Collect all responses
let buffer = '';
let resolved = 0;
await new Promise((resolve, reject) => {
sock.on('data', d => {
buffer += d.toString();
while (buffer.includes('\n')) {
const nlIdx = buffer.indexOf('\n');
const line = buffer.substring(0, nlIdx).trim();
buffer = buffer.substring(nlIdx + 1);
if (!line) continue;
const resp = JSON.parse(line);
if (!resp.success) {
reject(new Error('Failed at key ' + resolved + ': ' + JSON.stringify(resp)));
return;
}
resolved++;
if (resolved >= commands.length) {
resolve();
return;
}
}
});
sock.on('error', reject);
});
const elapsed = Date.now() - start;
console.log('Done — ' + commands.length + ' keys in ' + elapsed + 'ms');
sock.end();
process.exit(0);
}
main().catch(e => { console.error(e.message); process.exit(1); });
setTimeout(() => { console.error('timeout'); process.exit(1); }, 120000);

add to references

Canvas App Automation (Google Sheets, Docs, Figma, etc.)

Canvas-based web apps render content on <canvas> or custom elements, not DOM <input> fields. Standard agent-browser fill @ref and agent-browser type @ref fail because there are no DOM refs to target.

Which Apps Need This

App Why Notes
Google Sheets Cells are canvas-rendered Name Box is #t-name-box
Google Docs Canvas text editor
Google Slides Canvas presentation
Excel Online Canvas cells
Figma Canvas + custom inputs
Notion Custom contentEditable blocks Partial DOM, custom keyboard nav
Airtable Custom grid renderer
Linear Custom editor
VS Code web / code-server Monaco editor Custom keyboard handling
Terminal emulators (xterm.js) Canvas terminal

The Script: ab-batch-type.js

Located at: scripts/ab-batch-type.js (relative to this skill)

Pipelines all keystrokes over the daemon socket in one write. Zero delays — ~450 keys/sec.

Usage

# Navigate to target cell first
agent-browser click '#t-name-box' && ab fill '#t-name-box' "A1" && ab press Enter

# Args mode
node <skill-path>/scripts/ab-batch-type.js "Hello" Tab "World" Enter "Row 2" Tab "Data" Enter

# Stdin mode (unlimited size, avoids shell arg limits)
cat <<'EOF' | node <skill-path>/scripts/ab-batch-type.js
"Title" Tab "Year" Tab "Director" Tab "Genre" Enter
"Movie 1" Tab "2024" Tab "Director 1" Tab "Drama" Enter
"Movie 2" Tab "2023" Tab "Director 2" Tab "Comedy" Enter
EOF

Special keys: Tab Enter Escape Backspace Delete ArrowDown ArrowUp ArrowLeft ArrowRight

Google Sheets Rules

  • Name Box: always use #t-name-box CSS selector, NOT snapshot refs (refs are unreliable for Sheets toolbar)
  • Navigate to cell: agent-browser click '#t-name-box' && ab fill '#t-name-box' "A1" && ab press Enter
  • Verify position: agent-browser eval 'document.querySelector("#t-name-box").value'
  • Tab = move right, Enter = commit + move to start of next row
  • All events must use the daemon's keyboard action — never mix with input_keyboard (CDP direct). They have separate focus tracking; mixing breaks cell navigation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment