Skip to content

Commit 08a6768

Browse files
committed
fix tab focus and check uncheck
1 parent aa37ae3 commit 08a6768

5 files changed

Lines changed: 173 additions & 52 deletions

File tree

‎src/agent/index.ts‎

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,29 @@ export class HyperAgent<T extends BrowserProviders = "Local"> {
332332
if (!this.context) {
333333
throw new HyperagentError("No context found");
334334
}
335+
336+
// Poll context for new pages to catch any that opened since the last check
337+
// This handles race conditions where the 'page' event might not have fired yet
338+
// or where we missed it during a heavy operation.
339+
const pages = this.context.pages();
340+
if (pages.length > 0) {
341+
const lastPage = pages[pages.length - 1];
342+
// If the last page is different and not closed, switch to it
343+
// We prefer the newest page as it's likely the result of the user's last action
344+
if (
345+
lastPage &&
346+
!lastPage.isClosed() &&
347+
lastPage !== this._currentPage
348+
) {
349+
if (this.debug) {
350+
console.log(
351+
`[HyperAgent] Polling detected new page, switching focus: ${lastPage.url()}`
352+
);
353+
}
354+
this._currentPage = lastPage;
355+
}
356+
}
357+
335358
if (!this.currentPage || this.currentPage.isClosed()) {
336359
this._currentPage = await this.context.newPage();
337360

@@ -406,6 +429,7 @@ export class HyperAgent<T extends BrowserProviders = "Local"> {
406429
mcpClient: this.mcpClient,
407430
variables: this._variables,
408431
cdpActions: this.cdpActionsEnabled,
432+
activePage: () => this.getCurrentPage(),
409433
},
410434
taskState,
411435
mergedParams
@@ -458,6 +482,7 @@ export class HyperAgent<T extends BrowserProviders = "Local"> {
458482
mcpClient: this.mcpClient,
459483
variables: this._variables,
460484
cdpActions: this.cdpActionsEnabled,
485+
activePage: () => this.getCurrentPage(),
461486
},
462487
taskState,
463488
mergedParams
@@ -1062,12 +1087,18 @@ export class HyperAgent<T extends BrowserProviders = "Local"> {
10621087

10631088
private setupHyperPage(page: Page): HyperPage {
10641089
const hyperPage = page as HyperPage;
1065-
hyperPage.ai = (task: string, params?: TaskParams) =>
1066-
this.executeTask(task, params, page);
1067-
hyperPage.aiAction = (instruction: string, params?: TaskParams) =>
1068-
this.executeSingleAction(instruction, page, params);
1069-
hyperPage.aiAsync = (task: string, params?: TaskParams) =>
1070-
this.executeTaskAsync(task, params, page);
1090+
hyperPage.ai = async (task: string, params?: TaskParams) => {
1091+
const activePage = await this.getCurrentPage();
1092+
return this.executeTask(task, params, activePage);
1093+
};
1094+
hyperPage.aiAction = async (instruction: string, params?: TaskParams) => {
1095+
const activePage = await this.getCurrentPage();
1096+
return this.executeSingleAction(instruction, activePage, params);
1097+
};
1098+
hyperPage.aiAsync = async (task: string, params?: TaskParams) => {
1099+
const activePage = await this.getCurrentPage();
1100+
return this.executeTaskAsync(task, params, activePage);
1101+
};
10711102
hyperPage.extract = async (task, outputSchema, params) => {
10721103
if (!task && !outputSchema) {
10731104
throw new HyperagentError(
@@ -1080,11 +1111,14 @@ export class HyperAgent<T extends BrowserProviders = "Local"> {
10801111
...params,
10811112
outputSchema,
10821113
};
1114+
1115+
const activePage = await this.getCurrentPage();
1116+
10831117
if (task) {
10841118
const res = await this.executeTask(
10851119
`You have to perform an extraction on the current page. You have to perform the extraction according to the task: ${task}. Make sure your final response only contains the extracted content`,
10861120
taskParams,
1087-
page
1121+
activePage
10881122
);
10891123
if (outputSchema) {
10901124
const outputText = res.output;
@@ -1106,7 +1140,7 @@ export class HyperAgent<T extends BrowserProviders = "Local"> {
11061140
const res = await this.executeTask(
11071141
"You have to perform a data extraction on the current page. Make sure your final response only contains the extracted content",
11081142
taskParams,
1109-
page
1143+
activePage
11101144
);
11111145
if (typeof res.output !== "string" || res.output === "") {
11121146
throw new Error(

‎src/agent/shared/find-element.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { ExamineDomResult } from "../examine-dom/types";
1010
import type { AccessibilityNode } from "@/context-providers/a11y-dom/types";
1111
import { captureDOMState } from "./dom-capture";
1212
import type { A11yDOMState } from "@/context-providers/a11y-dom/types";
13+
import { waitForSettledDOM } from "@/utils/waitForSettledDOM";
1314

1415
export interface FindElementOptions {
1516
/**
@@ -70,25 +71,23 @@ export async function findElementWithInstruction(
7071

7172
// Retry loop with DOM refresh (matches aiAction's findElementWithRetry pattern)
7273
for (let attempt = 0; attempt < maxRetries; attempt++) {
73-
7474
if (debug) {
7575
if (attempt === 0) {
7676
console.log(`[findElement] Starting attempt ${attempt + 1}`);
7777
} else {
78-
console.log(
79-
`[findElement] Retry ${attempt + 1}/${maxRetries}`
80-
);
78+
console.log(`[findElement] Retry ${attempt + 1}/${maxRetries}`);
8179
}
8280
}
8381

82+
await waitForSettledDOM(page);
8483
// Fetch FRESH a11y tree using the robust shared utility
8584
// captureDOMState handles DOM settling and retries for bad snapshots internally for this *single* capture attempt
8685
// We still need our outer loop for retrying the *finding* logic (e.g. if the LLM can't find the element)
8786
const domState = await captureDOMState(page, {
8887
debug,
8988
// Don't retry capture inside captureDOMState too aggressively since we have an outer loop here
9089
// But we do want it to handle transient CDP errors
91-
maxRetries: 2
90+
maxRetries: 2,
9291
});
9392

9493
if (debug) {

‎src/agent/tools/agent.ts‎

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ const runAction = async (
157157
};
158158

159159
if (ctx.cdpActions) {
160-
const { cdpClient, frameContextManager } = await initializeRuntimeContext(page, ctx.debug);
160+
const { cdpClient, frameContextManager } = await initializeRuntimeContext(
161+
page,
162+
ctx.debug
163+
);
161164
actionCtx.cdp = {
162165
resolveElement,
163166
dispatchCDPAction,
@@ -233,27 +236,32 @@ export const runAgentTask = async (
233236
];
234237

235238
let output = "";
236-
const page = taskState.startingPage;
239+
let page = taskState.startingPage;
237240
const useDomCache = params?.useDomCache === true;
238241
const enableDomStreaming = params?.enableDomStreaming === true;
239-
242+
240243
// Track schema validation errors across steps
241244
if (!ctx.schemaErrors) {
242245
ctx.schemaErrors = [];
243246
}
244-
247+
245248
const navigationDirtyHandler = (): void => {
246249
markDomSnapshotDirty(page);
247250
};
248-
page.on("framenavigated", navigationDirtyHandler);
249-
page.on("framedetached", navigationDirtyHandler);
250-
page.on("load", navigationDirtyHandler);
251-
252-
const cleanupDomListeners = (): void => {
253-
page.off?.("framenavigated", navigationDirtyHandler);
254-
page.off?.("framedetached", navigationDirtyHandler);
255-
page.off?.("load", navigationDirtyHandler);
251+
252+
const setupDomListeners = (p: Page) => {
253+
p.on("framenavigated", navigationDirtyHandler);
254+
p.on("framedetached", navigationDirtyHandler);
255+
p.on("load", navigationDirtyHandler);
256+
};
257+
258+
const cleanupDomListeners = (p: Page) => {
259+
p.off?.("framenavigated", navigationDirtyHandler);
260+
p.off?.("framedetached", navigationDirtyHandler);
261+
p.off?.("load", navigationDirtyHandler);
256262
};
263+
264+
setupDomListeners(page);
257265
let currStep = 0;
258266
let consecutiveFailuresOrWaits = 0;
259267
const MAX_CONSECUTIVE_FAILURES_OR_WAITS = 5;
@@ -263,8 +271,23 @@ export const runAgentTask = async (
263271
try {
264272
// Initialize context at the start of the task
265273
await initializeRuntimeContext(page, ctx.debug);
266-
274+
267275
while (true) {
276+
// Check for page context switch
277+
if (ctx.activePage) {
278+
const newPage = await ctx.activePage();
279+
if (newPage && newPage !== page) {
280+
if (ctx.debug) {
281+
console.log(`[Agent] Switching active page context to ${newPage.url()}`);
282+
}
283+
cleanupDomListeners(page);
284+
page = newPage;
285+
setupDomListeners(page);
286+
await initializeRuntimeContext(page, ctx.debug);
287+
markDomSnapshotDirty(page);
288+
}
289+
}
290+
268291
// Status Checks
269292
const status: TaskStatus = taskState.status;
270293
if (status === TaskStatus.PAUSED) {
@@ -292,16 +315,19 @@ export const runAgentTask = async (
292315
const domChunks: string | null = null;
293316
try {
294317
const domFetchStart = performance.now();
295-
318+
319+
await waitForSettledDOM(page);
296320
domState = await captureDOMState(page, {
297321
useCache: useDomCache,
298322
debug: ctx.debug,
299323
enableVisualMode: params?.enableVisualMode ?? false,
300324
debugStepDir: ctx.debug ? debugStepDir : undefined,
301325
enableStreaming: enableDomStreaming,
302-
onFrameChunk: enableDomStreaming ? () => {
303-
// captureDOMState handles aggregation
304-
} : undefined
326+
onFrameChunk: enableDomStreaming
327+
? () => {
328+
// captureDOMState handles aggregation
329+
}
330+
: undefined,
305331
});
306332

307333
const domDuration = performance.now() - domFetchStart;
@@ -366,14 +392,14 @@ export const runAgentTask = async (
366392
trimmedScreenshot,
367393
Object.values(ctx.variables)
368394
);
369-
395+
370396
// Append accumulated schema errors from previous steps
371397
if (ctx.schemaErrors && ctx.schemaErrors.length > 0) {
372398
const errorSummary = ctx.schemaErrors
373399
.slice(-3) // Only keep last 3 errors to avoid context bloat
374-
.map(err => `Step ${err.stepIndex}: ${err.error}`)
375-
.join('\n');
376-
400+
.map((err) => `Step ${err.stepIndex}: ${err.error}`)
401+
.join("\n");
402+
377403
msgs = [
378404
...msgs,
379405
{
@@ -395,7 +421,7 @@ export const runAgentTask = async (
395421
const agentOutput = await (async () => {
396422
const maxAttempts = 3;
397423
let currentMsgs = msgs;
398-
424+
399425
for (let attempt = 0; attempt < maxAttempts; attempt++) {
400426
const structuredResult = await retry({
401427
func: () =>
@@ -431,7 +457,7 @@ export const runAgentTask = async (
431457

432458
const providerId = ctx.llm?.getProviderId?.() ?? "unknown-provider";
433459
const modelId = ctx.llm?.getModelId?.() ?? "unknown-model";
434-
460+
435461
// Try to get detailed Zod validation error
436462
let validationError = "Unknown validation error";
437463
if (structuredResult.rawText) {
@@ -446,27 +472,28 @@ export const runAgentTask = async (
446472
}
447473
}
448474
}
449-
475+
450476
console.error(
451477
`[LLM][StructuredOutput] Failed to parse response from ${providerId} (${modelId}). Raw response: ${
452478
structuredResult.rawText?.trim() || "<empty>"
453479
} (attempt ${attempt + 1}/${maxAttempts})`
454480
);
455-
481+
456482
// Store error for cross-step learning
457483
ctx.schemaErrors?.push({
458484
stepIndex: currStep,
459485
error: validationError,
460486
rawResponse: structuredResult.rawText || "",
461487
});
462-
488+
463489
// Append error feedback for next retry
464490
if (attempt < maxAttempts - 1) {
465491
currentMsgs = [
466492
...currentMsgs,
467493
{
468494
role: "assistant",
469-
content: structuredResult.rawText || "Failed to generate response",
495+
content:
496+
structuredResult.rawText || "Failed to generate response",
470497
},
471498
{
472499
role: "user",
@@ -629,7 +656,7 @@ export const runAgentTask = async (
629656

630657
logPerf(ctx.debug, `[Perf][runAgentTask] Task ${taskId}`, taskStart);
631658
} finally {
632-
cleanupDomListeners();
659+
cleanupDomListeners(page);
633660
}
634661

635662
const taskOutput: TaskOutput = {

‎src/agent/tools/types.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AgentActionDefinition } from "@/types/agent/actions/types";
22
import { MCPClient } from "../mcp/client";
33
import { HyperAgentLLM } from "@/llm/types";
44
import { HyperVariable } from "@/types/agent/types";
5+
import { Page } from "playwright-core";
56

67
export interface AgentCtx {
78
mcpClient?: MCPClient;
@@ -17,4 +18,5 @@ export interface AgentCtx {
1718
error: string;
1819
rawResponse: string;
1920
}>;
21+
activePage?: () => Promise<Page>;
2022
}

0 commit comments

Comments
 (0)