@oh-my-pi/pi-coding-agent 16.1.9 → 16.1.11
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 +46 -0
- package/dist/cli.js +3455 -3430
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/export/html/index.d.ts +31 -2
- package/dist/types/export/html/web-palette.d.ts +117 -0
- package/dist/types/hindsight/content.d.ts +7 -0
- package/dist/types/hindsight/transcript.d.ts +1 -1
- package/dist/types/modes/components/hook-editor.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +0 -1
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/types.d.ts +17 -0
- package/dist/types/modes/utils/keybinding-matchers.d.ts +13 -0
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/session-context.d.ts +2 -0
- package/dist/types/session/session-entries.d.ts +6 -0
- package/dist/types/session/session-manager.d.ts +2 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tools/acp-bridge.d.ts +29 -0
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +39 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +10 -0
- package/dist/types/tools/browser/tab-worker.d.ts +20 -0
- package/dist/types/tools/browser.d.ts +1 -0
- package/dist/types/tools/find.d.ts +1 -2
- package/dist/types/tools/write.d.ts +1 -2
- package/dist/types/utils/image-loading.d.ts +4 -3
- package/dist/types/utils/jj.d.ts +25 -0
- package/dist/types/utils/title-generator.d.ts +0 -2
- package/package.json +12 -12
- package/scripts/generate-aria-snapshot.ts +134 -0
- package/scripts/generate-share-viewer.ts +4 -2
- package/src/autoresearch/git.ts +12 -0
- package/src/config/settings-schema.ts +12 -0
- package/src/discovery/opencode.ts +47 -4
- package/src/edit/hashline/filesystem.ts +8 -0
- package/src/edit/modes/patch.ts +18 -2
- package/src/edit/modes/replace.ts +13 -10
- package/src/edit/streaming.ts +5 -5
- package/src/export/html/index.ts +50 -8
- package/src/export/html/web-palette.ts +142 -0
- package/src/hindsight/backend.ts +4 -4
- package/src/hindsight/content.ts +17 -1
- package/src/hindsight/transcript.ts +2 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/modes/components/agent-dashboard.ts +8 -8
- package/src/modes/components/hook-editor.ts +13 -10
- package/src/modes/components/session-selector.ts +3 -0
- package/src/modes/controllers/event-controller.ts +29 -5
- package/src/modes/controllers/extension-ui-controller.ts +6 -2
- package/src/modes/controllers/selector-controller.ts +3 -5
- package/src/modes/interactive-mode.ts +69 -29
- package/src/modes/theme/dark.json +1 -1
- package/src/modes/types.ts +18 -0
- package/src/modes/utils/keybinding-matchers.ts +36 -1
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/project-prompt.md +2 -0
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/browser.md +5 -2
- package/src/sdk.ts +20 -2
- package/src/session/agent-session.ts +38 -14
- package/src/session/session-context.ts +5 -0
- package/src/session/session-entries.ts +6 -0
- package/src/session/session-manager.ts +3 -1
- package/src/system-prompt.ts +15 -3
- package/src/task/worktree.ts +12 -4
- package/src/thinking.ts +1 -1
- package/src/tools/acp-bridge.ts +66 -0
- package/src/tools/bash.ts +3 -3
- package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
- package/src/tools/browser/aria/aria-snapshot.ts +103 -0
- package/src/tools/browser/cmux/cmux-tab.ts +73 -1
- package/src/tools/browser/tab-worker.ts +242 -47
- package/src/tools/browser.ts +5 -0
- package/src/tools/find.ts +1 -2
- package/src/tools/index.ts +1 -1
- package/src/tools/write.ts +3 -27
- package/src/utils/git.ts +3 -2
- package/src/utils/image-loading.ts +6 -3
- package/src/utils/jj.ts +47 -0
- package/src/utils/title-generator.ts +31 -99
|
@@ -10,6 +10,7 @@ import { resizeImage } from "../../../utils/image-resize";
|
|
|
10
10
|
import { resolveToCwd } from "../../path-utils";
|
|
11
11
|
import { formatScreenshot } from "../../render-utils";
|
|
12
12
|
import { ToolAbortError, ToolError } from "../../tool-errors";
|
|
13
|
+
import { type AriaSnapshotOptions, buildAriaSnapshotScript } from "../aria/aria-snapshot";
|
|
13
14
|
import { DEFAULT_VIEWPORT } from "../launch";
|
|
14
15
|
import { extractReadableFromHtml, type ReadableFormat } from "../readable";
|
|
15
16
|
import type { Observation, ReadyInfo, RunResultOk, ScreenshotResult, SessionSnapshot } from "../tab-protocol";
|
|
@@ -49,7 +50,7 @@ interface RunContext {
|
|
|
49
50
|
|
|
50
51
|
type WaitUntil = "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
|
|
51
52
|
type DragTarget = string | { readonly x: number; readonly y: number };
|
|
52
|
-
type SelectorKind = "css" | "ref" | "text" | "aria" | "xpath" | "pierce" | "ax";
|
|
53
|
+
type SelectorKind = "css" | "ref" | "aria-ref" | "text" | "aria" | "xpath" | "pierce" | "ax";
|
|
53
54
|
|
|
54
55
|
interface SelectorSpec {
|
|
55
56
|
kind: SelectorKind;
|
|
@@ -124,6 +125,20 @@ const accessibleName = element =>
|
|
|
124
125
|
const findElement = spec => {
|
|
125
126
|
if (spec.kind === "css") return document.querySelector(spec.value);
|
|
126
127
|
if (spec.kind === "pierce") return pierceQuery(document, spec.value);
|
|
128
|
+
if (spec.kind === "aria-ref") {
|
|
129
|
+
const wanted = spec.value;
|
|
130
|
+
const scan = root => {
|
|
131
|
+
for (const el of Array.from(root.querySelectorAll("*"))) {
|
|
132
|
+
if (el._ariaRef && el._ariaRef.ref === wanted) return el;
|
|
133
|
+
if (el.shadowRoot) {
|
|
134
|
+
const found = scan(el.shadowRoot);
|
|
135
|
+
if (found) return found;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
return scan(document);
|
|
141
|
+
}
|
|
127
142
|
if (spec.kind === "xpath") {
|
|
128
143
|
const result = document.evaluate(spec.value, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
129
144
|
return result.singleNodeValue instanceof Element ? result.singleNodeValue : null;
|
|
@@ -352,6 +367,24 @@ export class CmuxTab {
|
|
|
352
367
|
return observation;
|
|
353
368
|
}
|
|
354
369
|
|
|
370
|
+
async ariaSnapshot(selector?: string, opts?: AriaSnapshotOptions): Promise<string> {
|
|
371
|
+
const timeoutMs = Math.min(this.#runContext?.timeoutMs ?? 30_000, 30_000);
|
|
372
|
+
const result = (await this.#request(
|
|
373
|
+
"browser.eval",
|
|
374
|
+
{ script: buildAriaSnapshotScript(selector, opts) },
|
|
375
|
+
timeoutMs,
|
|
376
|
+
)) as CmuxEvalResult;
|
|
377
|
+
return result.value as string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async ref(id: string): Promise<CmuxElementHandle> {
|
|
381
|
+
const refId = /^e\d+$/.test(id.trim()) ? id.trim() : id.trim().replace(/^(?:aria-ref=|aria-ref\/|ariaref\/)/, "");
|
|
382
|
+
const selector = `aria-ref=${refId}`;
|
|
383
|
+
const timeoutMs = this.#runContext?.timeoutMs ?? 30_000;
|
|
384
|
+
await this.#waitForSelector(selector, timeoutMs);
|
|
385
|
+
return new CmuxElementHandle(this, selector);
|
|
386
|
+
}
|
|
387
|
+
|
|
355
388
|
async click(selector: string): Promise<void> {
|
|
356
389
|
await this.#selectorAction(selector, "click");
|
|
357
390
|
}
|
|
@@ -401,6 +434,12 @@ export class CmuxTab {
|
|
|
401
434
|
return new CmuxElementHandle(this, selector);
|
|
402
435
|
}
|
|
403
436
|
|
|
437
|
+
async waitForSelector(selector: string, opts?: { timeout?: number }): Promise<CmuxElementHandle> {
|
|
438
|
+
const timeoutMs = opts?.timeout ?? this.#runContext?.timeoutMs ?? 30_000;
|
|
439
|
+
await this.#waitForSelector(selector, timeoutMs);
|
|
440
|
+
return new CmuxElementHandle(this, selector);
|
|
441
|
+
}
|
|
442
|
+
|
|
404
443
|
async evaluate<TResult, TArgs extends unknown[]>(
|
|
405
444
|
fn: string | ((...args: TArgs) => TResult | Promise<TResult>),
|
|
406
445
|
...args: TArgs
|
|
@@ -516,6 +555,37 @@ export class CmuxTab {
|
|
|
516
555
|
throw new ToolError(`tab.waitForUrl() timed out after ${timeoutMs}ms`);
|
|
517
556
|
}
|
|
518
557
|
|
|
558
|
+
async waitForNavigation(opts?: { waitUntil?: WaitUntil; timeout?: number }): Promise<null> {
|
|
559
|
+
const timeoutMs = opts?.timeout ?? this.#runContext?.timeoutMs ?? 30_000;
|
|
560
|
+
// Cmux has no native "next navigation" wait — snapshot the current URL via a fresh
|
|
561
|
+
// `browser.url.get` (never the possibly-stale `#lastUrl`), then poll for a change
|
|
562
|
+
// from it (mirroring headless `page.waitForNavigation` intent) and optionally settle
|
|
563
|
+
// on the requested load state. Start it BEFORE the click/submit that navigates; after
|
|
564
|
+
// a completed nav it times out like puppeteer does.
|
|
565
|
+
const baseline = (await this.#request("browser.url.get", {}, Math.min(timeoutMs, 5_000))) as CmuxUrlGetResult;
|
|
566
|
+
const startUrl = typeof baseline.url === "string" && baseline.url.length > 0 ? baseline.url : this.#lastUrl;
|
|
567
|
+
if (typeof baseline.url === "string" && baseline.url.length > 0) this.#lastUrl = baseline.url;
|
|
568
|
+
const deadline = Date.now() + timeoutMs;
|
|
569
|
+
while (Date.now() <= deadline) {
|
|
570
|
+
const result = (await this.#request("browser.url.get", {}, Math.min(timeoutMs, 5_000))) as CmuxUrlGetResult;
|
|
571
|
+
if (typeof result.url === "string" && result.url.length > 0) {
|
|
572
|
+
this.#lastUrl = result.url;
|
|
573
|
+
if (result.url !== startUrl) {
|
|
574
|
+
if (opts?.waitUntil) {
|
|
575
|
+
await this.#request(
|
|
576
|
+
"browser.wait",
|
|
577
|
+
{ load_state: mapWaitUntil(opts.waitUntil), timeout_ms: timeoutMs },
|
|
578
|
+
timeoutMs,
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
await Bun.sleep(200);
|
|
585
|
+
}
|
|
586
|
+
throw new ToolError(`tab.waitForNavigation() timed out after ${timeoutMs}ms`);
|
|
587
|
+
}
|
|
588
|
+
|
|
519
589
|
async drag(from: DragTarget, to: DragTarget): Promise<void> {
|
|
520
590
|
const start = await this.#dragPoint(from);
|
|
521
591
|
const end = await this.#dragPoint(to);
|
|
@@ -893,6 +963,8 @@ export class CmuxTab {
|
|
|
893
963
|
else if (normalized.startsWith("p-aria/")) normalized = `aria/${normalized.slice("p-aria/".length)}`;
|
|
894
964
|
else if (normalized.startsWith("p-xpath/")) normalized = `xpath/${normalized.slice("p-xpath/".length)}`;
|
|
895
965
|
else if (normalized.startsWith("p-pierce/")) normalized = `pierce/${normalized.slice("p-pierce/".length)}`;
|
|
966
|
+
const ariaRef = /^(?:aria-ref=|aria-ref\/|ariaref\/)(e\d+)$/.exec(normalized);
|
|
967
|
+
if (ariaRef) return { kind: "aria-ref", value: ariaRef[1]!, raw };
|
|
896
968
|
const ref = /^@?e(\d+)$/.exec(normalized);
|
|
897
969
|
if (ref) return { kind: "ref", value: ref[1]!, raw, ref: `@e${ref[1]}` };
|
|
898
970
|
const slash = normalized.indexOf("/");
|
|
@@ -22,6 +22,12 @@ import { resizeImage } from "../../utils/image-resize";
|
|
|
22
22
|
import { resolveToCwd } from "../path-utils";
|
|
23
23
|
import { formatScreenshot } from "../render-utils";
|
|
24
24
|
import { ToolAbortError, ToolError, throwIfAborted } from "../tool-errors";
|
|
25
|
+
import {
|
|
26
|
+
type AriaSnapshotOptions,
|
|
27
|
+
captureAriaSnapshot,
|
|
28
|
+
parseAriaRefSelector,
|
|
29
|
+
resolveAriaRefHandle,
|
|
30
|
+
} from "./aria/aria-snapshot";
|
|
25
31
|
import {
|
|
26
32
|
applyStealthPatches,
|
|
27
33
|
applyViewport,
|
|
@@ -76,17 +82,85 @@ const INTERACTIVE_AX_ROLES = new Set([
|
|
|
76
82
|
|
|
77
83
|
const LEGACY_SELECTOR_PREFIXES = ["p-aria/", "p-text/", "p-xpath/", "p-pierce/"] as const;
|
|
78
84
|
|
|
85
|
+
const SELECTOR_HANDLER_PREFIXES = [
|
|
86
|
+
"aria/",
|
|
87
|
+
"text/",
|
|
88
|
+
"xpath/",
|
|
89
|
+
"pierce/",
|
|
90
|
+
"aria-ref=",
|
|
91
|
+
"aria-ref/",
|
|
92
|
+
"ariaref/",
|
|
93
|
+
"p-",
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Playwright-only selector engines/pseudos puppeteer cannot parse. Without this guard a
|
|
98
|
+
* `tab.click(":has-text(...)")` would wait the full action timeout and fail opaquely;
|
|
99
|
+
* fail fast instead with a pointer to the puppeteer-native alternative. Skipped for
|
|
100
|
+
* explicit query-handler prefixes (`text/`, `aria/`, …) whose payload is literal text.
|
|
101
|
+
*/
|
|
102
|
+
const PLAYWRIGHT_ONLY_SELECTOR_RE =
|
|
103
|
+
/:has-text\(|:text\(|:text-is\(|:text-matches\(|:visible\b|:hidden\b|:nth-match\(|:near\(|:above\(|:below\(|:right-of\(|:left-of\(/;
|
|
104
|
+
|
|
79
105
|
type DialogPolicy = "accept" | "dismiss";
|
|
80
106
|
type DragTarget = string | { readonly x: number; readonly y: number };
|
|
81
107
|
type ActionabilityResult = { ok: true; x: number; y: number } | { ok: false; reason: string };
|
|
82
108
|
|
|
83
109
|
/**
|
|
84
|
-
* Per-op
|
|
85
|
-
* (`
|
|
86
|
-
*
|
|
87
|
-
*
|
|
110
|
+
* Per-op fail-fast ceilings for `tab.*` helpers. All are kept strictly under the cell
|
|
111
|
+
* budget (`timeoutMs - OP_DEADLINE_SLACK_MS`) so a stalled helper rejects with a named,
|
|
112
|
+
* attributable error that leaves recovery budget — never the opaque whole-cell
|
|
113
|
+
* "Browser code execution timed out" path that consumed the entire run.
|
|
114
|
+
*
|
|
115
|
+
* - `QUICK_OP_TIMEOUT_MS`: page-coupled reads that should resolve fast (`observe`,
|
|
116
|
+
* `screenshot`, `extract`, `ariaSnapshot`).
|
|
117
|
+
* - `ACTION_OP_TIMEOUT_MS`: interactive point actions (`click`, `fill`, `type`, …) and
|
|
118
|
+
* the default for wait helpers when no explicit `{ timeout }` is given.
|
|
119
|
+
*
|
|
120
|
+
* `goto` and `evaluate` stay uncapped (`Number.POSITIVE_INFINITY`): navigation and user
|
|
121
|
+
* code legitimately use the full cell budget.
|
|
88
122
|
*/
|
|
89
123
|
const QUICK_OP_TIMEOUT_MS = 20_000;
|
|
124
|
+
const ACTION_OP_TIMEOUT_MS = 15_000;
|
|
125
|
+
/** Headroom subtracted from the cell budget so a per-op deadline fires before it. */
|
|
126
|
+
const OP_DEADLINE_SLACK_MS = 1_000;
|
|
127
|
+
|
|
128
|
+
export interface OpTimeouts {
|
|
129
|
+
/** Largest per-op deadline allowed — strictly below the cell budget. */
|
|
130
|
+
budgetBound: number;
|
|
131
|
+
/** Ceiling for quick page reads. */
|
|
132
|
+
quickOpMs: number;
|
|
133
|
+
/** Ceiling for interactive actions + default for waits. */
|
|
134
|
+
actionOpMs: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Resolve the per-op fail-fast ceilings for a given cell budget. */
|
|
138
|
+
export function resolveOpTimeouts(cellTimeoutMs: number): OpTimeouts {
|
|
139
|
+
const budgetBound = Math.max(1, cellTimeoutMs - OP_DEADLINE_SLACK_MS);
|
|
140
|
+
return {
|
|
141
|
+
budgetBound,
|
|
142
|
+
quickOpMs: Math.min(budgetBound, QUICK_OP_TIMEOUT_MS),
|
|
143
|
+
actionOpMs: Math.min(budgetBound, ACTION_OP_TIMEOUT_MS),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Effective timeout for a wait helper (`waitFor*`). A positive explicit `{ timeout }` is
|
|
149
|
+
* honored but clamped to the cell budget so it still fails fast + named; raising the tool
|
|
150
|
+
* `timeout` raises that cap, so a longer budget stays meaningful. No `{ timeout }` → the
|
|
151
|
+
* action ceiling. Puppeteer's `{ timeout: 0 }` / `Infinity` ("disable") maps to the largest
|
|
152
|
+
* bounded wait (`budgetBound`) — the harness never permits an unbounded wait. Garbage input
|
|
153
|
+
* (negative, `NaN`) falls back to the action ceiling rather than the longest wait.
|
|
154
|
+
*/
|
|
155
|
+
export function resolveWaitTimeout(cellTimeoutMs: number, explicit?: number): number {
|
|
156
|
+
const { budgetBound, actionOpMs } = resolveOpTimeouts(cellTimeoutMs);
|
|
157
|
+
if (explicit === undefined) return actionOpMs;
|
|
158
|
+
// Puppeteer "disable" sentinels — still bounded by the budget here.
|
|
159
|
+
if (explicit === 0 || explicit === Number.POSITIVE_INFINITY) return budgetBound;
|
|
160
|
+
// Positive finite → honored + clamped. Negative/NaN garbage → default, not the longest wait.
|
|
161
|
+
if (Number.isFinite(explicit) && explicit > 0) return Math.min(explicit, budgetBound);
|
|
162
|
+
return actionOpMs;
|
|
163
|
+
}
|
|
90
164
|
|
|
91
165
|
interface ScreenshotOptions {
|
|
92
166
|
selector?: string;
|
|
@@ -106,6 +180,7 @@ interface TabApi {
|
|
|
106
180
|
opts?: { waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2" },
|
|
107
181
|
): Promise<void>;
|
|
108
182
|
observe(opts?: { includeAll?: boolean; viewportOnly?: boolean }): Promise<Observation>;
|
|
183
|
+
ariaSnapshot(selector?: string, opts?: AriaSnapshotOptions): Promise<string>;
|
|
109
184
|
screenshot(opts?: ScreenshotOptions): Promise<ScreenshotResult>;
|
|
110
185
|
extract(format?: ReadableFormat): Promise<string>;
|
|
111
186
|
click(selector: string): Promise<void>;
|
|
@@ -114,7 +189,7 @@ interface TabApi {
|
|
|
114
189
|
press(key: KeyInput, opts?: { selector?: string }): Promise<void>;
|
|
115
190
|
scroll(deltaX: number, deltaY: number): Promise<void>;
|
|
116
191
|
drag(from: DragTarget, to: DragTarget): Promise<void>;
|
|
117
|
-
waitFor(selector: string): Promise<ElementHandle>;
|
|
192
|
+
waitFor(selector: string, opts?: { timeout?: number }): Promise<ElementHandle>;
|
|
118
193
|
evaluate<TResult, TArgs extends unknown[]>(
|
|
119
194
|
fn: string | ((...args: TArgs) => TResult | Promise<TResult>),
|
|
120
195
|
...args: TArgs
|
|
@@ -127,11 +202,29 @@ interface TabApi {
|
|
|
127
202
|
pattern: string | RegExp | ((response: HTTPResponse) => boolean | Promise<boolean>),
|
|
128
203
|
opts?: { timeout?: number },
|
|
129
204
|
): Promise<HTTPResponse>;
|
|
205
|
+
waitForSelector(
|
|
206
|
+
selector: string,
|
|
207
|
+
opts?: { timeout?: number; visible?: boolean; hidden?: boolean },
|
|
208
|
+
): Promise<ElementHandle | null>;
|
|
209
|
+
waitForNavigation(opts?: {
|
|
210
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
|
|
211
|
+
timeout?: number;
|
|
212
|
+
}): Promise<HTTPResponse | null>;
|
|
130
213
|
id(n: number): Promise<ElementHandle>;
|
|
214
|
+
ref(id: string): Promise<ElementHandle>;
|
|
131
215
|
}
|
|
132
216
|
|
|
133
|
-
function normalizeSelector(selector: string): string {
|
|
217
|
+
export function normalizeSelector(selector: string): string {
|
|
134
218
|
if (!selector) return selector;
|
|
219
|
+
if (
|
|
220
|
+
!SELECTOR_HANDLER_PREFIXES.some(prefix => selector.startsWith(prefix)) &&
|
|
221
|
+
PLAYWRIGHT_ONLY_SELECTOR_RE.test(selector)
|
|
222
|
+
) {
|
|
223
|
+
throw new ToolError(
|
|
224
|
+
`Playwright-only selector ${JSON.stringify(selector)} is not supported by the browser tool. ` +
|
|
225
|
+
`Use a puppeteer text selector ("text/Allow all"), an aria selector ("aria/Name"), CSS, or "xpath/...".`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
135
228
|
if (selector.startsWith("p-") && !LEGACY_SELECTOR_PREFIXES.some(prefix => selector.startsWith(prefix))) {
|
|
136
229
|
throw new ToolError(
|
|
137
230
|
`Unsupported selector prefix. Use CSS or puppeteer query handlers (aria/, text/, xpath/, pierce/). Got: ${selector}`,
|
|
@@ -753,8 +846,15 @@ export class WorkerCore {
|
|
|
753
846
|
try {
|
|
754
847
|
return await fn(opSignal);
|
|
755
848
|
} catch (err) {
|
|
756
|
-
//
|
|
757
|
-
|
|
849
|
+
// Fail fast with a named, attributable error instead of the opaque whole-cell timeout:
|
|
850
|
+
// our per-op deadline fired, or puppeteer's own (equal) timeout fired first — having
|
|
851
|
+
// already torn down the CDP action via the op signal, so no work is left dangling.
|
|
852
|
+
// Cell-budget aborts and uncapped helpers (goto/evaluate) keep their native errors.
|
|
853
|
+
if (
|
|
854
|
+
capped &&
|
|
855
|
+
!cellSignal.aborted &&
|
|
856
|
+
(opTimeout?.aborted || (err instanceof Error && err.name === "TimeoutError"))
|
|
857
|
+
) {
|
|
758
858
|
throw new ToolError(`${label} timed out after ${perOpTimeoutMs}ms`);
|
|
759
859
|
}
|
|
760
860
|
throw err;
|
|
@@ -773,7 +873,8 @@ export class WorkerCore {
|
|
|
773
873
|
active: ActiveRun,
|
|
774
874
|
): TabApi {
|
|
775
875
|
const page = this.#requirePage();
|
|
776
|
-
const quickOpMs =
|
|
876
|
+
const { quickOpMs, actionOpMs } = resolveOpTimeouts(timeoutMs);
|
|
877
|
+
const waitMs = (explicit?: number): number => resolveWaitTimeout(timeoutMs, explicit);
|
|
777
878
|
const INF = Number.POSITIVE_INFINITY;
|
|
778
879
|
const op = <T>(label: string, perOpMs: number, fn: (sig: AbortSignal) => Promise<T>): Promise<T> =>
|
|
779
880
|
this.#runOp(active, label, signal, perOpMs, fn);
|
|
@@ -792,6 +893,28 @@ export class WorkerCore {
|
|
|
792
893
|
);
|
|
793
894
|
}),
|
|
794
895
|
observe: opts => op("tab.observe()", quickOpMs, sig => this.#collectObservation({ ...opts, signal: sig })),
|
|
896
|
+
ariaSnapshot: (selector, opts) =>
|
|
897
|
+
op(
|
|
898
|
+
selector ? `tab.ariaSnapshot(${JSON.stringify(selector)})` : "tab.ariaSnapshot()",
|
|
899
|
+
quickOpMs,
|
|
900
|
+
async sig => {
|
|
901
|
+
let root: ElementHandle | null = null;
|
|
902
|
+
if (selector) {
|
|
903
|
+
root = (await untilAborted(sig, () =>
|
|
904
|
+
page.$(normalizeSelector(selector)),
|
|
905
|
+
)) as ElementHandle | null;
|
|
906
|
+
if (!root)
|
|
907
|
+
throw new ToolError(
|
|
908
|
+
`tab.ariaSnapshot: selector ${JSON.stringify(selector)} matched no element`,
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
return await untilAborted(sig, () => captureAriaSnapshot(page, root, opts));
|
|
913
|
+
} finally {
|
|
914
|
+
await root?.dispose().catch(() => undefined);
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
),
|
|
795
918
|
screenshot: opts =>
|
|
796
919
|
op(describeScreenshot(opts), quickOpMs, sig =>
|
|
797
920
|
this.#captureScreenshot(session, displays, screenshots, sig, opts),
|
|
@@ -814,44 +937,88 @@ export class WorkerCore {
|
|
|
814
937
|
return content;
|
|
815
938
|
}),
|
|
816
939
|
click: selector =>
|
|
817
|
-
op(`tab.click(${JSON.stringify(selector)})`,
|
|
940
|
+
op(`tab.click(${JSON.stringify(selector)})`, actionOpMs, async sig => {
|
|
941
|
+
if (parseAriaRefSelector(selector) !== null) {
|
|
942
|
+
const handle = await this.#resolveAriaRef(selector);
|
|
943
|
+
try {
|
|
944
|
+
await untilAborted(sig, () => handle.click());
|
|
945
|
+
} finally {
|
|
946
|
+
await handle.dispose().catch(() => undefined);
|
|
947
|
+
}
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
818
950
|
const resolved = normalizeSelector(selector);
|
|
819
|
-
if (resolved.startsWith("text/")) await clickQueryHandlerText(page, resolved,
|
|
820
|
-
else await untilAborted(sig, () => page.locator(resolved).setTimeout(
|
|
951
|
+
if (resolved.startsWith("text/")) await clickQueryHandlerText(page, resolved, actionOpMs, sig);
|
|
952
|
+
else await untilAborted(sig, () => page.locator(resolved).setTimeout(actionOpMs).click({ signal: sig }));
|
|
821
953
|
}),
|
|
822
954
|
type: (selector, text) =>
|
|
823
|
-
op(`tab.type(${JSON.stringify(selector)})`,
|
|
824
|
-
const handle =
|
|
825
|
-
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
826
|
-
)) as ElementHandle;
|
|
955
|
+
op(`tab.type(${JSON.stringify(selector)})`, actionOpMs, async sig => {
|
|
956
|
+
const handle = await this.#resolveActionHandle(selector, actionOpMs, sig);
|
|
827
957
|
try {
|
|
828
958
|
await untilAborted(sig, () => handle.type(text, { delay: 0 }));
|
|
829
959
|
} finally {
|
|
830
|
-
await handle.dispose();
|
|
960
|
+
await handle.dispose().catch(() => undefined);
|
|
831
961
|
}
|
|
832
962
|
}),
|
|
833
963
|
fill: (selector, value) =>
|
|
834
|
-
op(`tab.fill(${JSON.stringify(selector)})`,
|
|
835
|
-
|
|
836
|
-
|
|
964
|
+
op(`tab.fill(${JSON.stringify(selector)})`, actionOpMs, async sig => {
|
|
965
|
+
if (parseAriaRefSelector(selector) !== null) {
|
|
966
|
+
const handle = await this.#resolveAriaRef(selector);
|
|
967
|
+
try {
|
|
968
|
+
await untilAborted(sig, () =>
|
|
969
|
+
handle.evaluate(el => {
|
|
970
|
+
const node = el as unknown as { value?: string; focus?: () => void };
|
|
971
|
+
node.focus?.();
|
|
972
|
+
if ("value" in node) node.value = "";
|
|
973
|
+
}),
|
|
974
|
+
);
|
|
975
|
+
await untilAborted(sig, () => handle.type(value, { delay: 0 }));
|
|
976
|
+
} finally {
|
|
977
|
+
await handle.dispose().catch(() => undefined);
|
|
978
|
+
}
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
await untilAborted(sig, () =>
|
|
982
|
+
page.locator(normalizeSelector(selector)).setTimeout(actionOpMs).fill(value, { signal: sig }),
|
|
983
|
+
);
|
|
984
|
+
}),
|
|
837
985
|
press: (key, opts) =>
|
|
838
|
-
op(`tab.press(${JSON.stringify(key)})`,
|
|
986
|
+
op(`tab.press(${JSON.stringify(key)})`, actionOpMs, async sig => {
|
|
839
987
|
const selector = opts?.selector;
|
|
840
988
|
if (selector) await untilAborted(sig, () => page.focus(normalizeSelector(selector)));
|
|
841
989
|
await untilAborted(sig, () => page.keyboard.press(key));
|
|
842
990
|
}),
|
|
843
991
|
scroll: (deltaX, deltaY) =>
|
|
844
|
-
op("tab.scroll()",
|
|
845
|
-
drag: (from, to) => op("tab.drag()",
|
|
846
|
-
waitFor: selector =>
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
),
|
|
992
|
+
op("tab.scroll()", actionOpMs, sig => untilAborted(sig, () => page.mouse.wheel({ deltaX, deltaY }))),
|
|
993
|
+
drag: (from, to) => op("tab.drag()", actionOpMs, sig => this.#drag(from, to, sig)),
|
|
994
|
+
waitFor: (selector, opts) => {
|
|
995
|
+
const w = waitMs(opts?.timeout);
|
|
996
|
+
return op(`tab.waitFor(${JSON.stringify(selector)})`, w, sig =>
|
|
997
|
+
this.#resolveActionHandle(selector, w, sig),
|
|
998
|
+
);
|
|
999
|
+
},
|
|
1000
|
+
waitForSelector: (selector, opts) => {
|
|
1001
|
+
const w = waitMs(opts?.timeout);
|
|
1002
|
+
return op(`tab.waitForSelector(${JSON.stringify(selector)})`, w, async sig => {
|
|
1003
|
+
if (parseAriaRefSelector(selector) !== null) return this.#resolveAriaRef(selector);
|
|
1004
|
+
return (await untilAborted(sig, () =>
|
|
1005
|
+
page.waitForSelector(normalizeSelector(selector), {
|
|
1006
|
+
timeout: w,
|
|
1007
|
+
visible: opts?.visible,
|
|
1008
|
+
hidden: opts?.hidden,
|
|
1009
|
+
signal: sig,
|
|
1010
|
+
}),
|
|
1011
|
+
)) as ElementHandle | null;
|
|
1012
|
+
});
|
|
1013
|
+
},
|
|
1014
|
+
waitForNavigation: opts => {
|
|
1015
|
+
const w = waitMs(opts?.timeout);
|
|
1016
|
+
return op("tab.waitForNavigation()", w, sig =>
|
|
1017
|
+
untilAborted(sig, () =>
|
|
1018
|
+
page.waitForNavigation({ waitUntil: opts?.waitUntil ?? "load", timeout: w, signal: sig }),
|
|
1019
|
+
),
|
|
1020
|
+
);
|
|
1021
|
+
},
|
|
855
1022
|
evaluate: (fn, ...args) =>
|
|
856
1023
|
op("tab.evaluate()", INF, sig =>
|
|
857
1024
|
untilAborted(sig, () =>
|
|
@@ -861,10 +1028,8 @@ export class WorkerCore {
|
|
|
861
1028
|
),
|
|
862
1029
|
) as never,
|
|
863
1030
|
scrollIntoView: selector =>
|
|
864
|
-
op(`tab.scrollIntoView(${JSON.stringify(selector)})`,
|
|
865
|
-
const handle =
|
|
866
|
-
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
867
|
-
)) as ElementHandle;
|
|
1031
|
+
op(`tab.scrollIntoView(${JSON.stringify(selector)})`, actionOpMs, async sig => {
|
|
1032
|
+
const handle = await this.#resolveActionHandle(selector, actionOpMs, sig);
|
|
868
1033
|
try {
|
|
869
1034
|
await untilAborted(sig, () =>
|
|
870
1035
|
handle.evaluate(el => {
|
|
@@ -879,16 +1044,23 @@ export class WorkerCore {
|
|
|
879
1044
|
}
|
|
880
1045
|
}),
|
|
881
1046
|
select: (selector, ...values) =>
|
|
882
|
-
op(`tab.select(${JSON.stringify(selector)})`,
|
|
1047
|
+
op(`tab.select(${JSON.stringify(selector)})`, actionOpMs, sig =>
|
|
1048
|
+
this.#select(selector, values, actionOpMs, sig),
|
|
1049
|
+
),
|
|
883
1050
|
uploadFile: (selector, ...filePaths) =>
|
|
884
|
-
op(`tab.uploadFile(${JSON.stringify(selector)})`,
|
|
885
|
-
this.#uploadFile(selector, filePaths,
|
|
1051
|
+
op(`tab.uploadFile(${JSON.stringify(selector)})`, actionOpMs, sig =>
|
|
1052
|
+
this.#uploadFile(selector, filePaths, actionOpMs, sig, session),
|
|
886
1053
|
),
|
|
887
|
-
waitForUrl: (pattern, opts) =>
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1054
|
+
waitForUrl: (pattern, opts) => {
|
|
1055
|
+
const w = waitMs(opts?.timeout);
|
|
1056
|
+
return op("tab.waitForUrl()", w, sig => this.#waitForUrl(pattern, w, sig));
|
|
1057
|
+
},
|
|
1058
|
+
waitForResponse: (pattern, opts) => {
|
|
1059
|
+
const w = waitMs(opts?.timeout);
|
|
1060
|
+
return op("tab.waitForResponse()", w, sig => this.#waitForResponse(pattern, w, sig));
|
|
1061
|
+
},
|
|
891
1062
|
id: id => this.#resolveCachedHandle(id),
|
|
1063
|
+
ref: id => this.#resolveAriaRef(id),
|
|
892
1064
|
};
|
|
893
1065
|
}
|
|
894
1066
|
|
|
@@ -1073,7 +1245,7 @@ export class WorkerCore {
|
|
|
1073
1245
|
async #select(selector: string, values: string[], timeoutMs: number, signal: AbortSignal): Promise<string[]> {
|
|
1074
1246
|
const page = this.#requirePage();
|
|
1075
1247
|
const handle = (await untilAborted(signal, () =>
|
|
1076
|
-
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
1248
|
+
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle({ signal }),
|
|
1077
1249
|
)) as ElementHandle;
|
|
1078
1250
|
try {
|
|
1079
1251
|
return (await untilAborted(signal, () =>
|
|
@@ -1119,7 +1291,7 @@ export class WorkerCore {
|
|
|
1119
1291
|
if (!filePaths.length) throw new ToolError("tab.uploadFile() requires at least one file path");
|
|
1120
1292
|
const page = this.#requirePage();
|
|
1121
1293
|
const handle = (await untilAborted(signal, () =>
|
|
1122
|
-
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle(),
|
|
1294
|
+
page.locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle({ signal }),
|
|
1123
1295
|
)) as ElementHandle;
|
|
1124
1296
|
try {
|
|
1125
1297
|
const absolute = filePaths.map(filePath => resolveToCwd(filePath, session.cwd));
|
|
@@ -1148,7 +1320,7 @@ export class WorkerCore {
|
|
|
1148
1320
|
const url = (globalThis as unknown as { location: { href: string } }).location.href;
|
|
1149
1321
|
return isRe ? new RegExp(m, fl).test(url) : url.includes(m);
|
|
1150
1322
|
},
|
|
1151
|
-
{ timeout, polling: 200 },
|
|
1323
|
+
{ timeout, polling: 200, signal },
|
|
1152
1324
|
matcher,
|
|
1153
1325
|
isRegex,
|
|
1154
1326
|
flags,
|
|
@@ -1169,7 +1341,7 @@ export class WorkerCore {
|
|
|
1169
1341
|
: pattern instanceof RegExp
|
|
1170
1342
|
? response => pattern.test(response.url())
|
|
1171
1343
|
: response => response.url().includes(pattern);
|
|
1172
|
-
return (await untilAborted(signal, () => page.waitForResponse(predicate, { timeout }))) as HTTPResponse;
|
|
1344
|
+
return (await untilAborted(signal, () => page.waitForResponse(predicate, { timeout, signal }))) as HTTPResponse;
|
|
1173
1345
|
}
|
|
1174
1346
|
|
|
1175
1347
|
async #resolveCachedHandle(id: number): Promise<ElementHandle> {
|
|
@@ -1188,6 +1360,29 @@ export class WorkerCore {
|
|
|
1188
1360
|
}
|
|
1189
1361
|
return handle;
|
|
1190
1362
|
}
|
|
1363
|
+
|
|
1364
|
+
async #resolveAriaRef(id: string): Promise<ElementHandle> {
|
|
1365
|
+
const ref = parseAriaRefSelector(id) ?? id.trim();
|
|
1366
|
+
const handle = await resolveAriaRefHandle(this.#requirePage(), ref);
|
|
1367
|
+
if (!handle) {
|
|
1368
|
+
throw new ToolError(
|
|
1369
|
+
`Unknown ARIA ref ${JSON.stringify(ref)}. Run tab.ariaSnapshot() to refresh refs (they renumber each snapshot).`,
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
return handle;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Resolve a selector to an ElementHandle for handle-based actions. An
|
|
1377
|
+
* `aria-ref=eN` selector resolves against the latest ariaSnapshot's refs
|
|
1378
|
+
* (main world); anything else goes through the normal locator wait.
|
|
1379
|
+
*/
|
|
1380
|
+
async #resolveActionHandle(selector: string, timeoutMs: number, sig: AbortSignal): Promise<ElementHandle> {
|
|
1381
|
+
if (parseAriaRefSelector(selector) !== null) return this.#resolveAriaRef(selector);
|
|
1382
|
+
return (await untilAborted(sig, () =>
|
|
1383
|
+
this.#requirePage().locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle({ signal: sig }),
|
|
1384
|
+
)) as ElementHandle;
|
|
1385
|
+
}
|
|
1191
1386
|
#clearElementCache(): void {
|
|
1192
1387
|
if (this.#elementCache.size === 0) {
|
|
1193
1388
|
this.#elementCounter = 0;
|
package/src/tools/browser.ts
CHANGED
|
@@ -16,6 +16,11 @@ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
|
16
16
|
import { toolResult } from "./tool-result";
|
|
17
17
|
import { clampTimeout } from "./tool-timeouts";
|
|
18
18
|
|
|
19
|
+
export {
|
|
20
|
+
type AriaSnapshotOptions,
|
|
21
|
+
buildAriaSnapshotScript,
|
|
22
|
+
parseAriaRefSelector,
|
|
23
|
+
} from "./browser/aria/aria-snapshot";
|
|
19
24
|
export { cmuxSnapshotToObservation, mapWaitUntil, resolveCmuxKind, serializeEval } from "./browser/cmux/rpc";
|
|
20
25
|
export { CmuxSocketClient } from "./browser/cmux/socket-client";
|
|
21
26
|
export { extractReadableFromHtml, type ReadableFormat, type ReadableResult } from "./browser/readable";
|
package/src/tools/find.ts
CHANGED
|
@@ -101,8 +101,7 @@ interface FindTarget {
|
|
|
101
101
|
export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
102
102
|
readonly name = "find";
|
|
103
103
|
readonly approval = "read" as const;
|
|
104
|
-
readonly
|
|
105
|
-
readonly loadMode = "discoverable";
|
|
104
|
+
readonly loadMode = "essential";
|
|
106
105
|
readonly label = "Find";
|
|
107
106
|
readonly description: string;
|
|
108
107
|
readonly parameters = findSchema;
|
package/src/tools/index.ts
CHANGED
|
@@ -375,7 +375,7 @@ export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool |
|
|
|
375
375
|
export type BuiltinToolLoadMode = "essential" | "discoverable";
|
|
376
376
|
|
|
377
377
|
/** Default essential tool names when tools.essentialOverride is empty. */
|
|
378
|
-
export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = ["read", "bash", "edit"] as const;
|
|
378
|
+
export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = ["read", "bash", "edit", "write", "find"] as const;
|
|
379
379
|
|
|
380
380
|
/**
|
|
381
381
|
* Resolve the active essential built-in tool names from settings.
|
package/src/tools/write.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
readArchiveEntries,
|
|
28
28
|
writeArchive,
|
|
29
29
|
} from "../utils/zip";
|
|
30
|
+
import { routeWriteThroughBridge } from "./acp-bridge";
|
|
30
31
|
import { truncateForPrompt } from "./approval";
|
|
31
32
|
import { assertEditableFile } from "./auto-generated-guard";
|
|
32
33
|
import {
|
|
@@ -143,15 +144,6 @@ function maybeWriteSnapshotHeader(session: ToolSession, absolutePath: string, co
|
|
|
143
144
|
return formatHashlineHeader(formatPathRelativeToCwd(absolutePath, session.cwd), tag);
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
function shouldRouteWriteThroughBridge(session: ToolSession, requestedPath: string, absolutePath: string): boolean {
|
|
147
|
-
if (isInternalUrlPath(requestedPath)) return false;
|
|
148
|
-
|
|
149
|
-
const state = session.getPlanModeState?.();
|
|
150
|
-
if (!state?.enabled || !isInternalUrlPath(state.planFilePath)) return true;
|
|
151
|
-
|
|
152
|
-
return absolutePath !== resolvePlanPath(session, state.planFilePath);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
147
|
/**
|
|
156
148
|
* Append a trailing note line to the first text block of a tool result.
|
|
157
149
|
* Mutates `result` in place (the result object is owned by this call).
|
|
@@ -293,8 +285,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
293
285
|
readonly parameters = writeSchema;
|
|
294
286
|
readonly strict = true;
|
|
295
287
|
readonly concurrency = "exclusive";
|
|
296
|
-
readonly loadMode = "
|
|
297
|
-
readonly summary = "Write content to a file (creates or overwrites)";
|
|
288
|
+
readonly loadMode = "essential";
|
|
298
289
|
|
|
299
290
|
/** Stream matchers should see the real file content, not its JSON-escaped argument encoding. */
|
|
300
291
|
matcherDigest(args: unknown): string | undefined {
|
|
@@ -772,11 +763,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
772
763
|
};
|
|
773
764
|
}
|
|
774
765
|
|
|
775
|
-
#routeWriteThroughBridge(absolutePath: string, content: string): Promise<void> | undefined {
|
|
776
|
-
const bridge = this.session.getClientBridge?.();
|
|
777
|
-
if (!bridge?.capabilities.writeTextFile || !bridge.writeTextFile) return undefined;
|
|
778
|
-
return bridge.writeTextFile({ path: absolutePath, content });
|
|
779
|
-
}
|
|
780
766
|
async execute(
|
|
781
767
|
_toolCallId: string,
|
|
782
768
|
{ path: rawPath, content }: WriteParams,
|
|
@@ -883,17 +869,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
883
869
|
|
|
884
870
|
// Try ACP bridge first for editor-visible filesystem paths. Internal
|
|
885
871
|
// artifacts such as local:// plans are owned by OMP, not the editor.
|
|
886
|
-
|
|
887
|
-
? this.#routeWriteThroughBridge(absolutePath, cleanContent)
|
|
888
|
-
: undefined;
|
|
889
|
-
if (bridgePromise !== undefined) {
|
|
890
|
-
try {
|
|
891
|
-
await bridgePromise;
|
|
892
|
-
} catch (error) {
|
|
893
|
-
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
894
|
-
}
|
|
895
|
-
invalidateFsScanAfterWrite(absolutePath);
|
|
896
|
-
this.session.bumpFileMutationVersion?.(absolutePath);
|
|
872
|
+
if (await routeWriteThroughBridge(this.session, path, absolutePath, cleanContent)) {
|
|
897
873
|
const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
|
|
898
874
|
const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
|
|
899
875
|
const header = maybeWriteSnapshotHeader(this.session, absolutePath, cleanContent);
|
package/src/utils/git.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { $which, hasFsCode, isEnoent, Snowflake } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { $which, hasFsCode, isEisdir, isEnoent, isEnotdir, Snowflake } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import {
|
|
6
6
|
parseDiffHunks as parseCommitDiffHunks,
|
|
7
7
|
parseFileDiffs,
|
|
@@ -376,7 +376,8 @@ async function writeTempPatch(content: string): Promise<string> {
|
|
|
376
376
|
type EntryType = "directory" | "file";
|
|
377
377
|
|
|
378
378
|
function shouldRetry(err: unknown, n: number) {
|
|
379
|
-
if (isEnoent(err) || hasFsCode(err, "ENFILE") || hasFsCode(err, "EMFILE"))
|
|
379
|
+
if (isEnoent(err) || isEisdir(err) || isEnotdir(err) || hasFsCode(err, "ENFILE") || hasFsCode(err, "EMFILE"))
|
|
380
|
+
return false;
|
|
380
381
|
if (hasFsCode(err, "EINTR")) return n < EINTR_MAX_RETRIES;
|
|
381
382
|
if (n > EINTR_MAX_RETRIES) throw err;
|
|
382
383
|
throw err;
|