/** * A real browser the agent can use, and you can watch. * * An agent that writes a web page and then greps its own source for a class name is guessing. The * question "does the button work" is answered by pressing it, and until now nothing here could. * This drives Chromium over the DevTools protocol: open a page, read what it actually renders, * click, type, and take a picture of the result. * * Chromium exposes DevTools over a WebSocket and the protocol is JSON — so a browser driver is a * few hundred lines rather than a hundred * megabytes of node_modules. Puppeteer and Playwright each ship their own Chromium build; KONECK * installs with `npm i -g koneck` and would be a very different proposition at that size. * * Chromium is not bundled either, for the same reason. What is present is used and what is absent * is reported as absent — the same rule as the language servers. * * Human-like on purpose: a click is a real mouse event dispatched at the element's centre, not * `el.click()`. Frameworks that synthesise their own event plumbing behave differently under the * two, and the point of a browser is to find out what a person would find out. */ /** Names a Chromium-family browser goes by, in the order they are tried. */ export declare const CHROME_BINARIES: string[]; /** * The places to look, for this machine. * * Bare names are looked up on PATH; anything with a separator is tested as a path. On Windows the * bare .exe names go first — somebody who has put a browser on PATH deserves to have that used — * and the install locations follow. */ export declare function browserCandidates(platform?: string, env?: NodeJS.ProcessEnv): string[]; /** The browser to drive, or nothing. */ export declare function findBrowser(candidates?: readonly string[], env?: NodeJS.ProcessEnv, platform?: string): string | null; /** * Whether a bare command name resolves on PATH. * * Its own function because two callers need it and one of them cannot go through findBrowser: * findBrowser answers KONECK_BROWSER first, so asking it whether KONECK_BROWSER resolves is a * question that answers itself. That circularity made a variable pointing at a non-existent command * report as fine. */ export declare function onPath(name: string, env?: NodeJS.ProcessEnv, platform?: string): boolean; /** * What is wrong with KONECK_BROWSER, if anything. * * Null when it is unset, or when it names something usable — a path that exists, or a bare name to * be looked up on PATH like any other candidate. */ export declare function namedBrowserProblem(env?: NodeJS.ProcessEnv): string | null; export interface PageState { url: string; title: string; /** What the page renders as text, which is what a person would read. */ text: string; /** Interactive things, so the agent can act without guessing at selectors. */ controls: Array<{ ref: number; tag: string; label: string; selector: string; /** Where it sits on the page, so an interface can point at it rather than guess. */ box?: { x: number; y: number; w: number; h: number; }; }>; } /** How long any one protocol call gets. A browser that has wedged must not wedge the run. */ export declare const CALL_TIMEOUT_MS = 20000; /** * How patient to be with a page, as a choice rather than as a constant. * * The floor exists because a page that draws its content after load cannot be told apart from one * that has finished — there is no signal that says "more is coming". Waiting is the only way to * find out, and 900ms catches the common case of a script that renders on a timer. * * That is a real trade and not one KONECK should make on everybody's behalf. A local dev server * answering in 30ms does not need it; a React app fetching its own data does. So it is a setting, * and the honest thing is to say what each choice costs. */ export type Patience = 'quick' | 'balanced' | 'thorough'; export declare const PATIENCE: Record; export declare function patienceFrom(value: unknown): Patience; /** * One browser, one page. * * A single page rather than a pool: the agent is doing one thing, the person is watching one * thing, and tabs would need naming, listing and disambiguating for no gain anybody asked for. */ export declare function windowSize(configured?: string): string; export declare class Browser { private child; private socket; private profile; private sessionId; private nextId; private readonly pending; /** The last picture taken, so the interface can show it without asking for another. */ lastShot: string | null; /** What the page has complained about lately, newest last, bounded. */ private readonly problems; /** * How long to wait for a page, as chosen rather than as fixed. * * Settable while running, because it is a preference about this project rather than about this * browser: somebody who has just watched a page be read half-drawn should be able to say so * without restarting anything. */ patience: Patience; /** The window size from configuration, as WIDTHxHEIGHT. KONECK_BROWSER_WINDOW outranks it. */ window?: string; get running(): boolean; private remember; /** Problems the page ran into, which nobody would otherwise see. */ recentProblems(): string[]; clearProblems(): void; /** Starts a browser, or explains why it could not. Never throws. */ start(headless?: boolean): Promise<{ ok: boolean; error?: string; }>; /** * Problems the page reports about itself. * * Kept because they are usually the answer. A page whose API call returns 500 renders as an * empty area, and an agent reading that page sees nothing wrong with it — while the console says * exactly what failed. These were being discarded, so the one fact that would have let it fix * the problem was the one fact it never had. */ /** * Set the moment the page starts loading something, cleared when it finishes. * * The protocol says so; polling for it was guessing. `settleAfterAction` used to sample the page * every fifty milliseconds for a fifth of a second to work out whether a click had navigated — * which meant every click that navigated nothing cost 200ms of waiting to discover that nothing * had happened. Measured at 218ms per click, and a panel that answers a fifth of a second after * you touch it is a panel that feels broken. */ private navigating; private noteEvent; /** A protocol call with no session — browser-level rather than page-level. */ private raw; /** A page-level call. Flat sessions carry the session on the message. */ private call; /** Runs an expression in the page and returns its value. */ private evaluate; open(url: string): Promise<{ ok: boolean; error?: string; }>; /** * Waits for the page to stop changing, rather than for a fixed time. * * readyState alone is not enough for anything that renders after load, which is most of what * anybody builds now. So it also waits for the rendered text to stop growing: two identical * readings in a row means the page has settled, and a page that never settles is bounded by the * attempt count rather than left to hang. */ /** * A quick check, for actions that cannot navigate. * * Typing into a field does not load a page, so waiting for one is pure latency — and it was: * measured at 1.1 seconds per field, 5.6 seconds to fill five, before the model had even been * asked what to do next. The full settle stays where it belongs, behind anything that can * navigate. */ private settleQuick; /** * After a click or a keypress: pay for a page load only if there was one. * * Most clicks in a form flow navigate nothing — a toggle, a disclosure, a validation message — * and those were charged the full settle, which has a floor of nearly a second. Five such clicks * is five seconds of waiting for a page that never moved. * * So it watches instead of assuming. A short poll asks whether anything actually changed: the * url, the amount of rendered text, or readyState leaving complete. If nothing did, the action is * over and it returns in about a tenth of a second. If something did, the page is doing work and * gets the full settle it deserves. * * This replaces settle(6), which meant six attempts while settle counted attempts and six * milliseconds once it was changed to take a deadline — so a click stopped waiting for navigation * at all. That mistake was mine, and the fix is not to restore the old number: a fixed wait is * either too slow for a click that does nothing or too short for one that loads a page. */ private settleAfterAction; private settle; /** * A cheap fingerprint of the page, for telling whether anything actually happened. * * Returning the whole page after every action costs the model a re-read of text it already has — * and after typing into a field that text is identical. The url and the rendered length together * catch navigation and any change worth re-reading, at the price of one small evaluate rather * than a full state(). */ signature(): Promise; state(): Promise; /** * Resolves what the caller meant into something on the page. * * Three ways, in order of how hard they are to get wrong. A ref is a number from the list just * read, and cannot be malformed. A visible label is what a person would say — "click Get * Started" — and needs no knowledge of the markup at all. A CSS selector is exact when it is * right and silently matches nothing when it is not, which is why it is last rather than first. */ private resolve; private findVisibleByText; /** What is on the page to act on, for an error message that helps rather than scolds. */ describeControls(): Promise; /** Where an element is, in page coordinates. Null when it is not there or not visible. */ private centreOf; /** * Clicks where a person would click. * * Real mouse events at the element's centre, not `el.click()`. The two behave differently under * frameworks that build their own event plumbing, and the reason to drive a browser at all is to * find out what a person would find out. */ click(target: { ref?: number; text?: string; label?: string; selector?: string; } | string): Promise<{ ok: boolean; error?: string; }>; /** A click at a point, which is what a user clicking the preview means. */ clickAt(x: number, y: number): Promise<{ ok: boolean; }>; private pointAndPress; /** * Sets a native (so filling proceeds normally), 'set' * when an option matched and was chosen, and 'nomatch' when it is a select but nothing matched — * which is a real failure worth reporting rather than typing into it. */ private selectNativeOption; private fireFieldEvents; /** Focuses a field and types into it, the way a keyboard would. */ type(target: { ref?: number; text?: string; label?: string; selector?: string; } | string, text: string, submit?: boolean): Promise<{ ok: boolean; error?: string; }>; /** * Fills several fields in one go. * * A form is one thought, not five. Filling it a field at a time cost a model round-trip each — * read the page, type, read the page, type — and the page was not changing between them, so * every one of those reads returned the same thing at the same price. Measured at 5.6 seconds of * browser time for five fields, plus five turns of the model's own latency. * * Each field is resolved and typed the same way a single one is, so nothing is traded for the * speed; what is dropped is the re-reading between them. */ fill(fields: ReadonlyArray<{ ref?: number; label?: string; selector?: string; text: string; }>, submit?: boolean, /** * A control to press once the fields are in, rather than pressing Enter. * * Because most forms are submitted by a button, not by a keystroke — and every action is a * separate model round trip. Measured on a real session: browser actions took 24 seconds in * total while the model spent 14.5 minutes deciding between them, so what makes a form fast is * not a faster click but one fewer decision. A login is a form and a button: one action. */ then?: { ref?: number; label?: string; selector?: string; }): Promise<{ ok: boolean; filled: string[]; missed: string[]; error?: string; }>; /** A named key, for the ones that mean something rather than insert something. */ /** * Text typed straight into whatever the page has focused. * * The panel could click a page and not type into it, so a form could be reached and never filled * — you clicked the field, the caret appeared, and every key you pressed went nowhere. Not a * decision anybody made; it was simply never wired. * * Insert rather than key events per character: it puts the whole string in at once, fires the * input events a page listens for, and does not need a keycode table to type a bracket. And a * quick settle rather than a full one, because typing does not navigate. */ typeText(text: string): Promise<{ ok: boolean; }>; press(key: string): Promise<{ ok: boolean; }>; /** * Back, and forward. * * A browser without a back button is not one anybody would recognise. It also matters more here * than in an ordinary browser: an agent clicking through a flow to see what happens needs to be * able to step out of a dead end without retyping a url and losing the session it just built up. */ back(): Promise<{ ok: boolean; error?: string; }>; forward(): Promise<{ ok: boolean; error?: string; }>; private goHistory; /** Whether stepping either way would do anything, so a control can be shown as usable or not. */ canGo(): Promise<{ back: boolean; forward: boolean; }>; scroll(by: number): Promise<{ ok: boolean; }>; /** A picture of the page as it is, base64 PNG. */ /** * A picture of the page. * * JPEG rather than PNG by default, and that is a latency decision rather than an aesthetic one: * the same page measured 118KB as a PNG and takes 71ms to encode, which is most of the delay * between clicking something in the panel and seeing the result. A screenshot of a rendered page * is a photograph, and photographs are what JPEG is for. */ screenshot(): Promise; stop(): Promise; } export declare function noteBrowser(browserPid: number | undefined, profile: string): void; export declare function forgetBrowser(profile: string): void; export declare function reapAbandonedBrowsers(): Promise; //# sourceMappingURL=browser.d.ts.map