fix(vom): bug fix

This commit is contained in:
Ljy-0827
2026-07-28 16:45:40 +08:00
parent a52c2d6bf2
commit 000fbf5540
5 changed files with 95 additions and 31 deletions
@@ -344,6 +344,11 @@ export default defineContentScript({
renderControlOverlay(); renderControlOverlay();
} }
async function waitForRenderedOverlayUpdate(): Promise<void> {
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
}
function clearCurrentAgentSession(): void { function clearCurrentAgentSession(): void {
const sessionId = overlays.snapshot().activeSessionId; const sessionId = overlays.snapshot().activeSessionId;
if (!sessionId) return; if (!sessionId) return;
@@ -528,6 +533,7 @@ export default defineContentScript({
}; };
overlays.clearAgentHelpRequest(requestId); overlays.clearAgentHelpRequest(requestId);
renderAll(); renderAll();
await waitForRenderedOverlayUpdate();
await chrome.runtime.sendMessage(msg).catch((err) => { await chrome.runtime.sendMessage(msg).catch((err) => {
console.debug("[bsk overlay] help finish failed", err); console.debug("[bsk overlay] help finish failed", err);
}); });
@@ -107,6 +107,30 @@ describe("handleRequestHelp", () => {
expect(res).toMatchObject({ outcome: "continued", note: "ok", tab_id: 5 }); expect(res).toMatchObject({ outcome: "continued", note: "ok", tab_id: 5 });
}); });
it("does not refresh observations after the user returns control", async () => {
const cdpSend = vi.fn(async () => ({}));
const deps = baseDeps({
cdp: { send: cdpSend } as unknown as RequestHelpDeps["cdp"],
});
const res = await handleRequestHelp(
fakeManager("abcd", 99, 5),
baseParams({ tab_id: 5 }),
deps,
);
expect(res).toMatchObject({ outcome: "continued", tab_id: 5 });
expect(cdpSend).not.toHaveBeenCalledWith(
expect.any(Number),
"Accessibility.getFullAXTree",
expect.anything(),
);
expect(cdpSend).not.toHaveBeenCalledWith(
expect.any(Number),
"DOMSnapshot.captureSnapshot",
expect.anything(),
);
});
it("forwards title into the help request message when provided", async () => { it("forwards title into the help request message when provided", async () => {
const deps = baseDeps(); const deps = baseDeps();
await handleRequestHelp( await handleRequestHelp(
+46 -25
View File
@@ -304,11 +304,12 @@ async function cleanupHelp(help: ActiveHelpRequest): Promise<void> {
); );
} }
function finishHelp( async function finishHelp(
help: ActiveHelpRequest, help: ActiveHelpRequest,
value: RequestHelpResult | RpcError, value: RequestHelpResult | RpcError,
notifyContent = true, notifyContent = true,
): void { waitForCleanup = false,
): Promise<void> {
if (help.settled) return; if (help.settled) return;
help.settled = true; help.settled = true;
activeHelpRequests.delete(help.requestId); activeHelpRequests.delete(help.requestId);
@@ -317,7 +318,11 @@ function finishHelp(
if (help.abortHandler) help.deps.signal?.removeEventListener("abort", help.abortHandler); if (help.abortHandler) help.deps.signal?.removeEventListener("abort", help.abortHandler);
clearRearmTimer(help.primaryTabId); clearRearmTimer(help.primaryTabId);
for (const tabId of help.overlayTabIds) clearRearmTimer(tabId); for (const tabId of help.overlayTabIds) clearRearmTimer(tabId);
if (notifyContent) void cleanupHelp(help); if (notifyContent) {
const cleanup = cleanupHelp(help);
if (waitForCleanup) await cleanup;
else void cleanup;
}
help.resolve(value); help.resolve(value);
} }
@@ -386,12 +391,17 @@ async function rearmHelp(help: ActiveHelpRequest, tabId: number): Promise<boolea
try { try {
const legacyFinish = await refreshAndSendHelpOverlay(help, tabId); const legacyFinish = await refreshAndSendHelpOverlay(help, tabId);
if (legacyFinish && !help.settled) { if (legacyFinish && !help.settled) {
finishHelp(help, { await finishHelp(
outcome: legacyFinish.outcome, help,
...(legacyFinish.note ? { note: legacyFinish.note } : {}), {
tab_id: help.primaryTabId, outcome: legacyFinish.outcome,
resolved_targets: help.resolvedTargets, ...(legacyFinish.note ? { note: legacyFinish.note } : {}),
}); tab_id: help.primaryTabId,
resolved_targets: help.resolvedTargets,
},
true,
true,
);
} }
return true; return true;
} catch { } catch {
@@ -501,12 +511,17 @@ async function finishHelpFromContent(
const match = await findHelpForTab(tabId, deps); const match = await findHelpForTab(tabId, deps);
if (match !== help) return; if (match !== help) return;
} }
finishHelp(help, { await finishHelp(
outcome: message.outcome, help,
...(message.note ? { note: message.note } : {}), {
tab_id: help.primaryTabId, outcome: message.outcome,
resolved_targets: help.resolvedTargets, ...(message.note ? { note: message.note } : {}),
}); tab_id: help.primaryTabId,
resolved_targets: help.resolvedTargets,
},
true,
true,
);
} }
function attachHelpTabListener(deps: RequestHelpDeps): () => void { function attachHelpTabListener(deps: RequestHelpDeps): () => void {
@@ -639,7 +654,7 @@ function startCompletionPolling(help: ActiveHelpRequest): void {
} }
help.completionMatchedSince ??= now; help.completionMatchedSince ??= now;
if (now - help.completionMatchedSince < stableMs) return; if (now - help.completionMatchedSince < stableMs) return;
finishHelp(help, { void finishHelp(help, {
outcome: "completed", outcome: "completed",
completed_by: "system", completed_by: "system",
tab_id: help.primaryTabId, tab_id: help.primaryTabId,
@@ -698,7 +713,7 @@ export async function handleRequestHelp(
return new Promise<RequestHelpResult | RpcError>((resolve) => { return new Promise<RequestHelpResult | RpcError>((resolve) => {
const timeoutTimer = setTimeout(() => { const timeoutTimer = setTimeout(() => {
finishHelp(help, { void finishHelp(help, {
outcome: "timed_out", outcome: "timed_out",
tab_id: help.primaryTabId, tab_id: help.primaryTabId,
resolved_targets: help.resolvedTargets, resolved_targets: help.resolvedTargets,
@@ -727,7 +742,8 @@ export async function handleRequestHelp(
abortHandler: null, abortHandler: null,
}; };
const onAbort = () => finishHelp(help, { code: "cancelled", message: "request_help aborted" }); const onAbort = () =>
void finishHelp(help, { code: "cancelled", message: "request_help aborted" });
help.abortHandler = onAbort; help.abortHandler = onAbort;
activeHelpRequests.set(requestId, help); activeHelpRequests.set(requestId, help);
if (deps.signal?.aborted) { if (deps.signal?.aborted) {
@@ -738,14 +754,19 @@ export async function handleRequestHelp(
startCompletionPolling(help); startCompletionPolling(help);
void sendCurrentHelpOverlay(help) void sendCurrentHelpOverlay(help)
.then((legacyFinish) => { .then(async (legacyFinish) => {
if (!legacyFinish || help.settled) return; if (!legacyFinish || help.settled) return;
finishHelp(help, { await finishHelp(
outcome: legacyFinish.outcome, help,
...(legacyFinish.note ? { note: legacyFinish.note } : {}), {
tab_id: help.primaryTabId, outcome: legacyFinish.outcome,
resolved_targets: help.resolvedTargets, ...(legacyFinish.note ? { note: legacyFinish.note } : {}),
}); tab_id: help.primaryTabId,
resolved_targets: help.resolvedTargets,
},
true,
true,
);
}) })
.catch(() => { .catch(() => {
scheduleRearmForTab(tabId, deps); scheduleRearmForTab(tabId, deps);
+5
View File
@@ -215,6 +215,11 @@ refreshes do not return control by themselves. The result `outcome` is one of:
`note` carries any text the user typed back. `resolved_targets` reports `note` carries any text the user typed back. `resolved_targets` reports
which refs/selectors matched a live element. which refs/selectors matched a live element.
`request-help` does not refresh the page model after the user returns
control. After a `continued` or `completed` result, issue a separate
observation tool call (usually `bsk snapshot --session <id>`) before using
new refs or reasoning about the post-help page state.
### Recording — `bsk record` ### Recording — `bsk record`
Capture the user's own actions in the Agent Window to a `trace.json`, for later LLM-driven automation: Capture the user's own actions in the Agent Window to a `trace.json`, for later LLM-driven automation:
+14 -6
View File
@@ -68,6 +68,7 @@ Write operations only affect tabs in the **Agent Window** (or tabs you **borrowe
``` ```
bsk navigate <url> --session <id> bsk navigate <url> --session <id>
bsk snapshot --session <id> → aria tree with @e1, @e2, … refs bsk snapshot --session <id> → aria tree with @e1, @e2, … refs
bsk observe --session <id> → semantic VOM view; may reveal hover/focus surfaces
bsk click @e3 --session <id> → or bsk fill, bsk select, bsk press bsk click @e3 --session <id> → or bsk fill, bsk select, bsk press
bsk snapshot --session <id> → again after navigation / DOM change bsk snapshot --session <id> → again after navigation / DOM change
``` ```
@@ -78,11 +79,12 @@ Prefer `@eN` refs from the latest snapshot over raw CSS selectors. Use `--ref` /
## Observation priority ## Observation priority
Start with `bsk snapshot` to understand page structure, text, controls, and element refs. Only escalate when the latest snapshot cannot answer the question: Start with `bsk snapshot` to understand page structure, text, controls, and element refs. Use `bsk observe` when semantic VOM output or conditional hover/focus surfaces would materially help. Only escalate to raw HTML or screenshots when the latest observation cannot answer the question:
1. `bsk snapshot` — default for page understanding and interaction planning 1. `bsk snapshot` — strict static page understanding and interaction planning
2. `bsk get-html` — when hidden DOM, metadata, or markup details are required 2. `bsk observe` — semantic VOM observation; may run bounded perception probes such as hover-surface discovery
3. `bsk screenshot` — when visual layout, canvas/image content, or styling cannot be inferred from the snapshot. Use `--ref @eN` (from the latest snapshot) to crop to one element; omit `--ref` for the full visible tab. 3. `bsk get-html` — when hidden DOM, metadata, or markup details are required
4. `bsk screenshot` — when visual layout, canvas/image content, or styling cannot be inferred from the observation. Use `--ref @eN` (from the latest snapshot/observe) to crop to one element; omit `--ref` for the full visible tab.
Do **not** call `bsk get-html` or `bsk screenshot` first just to inspect a page. Do **not** call `bsk get-html` or `bsk screenshot` first just to inspect a page.
@@ -141,7 +143,8 @@ Details and flags: **`bsk <cmd> --help`**
| Command | Summary | | Command | Summary |
|---------|---------| |---------|---------|
| `bsk snapshot` | First-choice page understanding: accessibility tree with `@eN` element refs | | `bsk snapshot` | First-choice static page understanding: accessibility tree with `@eN` element refs |
| `bsk observe` | Semantic VOM observation with bounded perception probes for conditional surfaces |
| `bsk get-html` | Raw HTML dump after snapshot is insufficient (high token cost) | | `bsk get-html` | Raw HTML dump after snapshot is insufficient (high token cost) |
| `bsk screenshot` | PNG capture after snapshot is insufficient: full visible tab, or `--ref @eN` to crop to one element (`--out` path optional) | | `bsk screenshot` | PNG capture after snapshot is insufficient: full visible tab, or `--ref @eN` to crop to one element (`--out` path optional) |
@@ -212,6 +215,11 @@ refreshes do not return control by themselves. The result `outcome` is one of:
`note` carries any text the user typed back. `resolved_targets` reports `note` carries any text the user typed back. `resolved_targets` reports
which refs/selectors matched a live element. which refs/selectors matched a live element.
`request-help` does not refresh the page model after the user returns
control. After a `continued` or `completed` result, issue a separate
observation tool call (usually `bsk snapshot --session <id>`) before using
new refs or reasoning about the post-help page state.
### Recording — `bsk record` ### Recording — `bsk record`
Capture the user's own actions in the Agent Window to a `trace.json`, for later LLM-driven automation: Capture the user's own actions in the Agent Window to a `trace.json`, for later LLM-driven automation:
@@ -260,7 +268,7 @@ Always **`bsk session stop <id>`** in a `finally`-style path so the Agent Window
2. **No long borrow** — do not leave a user's personal tab in the Agent Window across unrelated tasks. 2. **No long borrow** — do not leave a user's personal tab in the Agent Window across unrelated tasks.
3. **No skip stop** — always `bsk session stop <id>`; never assume idle timeout will clean up. 3. **No skip stop** — always `bsk session stop <id>`; never assume idle timeout will clean up.
4. **No post-success control** — once the user’s goal (or last trace step) is met, do not keep operating the page; stop the session unless they asked to keep it open. 4. **No post-success control** — once the user’s goal (or last trace step) is met, do not keep operating the page; stop the session unless they asked to keep it open.
5. **No observe escalation before snapshot** — use `bsk snapshot` first; only use `bsk get-html` or `bsk screenshot` when the snapshot is insufficient. Element screenshots (`--ref @eN`) still require a fresh snapshot ref — never skip snapshot just to grab a visual. 5. **No raw observe escalation before snapshot/observe** — use `bsk snapshot` first; use `bsk observe` when VOM semantics or conditional surfaces help. Only use `bsk get-html` or `bsk screenshot` when snapshot/observe is insufficient. Element screenshots (`--ref @eN`) still require a fresh snapshot/observe ref — never skip observation just to grab a visual.
6. **`evaluate` is powerful and risky** — use only when snapshot + click/fill/select cannot suffice; never on credential surfaces. 6. **`evaluate` is powerful and risky** — use only when snapshot + click/fill/select cannot suffice; never on credential surfaces.
--- ---