jeopi-cli 16.4.1 → 16.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/dist/cli.js +3179 -3140
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +7 -1
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
- package/dist/types/internal-urls/types.d.ts +7 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/auth-broker-config.d.ts +2 -2
- package/dist/types/tools/bash-skill-urls.d.ts +3 -2
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +22 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/config.ts +11 -8
- package/src/autolearn/controller.ts +14 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +63 -13
- package/src/cli/usage-error.ts +7 -0
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/config/model-resolver.ts +24 -6
- package/src/config/settings-schema.ts +3 -3
- package/src/edit/index.ts +10 -76
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
- package/src/eval/executor-base.ts +84 -7
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/memory-protocol.ts +13 -8
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +7 -0
- package/src/irc/bus.ts +48 -18
- package/src/lsp/client.ts +7 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +73 -2
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/controllers/event-controller.ts +17 -30
- package/src/modes/controllers/input-controller.ts +9 -6
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/workflow.ts +5 -4
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/browser.md +5 -5
- package/src/sdk.ts +3 -0
- package/src/session/agent-session.ts +237 -50
- package/src/session/auth-broker-config.ts +4 -4
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/builtin-registry.ts +8 -8
- package/src/task/executor.ts +8 -0
- package/src/tools/bash-skill-urls.ts +7 -4
- package/src/tools/bash.ts +30 -22
- package/src/tools/browser/cmux/cmux-tab.ts +49 -50
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +44 -5
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +280 -133
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/glob.ts +20 -6
- package/src/tools/grep.ts +1 -0
- package/src/tools/image-gen.ts +3 -4
- package/src/tools/irc.ts +27 -13
- package/src/tools/read.ts +14 -7
- package/src/tools/write.ts +22 -5
- package/src/tools/yield.ts +29 -1
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +68 -7
- package/src/web/search/index.ts +14 -6
- package/src/web/search/providers/perplexity.ts +18 -2
|
@@ -13,11 +13,11 @@ import type {
|
|
|
13
13
|
ImageFormat,
|
|
14
14
|
KeyInput,
|
|
15
15
|
Page,
|
|
16
|
+
Realm,
|
|
16
17
|
SerializedAXNode,
|
|
17
18
|
Target,
|
|
18
19
|
} from "puppeteer-core";
|
|
19
20
|
import { JsRuntime, type RuntimeHooks } from "../../eval/js/shared/runtime";
|
|
20
|
-
import type { JsDisplayOutput } from "../../eval/js/shared/types";
|
|
21
21
|
import { resizeImage } from "../../utils/image-resize";
|
|
22
22
|
import { resolveToCwd } from "../path-utils";
|
|
23
23
|
import { formatScreenshot } from "../render-utils";
|
|
@@ -36,13 +36,13 @@ import {
|
|
|
36
36
|
loadPuppeteerInWorker,
|
|
37
37
|
} from "./launch";
|
|
38
38
|
import { extractReadableFromHtml, type ReadableFormat } from "./readable";
|
|
39
|
-
import { waitForBrowserRun } from "./run-cancellation";
|
|
39
|
+
import { type WaitPredicateOptions, waitForBrowserRun } from "./run-cancellation";
|
|
40
|
+
import { cloneSafe, RunOutput } from "./run-output";
|
|
40
41
|
import type {
|
|
41
42
|
Observation,
|
|
42
43
|
ObservationEntry,
|
|
43
44
|
ReadyInfo,
|
|
44
45
|
RunErrorPayload,
|
|
45
|
-
RunResultOk,
|
|
46
46
|
ScreenshotResult,
|
|
47
47
|
SessionSnapshot,
|
|
48
48
|
ToolReply,
|
|
@@ -51,6 +51,13 @@ import type {
|
|
|
51
51
|
WorkerInitPayload,
|
|
52
52
|
} from "./tab-protocol";
|
|
53
53
|
|
|
54
|
+
declare module "puppeteer-core" {
|
|
55
|
+
interface Frame {
|
|
56
|
+
/** Puppeteer's main JavaScript realm, retained by our pinned runtime patch. */
|
|
57
|
+
mainRealm(): Realm;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
declare global {
|
|
55
62
|
interface Element extends HTMLElement {}
|
|
56
63
|
function getComputedStyle(element: Element): Record<string, unknown>;
|
|
@@ -116,15 +123,27 @@ type ActionabilityResult = { ok: true; x: number; y: number } | { ok: false; rea
|
|
|
116
123
|
* - `QUICK_OP_TIMEOUT_MS`: page-coupled reads that should resolve fast (`observe`,
|
|
117
124
|
* `screenshot`, `extract`, `ariaSnapshot`).
|
|
118
125
|
* - `ACTION_OP_TIMEOUT_MS`: interactive point actions (`click`, `fill`, `type`, …) and
|
|
119
|
-
* the default for wait helpers when no explicit `{ timeout }` is given.
|
|
126
|
+
* the default for wait helpers when no explicit `{ timeout }` is given. Selector ops
|
|
127
|
+
* additionally fail fast after `ZERO_MATCH_FAIL_FAST_MS` of confirmed zero matches
|
|
128
|
+
* (see `#zeroMatchWatchdog`), so the full ceiling is only spent on elements that
|
|
129
|
+
* exist but are not yet actionable.
|
|
120
130
|
*
|
|
121
131
|
* `goto` and `evaluate` stay uncapped (`Number.POSITIVE_INFINITY`): navigation and user
|
|
122
132
|
* code legitimately use the full cell budget.
|
|
123
133
|
*/
|
|
124
134
|
const QUICK_OP_TIMEOUT_MS = 20_000;
|
|
125
|
-
const ACTION_OP_TIMEOUT_MS =
|
|
135
|
+
const ACTION_OP_TIMEOUT_MS = 8_000;
|
|
126
136
|
/** Headroom subtracted from the cell budget so a per-op deadline fires before it. */
|
|
127
137
|
const OP_DEADLINE_SLACK_MS = 1_000;
|
|
138
|
+
/**
|
|
139
|
+
* A selector op whose selector has matched nothing for this long fails fast with the
|
|
140
|
+
* zero-match hint instead of burning the rest of its deadline: a wrong selector or a
|
|
141
|
+
* wrong page (consent wall, pre-navigation document) is the common agent failure and
|
|
142
|
+
* should cost ~2s, not the full action ceiling. Explicit `{ timeout }` waits opt out.
|
|
143
|
+
*/
|
|
144
|
+
const ZERO_MATCH_FAIL_FAST_MS = 2_000;
|
|
145
|
+
/** Poll cadence for the zero-match watchdog. */
|
|
146
|
+
const ZERO_MATCH_POLL_MS = 250;
|
|
128
147
|
|
|
129
148
|
export interface OpTimeouts {
|
|
130
149
|
/** Largest per-op deadline allowed — strictly below the cell budget. */
|
|
@@ -190,7 +209,7 @@ interface TabApi {
|
|
|
190
209
|
press(key: KeyInput, opts?: { selector?: string }): Promise<void>;
|
|
191
210
|
scroll(deltaX: number, deltaY: number): Promise<void>;
|
|
192
211
|
drag(from: DragTarget, to: DragTarget): Promise<void>;
|
|
193
|
-
waitFor(selector: string, opts?: { timeout?: number }): Promise<
|
|
212
|
+
waitFor(selector: string, opts?: { timeout?: number }): Promise<ActionableHandle>;
|
|
194
213
|
evaluate<TResult, TArgs extends unknown[]>(
|
|
195
214
|
fn: string | ((...args: TArgs) => TResult | Promise<TResult>),
|
|
196
215
|
...args: TArgs
|
|
@@ -206,13 +225,13 @@ interface TabApi {
|
|
|
206
225
|
waitForSelector(
|
|
207
226
|
selector: string,
|
|
208
227
|
opts?: { timeout?: number; visible?: boolean; hidden?: boolean },
|
|
209
|
-
): Promise<
|
|
228
|
+
): Promise<ActionableHandle | null>;
|
|
210
229
|
waitForNavigation(opts?: {
|
|
211
230
|
waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
|
|
212
231
|
timeout?: number;
|
|
213
232
|
}): Promise<HTTPResponse | null>;
|
|
214
|
-
id(n: number): Promise<
|
|
215
|
-
ref(id: string): Promise<
|
|
233
|
+
id(n: number): Promise<ActionableHandle>;
|
|
234
|
+
ref(id: string): Promise<ActionableHandle>;
|
|
216
235
|
}
|
|
217
236
|
|
|
218
237
|
export function normalizeSelector(selector: string): string {
|
|
@@ -259,16 +278,30 @@ function asElementHandle(handle: unknown): ElementHandle | null {
|
|
|
259
278
|
return handle ? (handle as ElementHandle) : null;
|
|
260
279
|
}
|
|
261
280
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
281
|
+
/** ElementHandle enriched with the `fill()` the tool docs promise on handles from `tab.id()`/`tab.ref()`/`tab.waitFor()`. */
|
|
282
|
+
export type ActionableHandle = ElementHandle & { fill(value: string): Promise<void> };
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Attach `fill()` to a puppeteer ElementHandle before handing it to user code.
|
|
286
|
+
* Puppeteer handles expose `type()` but no `fill()`; the semantics mirror the
|
|
287
|
+
* selector-based `tab.fill()`: focus, clear any existing value, then type.
|
|
288
|
+
*/
|
|
289
|
+
export function toActionableHandle(handle: ElementHandle): ActionableHandle {
|
|
290
|
+
const enriched = handle as ActionableHandle;
|
|
291
|
+
enriched.fill = value => fillViaHandle(enriched, value);
|
|
292
|
+
return enriched;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Focus, clear any existing value, then retype — shared by `tab.fill(aria-ref)` and enriched handles. */
|
|
296
|
+
async function fillViaHandle(handle: ElementHandle, value: string, signal?: AbortSignal): Promise<void> {
|
|
297
|
+
await untilAborted(signal, () =>
|
|
298
|
+
handle.evaluate(el => {
|
|
299
|
+
const node = el as unknown as { value?: string; focus?: () => void };
|
|
300
|
+
node.focus?.();
|
|
301
|
+
if ("value" in node) node.value = "";
|
|
302
|
+
}),
|
|
303
|
+
);
|
|
304
|
+
await untilAborted(signal, () => handle.type(value, { delay: 0 }));
|
|
272
305
|
}
|
|
273
306
|
|
|
274
307
|
/**
|
|
@@ -303,14 +336,6 @@ function errorPayload(error: unknown): RunErrorPayload {
|
|
|
303
336
|
return { name: "Error", message: String(error), isToolError: false, isAbort: false };
|
|
304
337
|
}
|
|
305
338
|
|
|
306
|
-
function safeJsonStringify(value: unknown): string {
|
|
307
|
-
try {
|
|
308
|
-
return JSON.stringify(value, null, 2);
|
|
309
|
-
} catch {
|
|
310
|
-
return String(value);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
339
|
function replyError(payload: RunErrorPayload): Error {
|
|
315
340
|
if (payload.isAbort) {
|
|
316
341
|
const err = new ToolAbortError(payload.message || "Tool call aborted");
|
|
@@ -508,6 +533,17 @@ async function clickQueryHandlerText(
|
|
|
508
533
|
);
|
|
509
534
|
}
|
|
510
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Hint appended to a selector op's fail-fast timeout, given the selector's current
|
|
538
|
+
* match count: a missing element (consent wall, wrong page) reads differently from
|
|
539
|
+
* a present-but-unactionable one.
|
|
540
|
+
*/
|
|
541
|
+
export function formatSelectorMatchHint(count: number): string {
|
|
542
|
+
return count === 0
|
|
543
|
+
? "; selector currently matches no elements — run tab.observe() or tab.ariaSnapshot() to inspect the page"
|
|
544
|
+
: `; selector currently matches ${count} element(s) but the action never became possible — the element may be hidden or covered (try tab.scrollIntoView() or a more specific selector)`;
|
|
545
|
+
}
|
|
546
|
+
|
|
511
547
|
export interface InflightOp {
|
|
512
548
|
label: string;
|
|
513
549
|
startedAt: number;
|
|
@@ -517,7 +553,7 @@ interface ActiveRun {
|
|
|
517
553
|
id: string;
|
|
518
554
|
ac: AbortController;
|
|
519
555
|
signal: AbortSignal;
|
|
520
|
-
|
|
556
|
+
output: RunOutput;
|
|
521
557
|
screenshots: ScreenshotResult[];
|
|
522
558
|
pendingTools: Map<string, { resolve(value: unknown): void; reject(error: Error): void }>;
|
|
523
559
|
/** Helper invocations currently awaiting the page/network, keyed by op id. */
|
|
@@ -701,13 +737,13 @@ export class WorkerCore {
|
|
|
701
737
|
const ac = new AbortController();
|
|
702
738
|
const runAc = new AbortController();
|
|
703
739
|
const signal = AbortSignal.any([timeoutSignal, ac.signal, runAc.signal]);
|
|
704
|
-
const
|
|
740
|
+
const output = new RunOutput();
|
|
705
741
|
const screenshots: ScreenshotResult[] = [];
|
|
706
742
|
const active: ActiveRun = {
|
|
707
743
|
id: msg.id,
|
|
708
744
|
ac,
|
|
709
745
|
signal,
|
|
710
|
-
|
|
746
|
+
output,
|
|
711
747
|
screenshots,
|
|
712
748
|
pendingTools: new Map(),
|
|
713
749
|
inflight: new Map(),
|
|
@@ -718,7 +754,7 @@ export class WorkerCore {
|
|
|
718
754
|
throwIfAborted(signal);
|
|
719
755
|
const page = this.#requirePage();
|
|
720
756
|
const browser = this.#requireBrowser();
|
|
721
|
-
const tabApi = this.#createTabApi(msg.name, msg.timeoutMs, signal, msg.session,
|
|
757
|
+
const tabApi = this.#createTabApi(msg.name, msg.timeoutMs, signal, msg.session, output, screenshots, active);
|
|
722
758
|
const runtime = this.#ensureRuntime(msg.session);
|
|
723
759
|
runtime.setCwd(msg.session.cwd);
|
|
724
760
|
runtime.setRunScope({
|
|
@@ -728,7 +764,8 @@ export class WorkerCore {
|
|
|
728
764
|
assert: (cond: unknown, text?: string): void => {
|
|
729
765
|
if (!cond) throw new ToolError(text ?? "Assertion failed");
|
|
730
766
|
},
|
|
731
|
-
wait: (
|
|
767
|
+
wait: (msOrPredicate: number | (() => unknown), opts?: WaitPredicateOptions): Promise<unknown> =>
|
|
768
|
+
waitForBrowserRun(msOrPredicate, signal, opts),
|
|
732
769
|
});
|
|
733
770
|
const { promise: cancelRejection, reject: rejectCancel } = Promise.withResolvers<never>();
|
|
734
771
|
const onCancel = (): void => {
|
|
@@ -762,7 +799,7 @@ export class WorkerCore {
|
|
|
762
799
|
type: "result",
|
|
763
800
|
id: msg.id,
|
|
764
801
|
ok: true,
|
|
765
|
-
payload: { displays, returnValue: cloneSafe(returnValue), screenshots },
|
|
802
|
+
payload: { displays: output.finish(), returnValue: cloneSafe(returnValue), screenshots },
|
|
766
803
|
});
|
|
767
804
|
} finally {
|
|
768
805
|
signal.removeEventListener("abort", onCancel);
|
|
@@ -790,11 +827,12 @@ export class WorkerCore {
|
|
|
790
827
|
return {
|
|
791
828
|
onText: chunk => {
|
|
792
829
|
throwIfAborted(active.signal);
|
|
830
|
+
active.output.pushText(chunk);
|
|
793
831
|
this.#log("debug", chunk.replace(/\n$/, ""));
|
|
794
832
|
},
|
|
795
833
|
onDisplay: output => {
|
|
796
834
|
throwIfAborted(active.signal);
|
|
797
|
-
|
|
835
|
+
active.output.pushDisplay(output);
|
|
798
836
|
},
|
|
799
837
|
callTool: (name, args) => {
|
|
800
838
|
throwIfAborted(active.signal);
|
|
@@ -803,20 +841,6 @@ export class WorkerCore {
|
|
|
803
841
|
};
|
|
804
842
|
}
|
|
805
843
|
|
|
806
|
-
#pushDisplay(displays: RunResultOk["displays"], output: JsDisplayOutput): void {
|
|
807
|
-
if (output.type === "image") {
|
|
808
|
-
displays.push({ type: "image", data: output.data, mimeType: output.mimeType });
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
if (output.type === "json") {
|
|
812
|
-
displays.push({ type: "text", text: safeJsonStringify(output.data) });
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
// status — surface as compact JSON so helper side effects (read/write/env) appear in
|
|
816
|
-
// the cell result alongside explicit display() output.
|
|
817
|
-
displays.push({ type: "text", text: safeJsonStringify(output.event) });
|
|
818
|
-
}
|
|
819
|
-
|
|
820
844
|
async #callTool(active: ActiveRun, name: string, args: unknown): Promise<unknown> {
|
|
821
845
|
const id = `tab-tc-${active.id}-${crypto.randomUUID()}`;
|
|
822
846
|
const { promise, resolve, reject } = Promise.withResolvers<unknown>();
|
|
@@ -841,7 +865,10 @@ export class WorkerCore {
|
|
|
841
865
|
* with a named error instead of silently consuming the whole cell budget. Pass
|
|
842
866
|
* `Number.POSITIVE_INFINITY` for `perOpTimeoutMs` to bound the op only by the cell
|
|
843
867
|
* budget (used for `evaluate` running user code and for locator helpers that already
|
|
844
|
-
* carry puppeteer's own `.setTimeout(timeoutMs)`).
|
|
868
|
+
* carry puppeteer's own `.setTimeout(timeoutMs)`). When the op targets a `selector`,
|
|
869
|
+
* the fail-fast timeout carries a best-effort match-count hint, and — when
|
|
870
|
+
* `zeroMatchAfterMs` is set — a watchdog aborts the op early once the selector has
|
|
871
|
+
* matched nothing for that long.
|
|
845
872
|
*/
|
|
846
873
|
async #runOp<T>(
|
|
847
874
|
active: ActiveRun,
|
|
@@ -849,14 +876,28 @@ export class WorkerCore {
|
|
|
849
876
|
cellSignal: AbortSignal,
|
|
850
877
|
perOpTimeoutMs: number,
|
|
851
878
|
fn: (signal: AbortSignal) => Promise<T>,
|
|
879
|
+
opts?: { selector?: string; zeroMatchAfterMs?: number },
|
|
852
880
|
): Promise<T> {
|
|
853
881
|
const opId = active.opCounter++;
|
|
854
882
|
active.inflight.set(opId, { label, startedAt: Date.now() });
|
|
855
883
|
const capped = Number.isFinite(perOpTimeoutMs) && perOpTimeoutMs > 0;
|
|
856
884
|
const opTimeout = capped ? AbortSignal.timeout(perOpTimeoutMs) : undefined;
|
|
857
885
|
const opSignal = opTimeout ? AbortSignal.any([cellSignal, opTimeout]) : cellSignal;
|
|
886
|
+
const selector = opts?.selector;
|
|
887
|
+
const watchdog =
|
|
888
|
+
selector !== undefined && opts?.zeroMatchAfterMs !== undefined && parseAriaRefSelector(selector) === null
|
|
889
|
+
? { selector, afterMs: opts.zeroMatchAfterMs }
|
|
890
|
+
: undefined;
|
|
891
|
+
// Fired when the watchdog wins the race (tears down the in-flight action) and in
|
|
892
|
+
// the finally (stops the watchdog's polling once the op settles either way).
|
|
893
|
+
const earlyAc = new AbortController();
|
|
858
894
|
try {
|
|
859
|
-
return await fn(opSignal);
|
|
895
|
+
if (!watchdog) return await fn(opSignal);
|
|
896
|
+
const racedSignal = AbortSignal.any([opSignal, earlyAc.signal]);
|
|
897
|
+
return await Promise.race([
|
|
898
|
+
fn(racedSignal),
|
|
899
|
+
this.#zeroMatchWatchdog(watchdog.selector, label, watchdog.afterMs, racedSignal),
|
|
900
|
+
]);
|
|
860
901
|
} catch (err) {
|
|
861
902
|
// Fail fast with a named, attributable error instead of the opaque whole-cell timeout:
|
|
862
903
|
// our per-op deadline fired, or puppeteer's own (equal) timeout fired first — having
|
|
@@ -867,20 +908,76 @@ export class WorkerCore {
|
|
|
867
908
|
!cellSignal.aborted &&
|
|
868
909
|
(opTimeout?.aborted || (err instanceof Error && err.name === "TimeoutError"))
|
|
869
910
|
) {
|
|
870
|
-
|
|
911
|
+
const hint = selector ? await this.#selectorTimeoutHint(selector) : "";
|
|
912
|
+
throw new ToolError(`${label} timed out after ${perOpTimeoutMs}ms${hint}`);
|
|
871
913
|
}
|
|
872
914
|
throw err;
|
|
873
915
|
} finally {
|
|
916
|
+
earlyAc.abort();
|
|
874
917
|
active.inflight.delete(opId);
|
|
875
918
|
}
|
|
876
919
|
}
|
|
877
920
|
|
|
921
|
+
/**
|
|
922
|
+
* Fail-fast arm raced against a selector op: rejects once the selector has matched
|
|
923
|
+
* nothing for the whole `afterMs` window, so a wrong selector or wrong page (consent
|
|
924
|
+
* wall, pre-navigation document) costs ~2s instead of the full action deadline.
|
|
925
|
+
* Disarms — hangs until the settled race drops it — the moment at least one element
|
|
926
|
+
* matches; an inconclusive probe (mid-navigation, detached frame) never counts
|
|
927
|
+
* toward the zero-match window.
|
|
928
|
+
*/
|
|
929
|
+
async #zeroMatchWatchdog(selector: string, label: string, afterMs: number, signal: AbortSignal): Promise<never> {
|
|
930
|
+
const page = this.#requirePage();
|
|
931
|
+
const resolved = normalizeSelector(selector);
|
|
932
|
+
const deadline = Date.now() + afterMs;
|
|
933
|
+
while (!signal.aborted) {
|
|
934
|
+
let count: number | null = null;
|
|
935
|
+
try {
|
|
936
|
+
const handles = await page.$$(resolved);
|
|
937
|
+
count = handles.length;
|
|
938
|
+
for (const handle of handles) void handle.dispose().catch(() => undefined);
|
|
939
|
+
} catch {
|
|
940
|
+
// Inconclusive probe — keep polling without advancing toward failure.
|
|
941
|
+
}
|
|
942
|
+
if (count !== null && count > 0) break;
|
|
943
|
+
if (count === 0 && Date.now() >= deadline) {
|
|
944
|
+
throw new ToolError(`${label} failed fast after ${afterMs}ms${formatSelectorMatchHint(0)}`);
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
await untilAborted(signal, () => Bun.sleep(ZERO_MATCH_POLL_MS));
|
|
948
|
+
} catch {
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return await new Promise<never>(() => {});
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Best-effort match-count probe for a timed-out selector op. Never throws;
|
|
957
|
+
* empty string when the probe fails, stalls, or the selector is an aria-ref.
|
|
958
|
+
*/
|
|
959
|
+
async #selectorTimeoutHint(selector: string): Promise<string> {
|
|
960
|
+
if (parseAriaRefSelector(selector) !== null) return "";
|
|
961
|
+
try {
|
|
962
|
+
const handles = await Promise.race([
|
|
963
|
+
this.#requirePage().$$(normalizeSelector(selector)),
|
|
964
|
+
Bun.sleep(1_000).then(() => null),
|
|
965
|
+
]);
|
|
966
|
+
if (!handles) return "";
|
|
967
|
+
const count = handles.length;
|
|
968
|
+
for (const handle of handles) void handle.dispose().catch(() => undefined);
|
|
969
|
+
return formatSelectorMatchHint(count);
|
|
970
|
+
} catch {
|
|
971
|
+
return "";
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
878
975
|
#createTabApi(
|
|
879
976
|
name: string,
|
|
880
977
|
timeoutMs: number,
|
|
881
978
|
signal: AbortSignal,
|
|
882
979
|
session: SessionSnapshot,
|
|
883
|
-
|
|
980
|
+
output: RunOutput,
|
|
884
981
|
screenshots: ScreenshotResult[],
|
|
885
982
|
active: ActiveRun,
|
|
886
983
|
): TabApi {
|
|
@@ -888,8 +985,12 @@ export class WorkerCore {
|
|
|
888
985
|
const { quickOpMs, actionOpMs } = resolveOpTimeouts(timeoutMs);
|
|
889
986
|
const waitMs = (explicit?: number): number => resolveWaitTimeout(timeoutMs, explicit);
|
|
890
987
|
const INF = Number.POSITIVE_INFINITY;
|
|
891
|
-
const op = <T>(
|
|
892
|
-
|
|
988
|
+
const op = <T>(
|
|
989
|
+
label: string,
|
|
990
|
+
perOpMs: number,
|
|
991
|
+
fn: (sig: AbortSignal) => Promise<T>,
|
|
992
|
+
selectorOpts?: { selector?: string; zeroMatchAfterMs?: number },
|
|
993
|
+
): Promise<T> => this.#runOp(active, label, signal, perOpMs, fn, selectorOpts);
|
|
893
994
|
return {
|
|
894
995
|
name,
|
|
895
996
|
page,
|
|
@@ -929,7 +1030,7 @@ export class WorkerCore {
|
|
|
929
1030
|
),
|
|
930
1031
|
screenshot: opts =>
|
|
931
1032
|
op(describeScreenshot(opts), quickOpMs, sig =>
|
|
932
|
-
this.#captureScreenshot(session,
|
|
1033
|
+
this.#captureScreenshot(session, output, screenshots, sig, opts),
|
|
933
1034
|
),
|
|
934
1035
|
extract: (format = "markdown") =>
|
|
935
1036
|
op(`tab.extract(${JSON.stringify(format)})`, quickOpMs, async sig => {
|
|
@@ -949,51 +1050,62 @@ export class WorkerCore {
|
|
|
949
1050
|
return content;
|
|
950
1051
|
}),
|
|
951
1052
|
click: selector =>
|
|
952
|
-
op(
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1053
|
+
op(
|
|
1054
|
+
`tab.click(${JSON.stringify(selector)})`,
|
|
1055
|
+
actionOpMs,
|
|
1056
|
+
async sig => {
|
|
1057
|
+
if (parseAriaRefSelector(selector) !== null) {
|
|
1058
|
+
const handle = await this.#resolveAriaRef(selector);
|
|
1059
|
+
try {
|
|
1060
|
+
await untilAborted(sig, () => handle.click());
|
|
1061
|
+
} finally {
|
|
1062
|
+
await handle.dispose().catch(() => undefined);
|
|
1063
|
+
}
|
|
1064
|
+
return;
|
|
959
1065
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
if (resolved.startsWith("text/")) await clickQueryHandlerText(page, resolved, actionOpMs, sig);
|
|
964
|
-
else await untilAborted(sig, () => page.locator(resolved).setTimeout(actionOpMs).click({ signal: sig }));
|
|
965
|
-
}),
|
|
966
|
-
type: (selector, text) =>
|
|
967
|
-
op(`tab.type(${JSON.stringify(selector)})`, actionOpMs, async sig => {
|
|
968
|
-
const handle = await this.#resolveActionHandle(selector, actionOpMs, sig);
|
|
969
|
-
try {
|
|
970
|
-
await untilAborted(sig, () => handle.type(text, { delay: 0 }));
|
|
971
|
-
} finally {
|
|
972
|
-
await handle.dispose().catch(() => undefined);
|
|
973
|
-
}
|
|
974
|
-
}),
|
|
975
|
-
fill: (selector, value) =>
|
|
976
|
-
op(`tab.fill(${JSON.stringify(selector)})`, actionOpMs, async sig => {
|
|
977
|
-
if (parseAriaRefSelector(selector) !== null) {
|
|
978
|
-
const handle = await this.#resolveAriaRef(selector);
|
|
979
|
-
try {
|
|
1066
|
+
const resolved = normalizeSelector(selector);
|
|
1067
|
+
if (resolved.startsWith("text/")) await clickQueryHandlerText(page, resolved, actionOpMs, sig);
|
|
1068
|
+
else
|
|
980
1069
|
await untilAborted(sig, () =>
|
|
981
|
-
|
|
982
|
-
const node = el as unknown as { value?: string; focus?: () => void };
|
|
983
|
-
node.focus?.();
|
|
984
|
-
if ("value" in node) node.value = "";
|
|
985
|
-
}),
|
|
1070
|
+
page.locator(resolved).setTimeout(actionOpMs).click({ signal: sig }),
|
|
986
1071
|
);
|
|
987
|
-
|
|
1072
|
+
},
|
|
1073
|
+
{ selector, zeroMatchAfterMs: ZERO_MATCH_FAIL_FAST_MS },
|
|
1074
|
+
),
|
|
1075
|
+
type: (selector, text) =>
|
|
1076
|
+
op(
|
|
1077
|
+
`tab.type(${JSON.stringify(selector)})`,
|
|
1078
|
+
actionOpMs,
|
|
1079
|
+
async sig => {
|
|
1080
|
+
const handle = await this.#resolveActionHandle(selector, actionOpMs, sig);
|
|
1081
|
+
try {
|
|
1082
|
+
await untilAborted(sig, () => handle.type(text, { delay: 0 }));
|
|
988
1083
|
} finally {
|
|
989
1084
|
await handle.dispose().catch(() => undefined);
|
|
990
1085
|
}
|
|
991
|
-
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1086
|
+
},
|
|
1087
|
+
{ selector, zeroMatchAfterMs: ZERO_MATCH_FAIL_FAST_MS },
|
|
1088
|
+
),
|
|
1089
|
+
fill: (selector, value) =>
|
|
1090
|
+
op(
|
|
1091
|
+
`tab.fill(${JSON.stringify(selector)})`,
|
|
1092
|
+
actionOpMs,
|
|
1093
|
+
async sig => {
|
|
1094
|
+
if (parseAriaRefSelector(selector) !== null) {
|
|
1095
|
+
const handle = await this.#resolveAriaRef(selector);
|
|
1096
|
+
try {
|
|
1097
|
+
await fillViaHandle(handle, value, sig);
|
|
1098
|
+
} finally {
|
|
1099
|
+
await handle.dispose().catch(() => undefined);
|
|
1100
|
+
}
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
await untilAborted(sig, () =>
|
|
1104
|
+
page.locator(normalizeSelector(selector)).setTimeout(actionOpMs).fill(value, { signal: sig }),
|
|
1105
|
+
);
|
|
1106
|
+
},
|
|
1107
|
+
{ selector, zeroMatchAfterMs: ZERO_MATCH_FAIL_FAST_MS },
|
|
1108
|
+
),
|
|
997
1109
|
press: (key, opts) =>
|
|
998
1110
|
op(`tab.press(${JSON.stringify(key)})`, actionOpMs, async sig => {
|
|
999
1111
|
const selector = opts?.selector;
|
|
@@ -1005,23 +1117,37 @@ export class WorkerCore {
|
|
|
1005
1117
|
drag: (from, to) => op("tab.drag()", actionOpMs, sig => this.#drag(from, to, sig)),
|
|
1006
1118
|
waitFor: (selector, opts) => {
|
|
1007
1119
|
const w = waitMs(opts?.timeout);
|
|
1008
|
-
return op(
|
|
1009
|
-
|
|
1120
|
+
return op(
|
|
1121
|
+
`tab.waitFor(${JSON.stringify(selector)})`,
|
|
1122
|
+
w,
|
|
1123
|
+
async sig => toActionableHandle(await this.#resolveActionHandle(selector, w, sig)),
|
|
1124
|
+
{ selector, zeroMatchAfterMs: opts?.timeout === undefined ? ZERO_MATCH_FAIL_FAST_MS : undefined },
|
|
1010
1125
|
);
|
|
1011
1126
|
},
|
|
1012
1127
|
waitForSelector: (selector, opts) => {
|
|
1013
1128
|
const w = waitMs(opts?.timeout);
|
|
1014
|
-
return op(
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1129
|
+
return op(
|
|
1130
|
+
`tab.waitForSelector(${JSON.stringify(selector)})`,
|
|
1131
|
+
w,
|
|
1132
|
+
async sig => {
|
|
1133
|
+
if (parseAriaRefSelector(selector) !== null)
|
|
1134
|
+
return toActionableHandle(await this.#resolveAriaRef(selector));
|
|
1135
|
+
const handle = (await untilAborted(sig, () =>
|
|
1136
|
+
page.waitForSelector(normalizeSelector(selector), {
|
|
1137
|
+
timeout: w,
|
|
1138
|
+
visible: opts?.visible,
|
|
1139
|
+
hidden: opts?.hidden,
|
|
1140
|
+
signal: sig,
|
|
1141
|
+
}),
|
|
1142
|
+
)) as ElementHandle | null;
|
|
1143
|
+
return handle ? toActionableHandle(handle) : null;
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
selector,
|
|
1147
|
+
// `hidden: true` waits for zero matches — that is success, never a fast-fail.
|
|
1148
|
+
zeroMatchAfterMs: opts?.timeout === undefined && !opts?.hidden ? ZERO_MATCH_FAIL_FAST_MS : undefined,
|
|
1149
|
+
},
|
|
1150
|
+
);
|
|
1025
1151
|
},
|
|
1026
1152
|
waitForNavigation: opts => {
|
|
1027
1153
|
const w = waitMs(opts?.timeout);
|
|
@@ -1035,33 +1161,47 @@ export class WorkerCore {
|
|
|
1035
1161
|
op("tab.evaluate()", INF, sig =>
|
|
1036
1162
|
untilAborted(sig, () =>
|
|
1037
1163
|
typeof fn === "string"
|
|
1038
|
-
? page.evaluate(fn)
|
|
1039
|
-
: page
|
|
1164
|
+
? page.mainFrame().mainRealm().evaluate(fn)
|
|
1165
|
+
: page
|
|
1166
|
+
.mainFrame()
|
|
1167
|
+
.mainRealm()
|
|
1168
|
+
.evaluate(fn as (...a: unknown[]) => unknown, ...args),
|
|
1040
1169
|
),
|
|
1041
1170
|
) as never,
|
|
1042
1171
|
scrollIntoView: selector =>
|
|
1043
|
-
op(
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1172
|
+
op(
|
|
1173
|
+
`tab.scrollIntoView(${JSON.stringify(selector)})`,
|
|
1174
|
+
actionOpMs,
|
|
1175
|
+
async sig => {
|
|
1176
|
+
const handle = await this.#resolveActionHandle(selector, actionOpMs, sig);
|
|
1177
|
+
try {
|
|
1178
|
+
await untilAborted(sig, () =>
|
|
1179
|
+
handle.evaluate(el => {
|
|
1180
|
+
const target = el as unknown as {
|
|
1181
|
+
scrollIntoView: (opts: { behavior: string; block: string; inline: string }) => void;
|
|
1182
|
+
};
|
|
1183
|
+
target.scrollIntoView({ behavior: "instant", block: "center", inline: "center" });
|
|
1184
|
+
}),
|
|
1185
|
+
);
|
|
1186
|
+
} finally {
|
|
1187
|
+
await handle.dispose().catch(() => undefined);
|
|
1188
|
+
}
|
|
1189
|
+
},
|
|
1190
|
+
{ selector, zeroMatchAfterMs: ZERO_MATCH_FAIL_FAST_MS },
|
|
1191
|
+
),
|
|
1058
1192
|
select: (selector, ...values) =>
|
|
1059
|
-
op(
|
|
1060
|
-
|
|
1193
|
+
op(
|
|
1194
|
+
`tab.select(${JSON.stringify(selector)})`,
|
|
1195
|
+
actionOpMs,
|
|
1196
|
+
sig => this.#select(selector, values, actionOpMs, sig),
|
|
1197
|
+
{ selector, zeroMatchAfterMs: ZERO_MATCH_FAIL_FAST_MS },
|
|
1061
1198
|
),
|
|
1062
1199
|
uploadFile: (selector, ...filePaths) =>
|
|
1063
|
-
op(
|
|
1064
|
-
|
|
1200
|
+
op(
|
|
1201
|
+
`tab.uploadFile(${JSON.stringify(selector)})`,
|
|
1202
|
+
actionOpMs,
|
|
1203
|
+
sig => this.#uploadFile(selector, filePaths, actionOpMs, sig, session),
|
|
1204
|
+
{ selector, zeroMatchAfterMs: ZERO_MATCH_FAIL_FAST_MS },
|
|
1065
1205
|
),
|
|
1066
1206
|
waitForUrl: (pattern, opts) => {
|
|
1067
1207
|
const w = waitMs(opts?.timeout);
|
|
@@ -1071,8 +1211,8 @@ export class WorkerCore {
|
|
|
1071
1211
|
const w = waitMs(opts?.timeout);
|
|
1072
1212
|
return op("tab.waitForResponse()", w, sig => this.#waitForResponse(pattern, w, sig));
|
|
1073
1213
|
},
|
|
1074
|
-
id: id => this.#resolveCachedHandle(id),
|
|
1075
|
-
ref: id => this.#resolveAriaRef(id),
|
|
1214
|
+
id: async id => toActionableHandle(await this.#resolveCachedHandle(id)),
|
|
1215
|
+
ref: async id => toActionableHandle(await this.#resolveAriaRef(id)),
|
|
1076
1216
|
};
|
|
1077
1217
|
}
|
|
1078
1218
|
|
|
@@ -1122,12 +1262,19 @@ export class WorkerCore {
|
|
|
1122
1262
|
|
|
1123
1263
|
async #captureScreenshot(
|
|
1124
1264
|
session: SessionSnapshot,
|
|
1125
|
-
|
|
1265
|
+
output: RunOutput,
|
|
1126
1266
|
screenshots: ScreenshotResult[],
|
|
1127
1267
|
signal: AbortSignal | undefined,
|
|
1128
1268
|
opts: ScreenshotOptions = {},
|
|
1129
1269
|
): Promise<ScreenshotResult> {
|
|
1130
1270
|
const page = this.#requirePage();
|
|
1271
|
+
// Multiple tabs can share one Chromium (sibling headless tabs on a shared
|
|
1272
|
+
// endpoint, cdp/app attach). CDP `Page.captureScreenshot` reads the
|
|
1273
|
+
// compositor surface, which follows the *active* target — a backgrounded
|
|
1274
|
+
// page can stall waiting for a fresh frame (the 20s screenshot timeouts)
|
|
1275
|
+
// or hand back a sibling tab's pixels. Activate first; best-effort so an
|
|
1276
|
+
// already-active or freshly-closed target never fails the capture.
|
|
1277
|
+
await untilAborted(signal, () => page.bringToFront()).catch(() => undefined);
|
|
1131
1278
|
const fullPage = opts.selector ? false : (opts.fullPage ?? false);
|
|
1132
1279
|
// An explicit save path picks the full-res capture format: puppeteer encodes
|
|
1133
1280
|
// png/jpeg/webp natively, so `save: "shot.webp"` gets real WebP bytes instead
|
|
@@ -1199,8 +1346,8 @@ export class WorkerCore {
|
|
|
1199
1346
|
dest,
|
|
1200
1347
|
resized,
|
|
1201
1348
|
});
|
|
1202
|
-
|
|
1203
|
-
|
|
1349
|
+
output.push({ type: "text", text: lines.join("\n") });
|
|
1350
|
+
output.push({ type: "image", data: resized.data, mimeType: resized.mimeType });
|
|
1204
1351
|
}
|
|
1205
1352
|
return info;
|
|
1206
1353
|
}
|