@oh-my-pi/pi-coding-agent 16.4.2 → 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 +42 -0
- package/dist/cli.js +17514 -17799
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +10 -22
- 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/status-line/component.d.ts +3 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +8 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +16 -0
- package/dist/types/task/executor.d.ts +31 -0
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +1 -0
- 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/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +16 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/vibe.d.ts +161 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/dist/types/vibe/runtime.d.ts +124 -0
- package/dist/types/vibe/state.d.ts +4 -0
- package/dist/types/web/search/provider.d.ts +2 -0
- package/dist/types/web/search/providers/bing.d.ts +14 -0
- package/dist/types/web/search/providers/browser-headers.d.ts +9 -0
- package/dist/types/web/search/providers/browser-page.d.ts +32 -0
- package/dist/types/web/search/providers/ecosia.d.ts +14 -0
- package/dist/types/web/search/providers/google.d.ts +13 -0
- package/dist/types/web/search/providers/mojeek.d.ts +14 -0
- package/dist/types/web/search/providers/public.d.ts +37 -0
- package/dist/types/web/search/providers/startpage.d.ts +14 -0
- package/dist/types/web/search/providers/yahoo.d.ts +14 -0
- package/dist/types/web/search/types.d.ts +28 -0
- package/package.json +15 -18
- package/scripts/build-binary.ts +56 -73
- package/scripts/bundle-dist.ts +36 -40
- package/scripts/compile-binary.ts +68 -0
- package/scripts/generate-docs-index.ts +3 -93
- package/scripts/legacy-pi-virtual-module.ts +192 -0
- 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/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/edit/index.ts +10 -76
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +328 -162
- package/src/extensibility/plugins/legacy-pi-virtual-modules.d.ts +4 -0
- package/src/internal-urls/docs-index.ts +2 -1
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +65 -1
- 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/status-line/component.ts +6 -0
- package/src/modes/components/status-line/segments.ts +6 -0
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/interactive-mode.ts +117 -1
- package/src/modes/types.ts +2 -0
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/system-prompt.md +9 -2
- package/src/prompts/system/vibe-mode-active.md +23 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/vibe-kill.md +3 -0
- package/src/prompts/tools/vibe-list.md +3 -0
- package/src/prompts/tools/vibe-send.md +9 -0
- package/src/prompts/tools/vibe-spawn.md +10 -0
- package/src/prompts/tools/vibe-turn-result.md +19 -0
- package/src/prompts/tools/vibe-wait.md +8 -0
- package/src/sdk.ts +5 -0
- package/src/session/agent-session.ts +103 -13
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/slash-commands/builtin-registry.ts +24 -8
- package/src/task/agents.ts +0 -2
- package/src/task/executor.ts +105 -0
- package/src/tools/__tests__/vibe-render.test.ts +210 -0
- package/src/tools/bash-skill-urls.ts +1 -1
- package/src/tools/browser/cmux/cmux-tab.ts +46 -48
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +264 -129
- package/src/tools/glob.ts +20 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/read.ts +17 -9
- package/src/tools/renderers.ts +6 -0
- package/src/tools/vibe.ts +608 -0
- package/src/tools/write.ts +15 -2
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +70 -7
- package/src/vibe/runtime.ts +710 -0
- package/src/vibe/state.ts +4 -0
- package/src/web/search/index.ts +14 -6
- package/src/web/search/provider.ts +37 -1
- package/src/web/search/providers/bing.ts +197 -0
- package/src/web/search/providers/browser-headers.ts +92 -0
- package/src/web/search/providers/browser-page.ts +123 -0
- package/src/web/search/providers/duckduckgo.ts +13 -34
- package/src/web/search/providers/ecosia.ts +178 -0
- package/src/web/search/providers/google.ts +193 -0
- package/src/web/search/providers/mojeek.ts +206 -0
- package/src/web/search/providers/public.ts +201 -0
- package/src/web/search/providers/startpage.ts +213 -0
- package/src/web/search/providers/yahoo.ts +179 -0
- package/src/web/search/types.ts +35 -0
- package/dist/types/extensibility/plugins/legacy-pi-bundled-keys.d.ts +0 -10
- package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +0 -10
- package/scripts/generate-legacy-pi-bundled-registry.ts +0 -420
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +0 -1011
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +0 -3430
- package/src/internal-urls/docs-index.generated.txt +0 -2
- package/src/prompts/agents/plan.md +0 -47
|
@@ -17,7 +17,6 @@ import type {
|
|
|
17
17
|
Target,
|
|
18
18
|
} from "puppeteer-core";
|
|
19
19
|
import { JsRuntime, type RuntimeHooks } from "../../eval/js/shared/runtime";
|
|
20
|
-
import type { JsDisplayOutput } from "../../eval/js/shared/types";
|
|
21
20
|
import { resizeImage } from "../../utils/image-resize";
|
|
22
21
|
import { resolveToCwd } from "../path-utils";
|
|
23
22
|
import { formatScreenshot } from "../render-utils";
|
|
@@ -37,12 +36,12 @@ import {
|
|
|
37
36
|
} from "./launch";
|
|
38
37
|
import { extractReadableFromHtml, type ReadableFormat } from "./readable";
|
|
39
38
|
import { markHandled, waitForBrowserRun } from "./run-cancellation";
|
|
39
|
+
import { cloneSafe, RunOutput } from "./run-output";
|
|
40
40
|
import type {
|
|
41
41
|
Observation,
|
|
42
42
|
ObservationEntry,
|
|
43
43
|
ReadyInfo,
|
|
44
44
|
RunErrorPayload,
|
|
45
|
-
RunResultOk,
|
|
46
45
|
ScreenshotResult,
|
|
47
46
|
SessionSnapshot,
|
|
48
47
|
ToolReply,
|
|
@@ -116,15 +115,27 @@ type ActionabilityResult = { ok: true; x: number; y: number } | { ok: false; rea
|
|
|
116
115
|
* - `QUICK_OP_TIMEOUT_MS`: page-coupled reads that should resolve fast (`observe`,
|
|
117
116
|
* `screenshot`, `extract`, `ariaSnapshot`).
|
|
118
117
|
* - `ACTION_OP_TIMEOUT_MS`: interactive point actions (`click`, `fill`, `type`, …) and
|
|
119
|
-
* the default for wait helpers when no explicit `{ timeout }` is given.
|
|
118
|
+
* the default for wait helpers when no explicit `{ timeout }` is given. Selector ops
|
|
119
|
+
* additionally fail fast after `ZERO_MATCH_FAIL_FAST_MS` of confirmed zero matches
|
|
120
|
+
* (see `#zeroMatchWatchdog`), so the full ceiling is only spent on elements that
|
|
121
|
+
* exist but are not yet actionable.
|
|
120
122
|
*
|
|
121
123
|
* `goto` and `evaluate` stay uncapped (`Number.POSITIVE_INFINITY`): navigation and user
|
|
122
124
|
* code legitimately use the full cell budget.
|
|
123
125
|
*/
|
|
124
126
|
const QUICK_OP_TIMEOUT_MS = 20_000;
|
|
125
|
-
const ACTION_OP_TIMEOUT_MS =
|
|
127
|
+
const ACTION_OP_TIMEOUT_MS = 8_000;
|
|
126
128
|
/** Headroom subtracted from the cell budget so a per-op deadline fires before it. */
|
|
127
129
|
const OP_DEADLINE_SLACK_MS = 1_000;
|
|
130
|
+
/**
|
|
131
|
+
* A selector op whose selector has matched nothing for this long fails fast with the
|
|
132
|
+
* zero-match hint instead of burning the rest of its deadline: a wrong selector or a
|
|
133
|
+
* wrong page (consent wall, pre-navigation document) is the common agent failure and
|
|
134
|
+
* should cost ~2s, not the full action ceiling. Explicit `{ timeout }` waits opt out.
|
|
135
|
+
*/
|
|
136
|
+
const ZERO_MATCH_FAIL_FAST_MS = 2_000;
|
|
137
|
+
/** Poll cadence for the zero-match watchdog. */
|
|
138
|
+
const ZERO_MATCH_POLL_MS = 250;
|
|
128
139
|
|
|
129
140
|
export interface OpTimeouts {
|
|
130
141
|
/** Largest per-op deadline allowed — strictly below the cell budget. */
|
|
@@ -190,7 +201,7 @@ interface TabApi {
|
|
|
190
201
|
press(key: KeyInput, opts?: { selector?: string }): Promise<void>;
|
|
191
202
|
scroll(deltaX: number, deltaY: number): Promise<void>;
|
|
192
203
|
drag(from: DragTarget, to: DragTarget): Promise<void>;
|
|
193
|
-
waitFor(selector: string, opts?: { timeout?: number }): Promise<
|
|
204
|
+
waitFor(selector: string, opts?: { timeout?: number }): Promise<ActionableHandle>;
|
|
194
205
|
evaluate<TResult, TArgs extends unknown[]>(
|
|
195
206
|
fn: string | ((...args: TArgs) => TResult | Promise<TResult>),
|
|
196
207
|
...args: TArgs
|
|
@@ -206,13 +217,13 @@ interface TabApi {
|
|
|
206
217
|
waitForSelector(
|
|
207
218
|
selector: string,
|
|
208
219
|
opts?: { timeout?: number; visible?: boolean; hidden?: boolean },
|
|
209
|
-
): Promise<
|
|
220
|
+
): Promise<ActionableHandle | null>;
|
|
210
221
|
waitForNavigation(opts?: {
|
|
211
222
|
waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
|
|
212
223
|
timeout?: number;
|
|
213
224
|
}): Promise<HTTPResponse | null>;
|
|
214
|
-
id(n: number): Promise<
|
|
215
|
-
ref(id: string): Promise<
|
|
225
|
+
id(n: number): Promise<ActionableHandle>;
|
|
226
|
+
ref(id: string): Promise<ActionableHandle>;
|
|
216
227
|
}
|
|
217
228
|
|
|
218
229
|
export function normalizeSelector(selector: string): string {
|
|
@@ -259,16 +270,30 @@ function asElementHandle(handle: unknown): ElementHandle | null {
|
|
|
259
270
|
return handle ? (handle as ElementHandle) : null;
|
|
260
271
|
}
|
|
261
272
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
273
|
+
/** ElementHandle enriched with the `fill()` the tool docs promise on handles from `tab.id()`/`tab.ref()`/`tab.waitFor()`. */
|
|
274
|
+
export type ActionableHandle = ElementHandle & { fill(value: string): Promise<void> };
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Attach `fill()` to a puppeteer ElementHandle before handing it to user code.
|
|
278
|
+
* Puppeteer handles expose `type()` but no `fill()`; the semantics mirror the
|
|
279
|
+
* selector-based `tab.fill()`: focus, clear any existing value, then type.
|
|
280
|
+
*/
|
|
281
|
+
export function toActionableHandle(handle: ElementHandle): ActionableHandle {
|
|
282
|
+
const enriched = handle as ActionableHandle;
|
|
283
|
+
enriched.fill = value => fillViaHandle(enriched, value);
|
|
284
|
+
return enriched;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Focus, clear any existing value, then retype — shared by `tab.fill(aria-ref)` and enriched handles. */
|
|
288
|
+
async function fillViaHandle(handle: ElementHandle, value: string, signal?: AbortSignal): Promise<void> {
|
|
289
|
+
await untilAborted(signal, () =>
|
|
290
|
+
handle.evaluate(el => {
|
|
291
|
+
const node = el as unknown as { value?: string; focus?: () => void };
|
|
292
|
+
node.focus?.();
|
|
293
|
+
if ("value" in node) node.value = "";
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
await untilAborted(signal, () => handle.type(value, { delay: 0 }));
|
|
272
297
|
}
|
|
273
298
|
|
|
274
299
|
/**
|
|
@@ -303,14 +328,6 @@ function errorPayload(error: unknown): RunErrorPayload {
|
|
|
303
328
|
return { name: "Error", message: String(error), isToolError: false, isAbort: false };
|
|
304
329
|
}
|
|
305
330
|
|
|
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
331
|
function replyError(payload: RunErrorPayload): Error {
|
|
315
332
|
if (payload.isAbort) {
|
|
316
333
|
const err = new ToolAbortError(payload.message || "Tool call aborted");
|
|
@@ -508,6 +525,17 @@ async function clickQueryHandlerText(
|
|
|
508
525
|
);
|
|
509
526
|
}
|
|
510
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Hint appended to a selector op's fail-fast timeout, given the selector's current
|
|
530
|
+
* match count: a missing element (consent wall, wrong page) reads differently from
|
|
531
|
+
* a present-but-unactionable one.
|
|
532
|
+
*/
|
|
533
|
+
export function formatSelectorMatchHint(count: number): string {
|
|
534
|
+
return count === 0
|
|
535
|
+
? "; selector currently matches no elements — run tab.observe() or tab.ariaSnapshot() to inspect the page"
|
|
536
|
+
: `; 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)`;
|
|
537
|
+
}
|
|
538
|
+
|
|
511
539
|
export interface InflightOp {
|
|
512
540
|
label: string;
|
|
513
541
|
startedAt: number;
|
|
@@ -517,7 +545,7 @@ interface ActiveRun {
|
|
|
517
545
|
id: string;
|
|
518
546
|
ac: AbortController;
|
|
519
547
|
signal: AbortSignal;
|
|
520
|
-
|
|
548
|
+
output: RunOutput;
|
|
521
549
|
screenshots: ScreenshotResult[];
|
|
522
550
|
pendingTools: Map<string, { resolve(value: unknown): void; reject(error: Error): void }>;
|
|
523
551
|
/** Helper invocations currently awaiting the page/network, keyed by op id. */
|
|
@@ -706,13 +734,13 @@ export class WorkerCore {
|
|
|
706
734
|
const ac = new AbortController();
|
|
707
735
|
const runAc = new AbortController();
|
|
708
736
|
const signal = AbortSignal.any([timeoutSignal, ac.signal, runAc.signal]);
|
|
709
|
-
const
|
|
737
|
+
const output = new RunOutput();
|
|
710
738
|
const screenshots: ScreenshotResult[] = [];
|
|
711
739
|
const active: ActiveRun = {
|
|
712
740
|
id: msg.id,
|
|
713
741
|
ac,
|
|
714
742
|
signal,
|
|
715
|
-
|
|
743
|
+
output,
|
|
716
744
|
screenshots,
|
|
717
745
|
pendingTools: new Map(),
|
|
718
746
|
inflight: new Map(),
|
|
@@ -723,7 +751,7 @@ export class WorkerCore {
|
|
|
723
751
|
throwIfAborted(signal);
|
|
724
752
|
const page = this.#requirePage();
|
|
725
753
|
const browser = this.#requireBrowser();
|
|
726
|
-
const tabApi = this.#createTabApi(msg.name, msg.timeoutMs, signal, msg.session,
|
|
754
|
+
const tabApi = this.#createTabApi(msg.name, msg.timeoutMs, signal, msg.session, output, screenshots, active);
|
|
727
755
|
const runtime = this.#ensureRuntime(msg.session);
|
|
728
756
|
runtime.setCwd(msg.session.cwd);
|
|
729
757
|
runtime.setRunScope({
|
|
@@ -774,7 +802,7 @@ export class WorkerCore {
|
|
|
774
802
|
type: "result",
|
|
775
803
|
id: msg.id,
|
|
776
804
|
ok: true,
|
|
777
|
-
payload: { displays, returnValue: cloneSafe(returnValue), screenshots },
|
|
805
|
+
payload: { displays: output.finish(), returnValue: cloneSafe(returnValue), screenshots },
|
|
778
806
|
});
|
|
779
807
|
} finally {
|
|
780
808
|
signal.removeEventListener("abort", onCancel);
|
|
@@ -802,11 +830,12 @@ export class WorkerCore {
|
|
|
802
830
|
return {
|
|
803
831
|
onText: chunk => {
|
|
804
832
|
throwIfAborted(active.signal);
|
|
833
|
+
active.output.pushText(chunk);
|
|
805
834
|
this.#log("debug", chunk.replace(/\n$/, ""));
|
|
806
835
|
},
|
|
807
836
|
onDisplay: output => {
|
|
808
837
|
throwIfAborted(active.signal);
|
|
809
|
-
|
|
838
|
+
active.output.pushDisplay(output);
|
|
810
839
|
},
|
|
811
840
|
callTool: (name, args) => {
|
|
812
841
|
throwIfAborted(active.signal);
|
|
@@ -815,20 +844,6 @@ export class WorkerCore {
|
|
|
815
844
|
};
|
|
816
845
|
}
|
|
817
846
|
|
|
818
|
-
#pushDisplay(displays: RunResultOk["displays"], output: JsDisplayOutput): void {
|
|
819
|
-
if (output.type === "image") {
|
|
820
|
-
displays.push({ type: "image", data: output.data, mimeType: output.mimeType });
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
if (output.type === "json") {
|
|
824
|
-
displays.push({ type: "text", text: safeJsonStringify(output.data) });
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
// status — surface as compact JSON so helper side effects (read/write/env) appear in
|
|
828
|
-
// the cell result alongside explicit display() output.
|
|
829
|
-
displays.push({ type: "text", text: safeJsonStringify(output.event) });
|
|
830
|
-
}
|
|
831
|
-
|
|
832
847
|
async #callTool(active: ActiveRun, name: string, args: unknown): Promise<unknown> {
|
|
833
848
|
const id = `tab-tc-${active.id}-${crypto.randomUUID()}`;
|
|
834
849
|
const { promise, resolve, reject } = Promise.withResolvers<unknown>();
|
|
@@ -853,7 +868,10 @@ export class WorkerCore {
|
|
|
853
868
|
* with a named error instead of silently consuming the whole cell budget. Pass
|
|
854
869
|
* `Number.POSITIVE_INFINITY` for `perOpTimeoutMs` to bound the op only by the cell
|
|
855
870
|
* budget (used for `evaluate` running user code and for locator helpers that already
|
|
856
|
-
* carry puppeteer's own `.setTimeout(timeoutMs)`).
|
|
871
|
+
* carry puppeteer's own `.setTimeout(timeoutMs)`). When the op targets a `selector`,
|
|
872
|
+
* the fail-fast timeout carries a best-effort match-count hint, and — when
|
|
873
|
+
* `zeroMatchAfterMs` is set — a watchdog aborts the op early once the selector has
|
|
874
|
+
* matched nothing for that long.
|
|
857
875
|
*/
|
|
858
876
|
async #runOp<T>(
|
|
859
877
|
active: ActiveRun,
|
|
@@ -861,14 +879,28 @@ export class WorkerCore {
|
|
|
861
879
|
cellSignal: AbortSignal,
|
|
862
880
|
perOpTimeoutMs: number,
|
|
863
881
|
fn: (signal: AbortSignal) => Promise<T>,
|
|
882
|
+
opts?: { selector?: string; zeroMatchAfterMs?: number },
|
|
864
883
|
): Promise<T> {
|
|
865
884
|
const opId = active.opCounter++;
|
|
866
885
|
active.inflight.set(opId, { label, startedAt: Date.now() });
|
|
867
886
|
const capped = Number.isFinite(perOpTimeoutMs) && perOpTimeoutMs > 0;
|
|
868
887
|
const opTimeout = capped ? AbortSignal.timeout(perOpTimeoutMs) : undefined;
|
|
869
888
|
const opSignal = opTimeout ? AbortSignal.any([cellSignal, opTimeout]) : cellSignal;
|
|
889
|
+
const selector = opts?.selector;
|
|
890
|
+
const watchdog =
|
|
891
|
+
selector !== undefined && opts?.zeroMatchAfterMs !== undefined && parseAriaRefSelector(selector) === null
|
|
892
|
+
? { selector, afterMs: opts.zeroMatchAfterMs }
|
|
893
|
+
: undefined;
|
|
894
|
+
// Fired when the watchdog wins the race (tears down the in-flight action) and in
|
|
895
|
+
// the finally (stops the watchdog's polling once the op settles either way).
|
|
896
|
+
const earlyAc = new AbortController();
|
|
870
897
|
try {
|
|
871
|
-
return await fn(opSignal);
|
|
898
|
+
if (!watchdog) return await fn(opSignal);
|
|
899
|
+
const racedSignal = AbortSignal.any([opSignal, earlyAc.signal]);
|
|
900
|
+
return await Promise.race([
|
|
901
|
+
fn(racedSignal),
|
|
902
|
+
this.#zeroMatchWatchdog(watchdog.selector, label, watchdog.afterMs, racedSignal),
|
|
903
|
+
]);
|
|
872
904
|
} catch (err) {
|
|
873
905
|
// Fail fast with a named, attributable error instead of the opaque whole-cell timeout:
|
|
874
906
|
// our per-op deadline fired, or puppeteer's own (equal) timeout fired first — having
|
|
@@ -879,20 +911,76 @@ export class WorkerCore {
|
|
|
879
911
|
!cellSignal.aborted &&
|
|
880
912
|
(opTimeout?.aborted || (err instanceof Error && err.name === "TimeoutError"))
|
|
881
913
|
) {
|
|
882
|
-
|
|
914
|
+
const hint = selector ? await this.#selectorTimeoutHint(selector) : "";
|
|
915
|
+
throw new ToolError(`${label} timed out after ${perOpTimeoutMs}ms${hint}`);
|
|
883
916
|
}
|
|
884
917
|
throw err;
|
|
885
918
|
} finally {
|
|
919
|
+
earlyAc.abort();
|
|
886
920
|
active.inflight.delete(opId);
|
|
887
921
|
}
|
|
888
922
|
}
|
|
889
923
|
|
|
924
|
+
/**
|
|
925
|
+
* Fail-fast arm raced against a selector op: rejects once the selector has matched
|
|
926
|
+
* nothing for the whole `afterMs` window, so a wrong selector or wrong page (consent
|
|
927
|
+
* wall, pre-navigation document) costs ~2s instead of the full action deadline.
|
|
928
|
+
* Disarms — hangs until the settled race drops it — the moment at least one element
|
|
929
|
+
* matches; an inconclusive probe (mid-navigation, detached frame) never counts
|
|
930
|
+
* toward the zero-match window.
|
|
931
|
+
*/
|
|
932
|
+
async #zeroMatchWatchdog(selector: string, label: string, afterMs: number, signal: AbortSignal): Promise<never> {
|
|
933
|
+
const page = this.#requirePage();
|
|
934
|
+
const resolved = normalizeSelector(selector);
|
|
935
|
+
const deadline = Date.now() + afterMs;
|
|
936
|
+
while (!signal.aborted) {
|
|
937
|
+
let count: number | null = null;
|
|
938
|
+
try {
|
|
939
|
+
const handles = await page.$$(resolved);
|
|
940
|
+
count = handles.length;
|
|
941
|
+
for (const handle of handles) void handle.dispose().catch(() => undefined);
|
|
942
|
+
} catch {
|
|
943
|
+
// Inconclusive probe — keep polling without advancing toward failure.
|
|
944
|
+
}
|
|
945
|
+
if (count !== null && count > 0) break;
|
|
946
|
+
if (count === 0 && Date.now() >= deadline) {
|
|
947
|
+
throw new ToolError(`${label} failed fast after ${afterMs}ms${formatSelectorMatchHint(0)}`);
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
await untilAborted(signal, () => Bun.sleep(ZERO_MATCH_POLL_MS));
|
|
951
|
+
} catch {
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return await new Promise<never>(() => {});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Best-effort match-count probe for a timed-out selector op. Never throws;
|
|
960
|
+
* empty string when the probe fails, stalls, or the selector is an aria-ref.
|
|
961
|
+
*/
|
|
962
|
+
async #selectorTimeoutHint(selector: string): Promise<string> {
|
|
963
|
+
if (parseAriaRefSelector(selector) !== null) return "";
|
|
964
|
+
try {
|
|
965
|
+
const handles = await Promise.race([
|
|
966
|
+
this.#requirePage().$$(normalizeSelector(selector)),
|
|
967
|
+
Bun.sleep(1_000).then(() => null),
|
|
968
|
+
]);
|
|
969
|
+
if (!handles) return "";
|
|
970
|
+
const count = handles.length;
|
|
971
|
+
for (const handle of handles) void handle.dispose().catch(() => undefined);
|
|
972
|
+
return formatSelectorMatchHint(count);
|
|
973
|
+
} catch {
|
|
974
|
+
return "";
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
890
978
|
#createTabApi(
|
|
891
979
|
name: string,
|
|
892
980
|
timeoutMs: number,
|
|
893
981
|
signal: AbortSignal,
|
|
894
982
|
session: SessionSnapshot,
|
|
895
|
-
|
|
983
|
+
output: RunOutput,
|
|
896
984
|
screenshots: ScreenshotResult[],
|
|
897
985
|
active: ActiveRun,
|
|
898
986
|
): TabApi {
|
|
@@ -900,8 +988,12 @@ export class WorkerCore {
|
|
|
900
988
|
const { quickOpMs, actionOpMs } = resolveOpTimeouts(timeoutMs);
|
|
901
989
|
const waitMs = (explicit?: number): number => resolveWaitTimeout(timeoutMs, explicit);
|
|
902
990
|
const INF = Number.POSITIVE_INFINITY;
|
|
903
|
-
const op = <T>(
|
|
904
|
-
|
|
991
|
+
const op = <T>(
|
|
992
|
+
label: string,
|
|
993
|
+
perOpMs: number,
|
|
994
|
+
fn: (sig: AbortSignal) => Promise<T>,
|
|
995
|
+
selectorOpts?: { selector?: string; zeroMatchAfterMs?: number },
|
|
996
|
+
): Promise<T> => markHandled(this.#runOp(active, label, signal, perOpMs, fn, selectorOpts));
|
|
905
997
|
return {
|
|
906
998
|
name,
|
|
907
999
|
page,
|
|
@@ -941,7 +1033,7 @@ export class WorkerCore {
|
|
|
941
1033
|
),
|
|
942
1034
|
screenshot: opts =>
|
|
943
1035
|
op(describeScreenshot(opts), quickOpMs, sig =>
|
|
944
|
-
this.#captureScreenshot(session,
|
|
1036
|
+
this.#captureScreenshot(session, output, screenshots, sig, opts),
|
|
945
1037
|
),
|
|
946
1038
|
extract: (format = "markdown") =>
|
|
947
1039
|
op(`tab.extract(${JSON.stringify(format)})`, quickOpMs, async sig => {
|
|
@@ -961,51 +1053,62 @@ export class WorkerCore {
|
|
|
961
1053
|
return content;
|
|
962
1054
|
}),
|
|
963
1055
|
click: selector =>
|
|
964
|
-
op(
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1056
|
+
op(
|
|
1057
|
+
`tab.click(${JSON.stringify(selector)})`,
|
|
1058
|
+
actionOpMs,
|
|
1059
|
+
async sig => {
|
|
1060
|
+
if (parseAriaRefSelector(selector) !== null) {
|
|
1061
|
+
const handle = await this.#resolveAriaRef(selector);
|
|
1062
|
+
try {
|
|
1063
|
+
await untilAborted(sig, () => handle.click());
|
|
1064
|
+
} finally {
|
|
1065
|
+
await handle.dispose().catch(() => undefined);
|
|
1066
|
+
}
|
|
1067
|
+
return;
|
|
971
1068
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
if (resolved.startsWith("text/")) await clickQueryHandlerText(page, resolved, actionOpMs, sig);
|
|
976
|
-
else await untilAborted(sig, () => page.locator(resolved).setTimeout(actionOpMs).click({ signal: sig }));
|
|
977
|
-
}),
|
|
978
|
-
type: (selector, text) =>
|
|
979
|
-
op(`tab.type(${JSON.stringify(selector)})`, actionOpMs, async sig => {
|
|
980
|
-
const handle = await this.#resolveActionHandle(selector, actionOpMs, sig);
|
|
981
|
-
try {
|
|
982
|
-
await untilAborted(sig, () => handle.type(text, { delay: 0 }));
|
|
983
|
-
} finally {
|
|
984
|
-
await handle.dispose().catch(() => undefined);
|
|
985
|
-
}
|
|
986
|
-
}),
|
|
987
|
-
fill: (selector, value) =>
|
|
988
|
-
op(`tab.fill(${JSON.stringify(selector)})`, actionOpMs, async sig => {
|
|
989
|
-
if (parseAriaRefSelector(selector) !== null) {
|
|
990
|
-
const handle = await this.#resolveAriaRef(selector);
|
|
991
|
-
try {
|
|
1069
|
+
const resolved = normalizeSelector(selector);
|
|
1070
|
+
if (resolved.startsWith("text/")) await clickQueryHandlerText(page, resolved, actionOpMs, sig);
|
|
1071
|
+
else
|
|
992
1072
|
await untilAborted(sig, () =>
|
|
993
|
-
|
|
994
|
-
const node = el as unknown as { value?: string; focus?: () => void };
|
|
995
|
-
node.focus?.();
|
|
996
|
-
if ("value" in node) node.value = "";
|
|
997
|
-
}),
|
|
1073
|
+
page.locator(resolved).setTimeout(actionOpMs).click({ signal: sig }),
|
|
998
1074
|
);
|
|
999
|
-
|
|
1075
|
+
},
|
|
1076
|
+
{ selector, zeroMatchAfterMs: ZERO_MATCH_FAIL_FAST_MS },
|
|
1077
|
+
),
|
|
1078
|
+
type: (selector, text) =>
|
|
1079
|
+
op(
|
|
1080
|
+
`tab.type(${JSON.stringify(selector)})`,
|
|
1081
|
+
actionOpMs,
|
|
1082
|
+
async sig => {
|
|
1083
|
+
const handle = await this.#resolveActionHandle(selector, actionOpMs, sig);
|
|
1084
|
+
try {
|
|
1085
|
+
await untilAborted(sig, () => handle.type(text, { delay: 0 }));
|
|
1000
1086
|
} finally {
|
|
1001
1087
|
await handle.dispose().catch(() => undefined);
|
|
1002
1088
|
}
|
|
1003
|
-
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1089
|
+
},
|
|
1090
|
+
{ selector, zeroMatchAfterMs: ZERO_MATCH_FAIL_FAST_MS },
|
|
1091
|
+
),
|
|
1092
|
+
fill: (selector, value) =>
|
|
1093
|
+
op(
|
|
1094
|
+
`tab.fill(${JSON.stringify(selector)})`,
|
|
1095
|
+
actionOpMs,
|
|
1096
|
+
async sig => {
|
|
1097
|
+
if (parseAriaRefSelector(selector) !== null) {
|
|
1098
|
+
const handle = await this.#resolveAriaRef(selector);
|
|
1099
|
+
try {
|
|
1100
|
+
await fillViaHandle(handle, value, sig);
|
|
1101
|
+
} finally {
|
|
1102
|
+
await handle.dispose().catch(() => undefined);
|
|
1103
|
+
}
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
await untilAborted(sig, () =>
|
|
1107
|
+
page.locator(normalizeSelector(selector)).setTimeout(actionOpMs).fill(value, { signal: sig }),
|
|
1108
|
+
);
|
|
1109
|
+
},
|
|
1110
|
+
{ selector, zeroMatchAfterMs: ZERO_MATCH_FAIL_FAST_MS },
|
|
1111
|
+
),
|
|
1009
1112
|
press: (key, opts) =>
|
|
1010
1113
|
op(`tab.press(${JSON.stringify(key)})`, actionOpMs, async sig => {
|
|
1011
1114
|
const selector = opts?.selector;
|
|
@@ -1017,23 +1120,37 @@ export class WorkerCore {
|
|
|
1017
1120
|
drag: (from, to) => op("tab.drag()", actionOpMs, sig => this.#drag(from, to, sig)),
|
|
1018
1121
|
waitFor: (selector, opts) => {
|
|
1019
1122
|
const w = waitMs(opts?.timeout);
|
|
1020
|
-
return op(
|
|
1021
|
-
|
|
1123
|
+
return op(
|
|
1124
|
+
`tab.waitFor(${JSON.stringify(selector)})`,
|
|
1125
|
+
w,
|
|
1126
|
+
async sig => toActionableHandle(await this.#resolveActionHandle(selector, w, sig)),
|
|
1127
|
+
{ selector, zeroMatchAfterMs: opts?.timeout === undefined ? ZERO_MATCH_FAIL_FAST_MS : undefined },
|
|
1022
1128
|
);
|
|
1023
1129
|
},
|
|
1024
1130
|
waitForSelector: (selector, opts) => {
|
|
1025
1131
|
const w = waitMs(opts?.timeout);
|
|
1026
|
-
return op(
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1132
|
+
return op(
|
|
1133
|
+
`tab.waitForSelector(${JSON.stringify(selector)})`,
|
|
1134
|
+
w,
|
|
1135
|
+
async sig => {
|
|
1136
|
+
if (parseAriaRefSelector(selector) !== null)
|
|
1137
|
+
return toActionableHandle(await this.#resolveAriaRef(selector));
|
|
1138
|
+
const handle = (await untilAborted(sig, () =>
|
|
1139
|
+
page.waitForSelector(normalizeSelector(selector), {
|
|
1140
|
+
timeout: w,
|
|
1141
|
+
visible: opts?.visible,
|
|
1142
|
+
hidden: opts?.hidden,
|
|
1143
|
+
signal: sig,
|
|
1144
|
+
}),
|
|
1145
|
+
)) as ElementHandle | null;
|
|
1146
|
+
return handle ? toActionableHandle(handle) : null;
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
selector,
|
|
1150
|
+
// `hidden: true` waits for zero matches — that is success, never a fast-fail.
|
|
1151
|
+
zeroMatchAfterMs: opts?.timeout === undefined && !opts?.hidden ? ZERO_MATCH_FAIL_FAST_MS : undefined,
|
|
1152
|
+
},
|
|
1153
|
+
);
|
|
1037
1154
|
},
|
|
1038
1155
|
waitForNavigation: opts => {
|
|
1039
1156
|
const w = waitMs(opts?.timeout);
|
|
@@ -1052,28 +1169,39 @@ export class WorkerCore {
|
|
|
1052
1169
|
),
|
|
1053
1170
|
) as never,
|
|
1054
1171
|
scrollIntoView: selector =>
|
|
1055
|
-
op(
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
+
),
|
|
1070
1192
|
select: (selector, ...values) =>
|
|
1071
|
-
op(
|
|
1072
|
-
|
|
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 },
|
|
1073
1198
|
),
|
|
1074
1199
|
uploadFile: (selector, ...filePaths) =>
|
|
1075
|
-
op(
|
|
1076
|
-
|
|
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 },
|
|
1077
1205
|
),
|
|
1078
1206
|
waitForUrl: (pattern, opts) => {
|
|
1079
1207
|
const w = waitMs(opts?.timeout);
|
|
@@ -1083,8 +1211,8 @@ export class WorkerCore {
|
|
|
1083
1211
|
const w = waitMs(opts?.timeout);
|
|
1084
1212
|
return op("tab.waitForResponse()", w, sig => this.#waitForResponse(pattern, w, sig));
|
|
1085
1213
|
},
|
|
1086
|
-
id: id => this.#resolveCachedHandle(id),
|
|
1087
|
-
ref: id => this.#resolveAriaRef(id),
|
|
1214
|
+
id: async id => toActionableHandle(await this.#resolveCachedHandle(id)),
|
|
1215
|
+
ref: async id => toActionableHandle(await this.#resolveAriaRef(id)),
|
|
1088
1216
|
};
|
|
1089
1217
|
}
|
|
1090
1218
|
|
|
@@ -1134,12 +1262,19 @@ export class WorkerCore {
|
|
|
1134
1262
|
|
|
1135
1263
|
async #captureScreenshot(
|
|
1136
1264
|
session: SessionSnapshot,
|
|
1137
|
-
|
|
1265
|
+
output: RunOutput,
|
|
1138
1266
|
screenshots: ScreenshotResult[],
|
|
1139
1267
|
signal: AbortSignal | undefined,
|
|
1140
1268
|
opts: ScreenshotOptions = {},
|
|
1141
1269
|
): Promise<ScreenshotResult> {
|
|
1142
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);
|
|
1143
1278
|
const fullPage = opts.selector ? false : (opts.fullPage ?? false);
|
|
1144
1279
|
// An explicit save path picks the full-res capture format: puppeteer encodes
|
|
1145
1280
|
// png/jpeg/webp natively, so `save: "shot.webp"` gets real WebP bytes instead
|
|
@@ -1211,8 +1346,8 @@ export class WorkerCore {
|
|
|
1211
1346
|
dest,
|
|
1212
1347
|
resized,
|
|
1213
1348
|
});
|
|
1214
|
-
|
|
1215
|
-
|
|
1349
|
+
output.push({ type: "text", text: lines.join("\n") });
|
|
1350
|
+
output.push({ type: "image", data: resized.data, mimeType: resized.mimeType });
|
|
1216
1351
|
}
|
|
1217
1352
|
return info;
|
|
1218
1353
|
}
|