fix(browser): bounded scroll acknowledgement wait

- Released tab.scroll after two seconds when a queued wheel event waits on a busy renderer acknowledgement.
- Preserved immediate dispatch failures and added regression coverage for both outcomes.

Fixes #5905
This commit is contained in:
roboomp
2026-07-17 20:02:53 +00:00
parent f8e7b29b70
commit b8ef46a0c8
3 changed files with 32 additions and 1 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed `tab.scroll()` timing out after a queued wheel event waits too long for a busy renderer's acknowledgement ([#5905](https://github.com/can1357/oh-my-pi/issues/5905)).
## [17.0.3] - 2026-07-17
### Changed
@@ -144,6 +144,8 @@ interface OpenDialogInfo {
*/
const QUICK_OP_TIMEOUT_MS = 20_000;
const ACTION_OP_TIMEOUT_MS = 8_000;
/** Maximum wait for a renderer acknowledgement after a wheel event is queued. */
const SCROLL_ACK_TIMEOUT_MS = 2_000;
/** Headroom subtracted from the cell budget so a per-op deadline fires before it. */
const OP_DEADLINE_SLACK_MS = CELL_BUDGET_SLACK_MS;
/**
@@ -175,6 +177,14 @@ export function resolveOpTimeouts(cellTimeoutMs: number): OpTimeouts {
};
}
/** Queue a wheel event without treating a delayed renderer acknowledgement as dispatch failure. */
export async function dispatchScroll(
dispatch: () => Promise<void>,
ackTimeoutMs = SCROLL_ACK_TIMEOUT_MS,
): Promise<void> {
await Promise.race([dispatch(), Bun.sleep(ackTimeoutMs)]);
}
/**
* Effective timeout for a wait helper (`waitFor*`). A positive explicit `{ timeout }` is
* honored but clamped to the cell budget so it still fails fast + named; raising the tool
@@ -1216,7 +1226,9 @@ export class WorkerCore {
await untilAborted(sig, () => page.keyboard.press(key));
}),
scroll: (deltaX, deltaY) =>
op("tab.scroll()", actionOpMs, sig => untilAborted(sig, () => page.mouse.wheel({ deltaX, deltaY }))),
op("tab.scroll()", actionOpMs, sig =>
untilAborted(sig, () => dispatchScroll(() => page.mouse.wheel({ deltaX, deltaY }))),
),
drag: (from, to) => op("tab.drag()", actionOpMs, sig => this.#drag(from, to, sig)),
waitFor: (selector, opts) => {
const w = waitMs(opts?.timeout);
@@ -1,6 +1,7 @@
import { describe, expect, it } from "bun:test";
import { resolvePredicateTimeout } from "@oh-my-pi/pi-coding-agent/tools/browser/run-cancellation";
import {
dispatchScroll,
normalizeSelector,
resolveOpTimeouts,
resolveWaitTimeout,
@@ -42,6 +43,20 @@ describe("browser per-op fail-fast ceilings", () => {
});
});
describe("browser scroll acknowledgement", () => {
it("returns after the acknowledgement deadline while the renderer remains stalled", async () => {
const acknowledgement = Promise.withResolvers<void>();
await expect(dispatchScroll(() => acknowledgement.promise, 1)).resolves.toBeUndefined();
});
it("preserves wheel dispatch failures received before the acknowledgement deadline", async () => {
await expect(dispatchScroll(() => Promise.reject(new Error("target closed")), 100)).rejects.toThrow(
"target closed",
);
});
});
describe("browser wait-helper timeout resolution", () => {
it("defaults a wait to the action ceiling when no explicit timeout is given", () => {
const cell = 30_000;