Skip to content

Commit fcb46c7

Browse files
authored
Merge pull request #286367 from microsoft/dev/dmitriv/fetch-tool-frames
Fix web page loader to handle nested iframes correctly
2 parents 861cb20 + 81fff3e commit fcb46c7

3 files changed

Lines changed: 260 additions & 237 deletions

File tree

src/vs/platform/webContentExtractor/electron-main/cdpAccessibilityDomain.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,14 @@ interface AXNodeTree {
6161
parent: AXNodeTree | null;
6262
}
6363

64-
function createNodeTree(nodes: AXNode[]): AXNodeTree | null {
64+
/**
65+
* Creates a forest of node trees from the given AXNodes.
66+
* When nodes come from multiple frames (e.g., main frame + iframes),
67+
* each frame has its own RootWebArea, resulting in multiple trees.
68+
*/
69+
function createNodeTrees(nodes: AXNode[]): AXNodeTree[] {
6570
if (nodes.length === 0) {
66-
return null;
71+
return [];
6772
}
6873

6974
// Create a map of node IDs to their corresponding nodes for quick lookup
@@ -141,14 +146,16 @@ function createNodeTree(nodes: AXNode[]): AXNodeTree | null {
141146
}
142147
}
143148

144-
// Find the root node (a node without a parent)
149+
// Find all root nodes (nodes without a parent)
150+
// When nodes come from multiple frames, each frame has its own root
151+
const roots: AXNodeTree[] = [];
145152
for (const node of nodeMap.values()) {
146153
if (!node.parent) {
147-
return node;
154+
roots.push(node);
148155
}
149156
}
150157

151-
return null;
158+
return roots;
152159
}
153160

154161
/**
@@ -159,23 +166,38 @@ const LINE_MAX_LENGTH = 80;
159166

160167
/**
161168
* Converts an accessibility tree represented by AXNode objects into a markdown string.
169+
* Handles multiple root nodes (e.g., from main frame + iframes) by processing each tree
170+
* and combining the results.
162171
*
163172
* @param uri The URI of the document
164173
* @param axNodes The array of AXNode objects representing the accessibility tree
165174
* @returns A markdown representation of the accessibility tree
166175
*/
167176
export function convertAXTreeToMarkdown(uri: URI, axNodes: AXNode[]): string {
168-
const tree = createNodeTree(axNodes);
169-
if (!tree) {
177+
const trees = createNodeTrees(axNodes);
178+
if (trees.length === 0) {
170179
return ''; // Return empty string for empty tree
171180
}
172181

173-
// Process tree to extract main content and navigation links
174-
const mainContent = extractMainContent(uri, tree);
175-
const navLinks = collectNavigationLinks(tree);
182+
// Process each tree and collect main content and navigation links
183+
const allMainContent: string[] = [];
184+
const allNavLinks: string[] = [];
185+
186+
for (const tree of trees) {
187+
const mainContent = extractMainContent(uri, tree);
188+
const navLinks = collectNavigationLinks(tree);
189+
190+
if (mainContent.trim().length > 0) {
191+
allMainContent.push(mainContent);
192+
}
193+
allNavLinks.push(...navLinks);
194+
}
195+
196+
// Combine all main content from all trees
197+
const combinedMainContent = allMainContent.join('\n\n');
176198

177199
// Combine main content and navigation links
178-
return mainContent + (navLinks.length > 0 ? '\n\n## Additional Links\n' + navLinks.join('\n') : '');
200+
return combinedMainContent + (allNavLinks.length > 0 ? '\n\n## Additional Links\n' + allNavLinks.join('\n') : '');
179201
}
180202

181203
function extractMainContent(uri: URI, tree: AXNodeTree): string {

src/vs/platform/webContentExtractor/electron-main/webPageLoader.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import type { BeforeSendResponse, BrowserWindow, BrowserWindowConstructorOptions, Event, OnBeforeSendHeadersListenerDetails } from 'electron';
77
import { Queue, raceTimeout, TimeoutTimer } from '../../../base/common/async.js';
8-
import { CancellationTokenSource } from '../../../base/common/cancellation.js';
8+
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
99
import { createSingleCallFunction } from '../../../base/common/functional.js';
1010
import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
1111
import { URI } from '../../../base/common/uri.js';
@@ -21,6 +21,17 @@ type NetworkRequestEventParams = Readonly<{
2121
type?: string;
2222
}>;
2323

24+
type FrameInfo = Readonly<{
25+
id: string;
26+
url?: string;
27+
name?: string;
28+
}>;
29+
30+
type FrameTreeNode = Readonly<{
31+
frame: FrameInfo;
32+
childFrames?: FrameTreeNode[];
33+
}>;
34+
2435
/**
2536
* A web page loader that uses Electron to load web pages and extract their content.
2637
*/
@@ -336,7 +347,7 @@ export class WebPageLoader extends Disposable {
336347
try {
337348
await raceTimeout((async () => {
338349
if (!cts.token.isCancellationRequested) {
339-
result = await this.extractAccessibilityTreeContent() ?? '';
350+
result = await this.extractAccessibilityTreeContent(cts.token) ?? '';
340351
}
341352

342353
if (!cts.token.isCancellationRequested && result.length < WebPageLoader.MIN_CONTENT_LENGTH) {
@@ -371,13 +382,44 @@ export class WebPageLoader extends Disposable {
371382

372383
/**
373384
* Extracts content from the Accessibility tree of the loaded web page.
374-
* @return The extracted content, or undefined if extraction fails.
385+
* @param token Cancellation token to abort the operation.
386+
* @return The extracted content, or undefined if extraction fails or is cancelled.
375387
*/
376-
private async extractAccessibilityTreeContent(): Promise<string | undefined> {
388+
private async extractAccessibilityTreeContent(token: CancellationToken): Promise<string | undefined> {
377389
this.trace(`Extracting content using Accessibility domain`);
378390
try {
379-
const { nodes } = await this._debugger.sendCommand('Accessibility.getFullAXTree') as { nodes: AXNode[] };
380-
return convertAXTreeToMarkdown(this._uri, nodes);
391+
// Enable the Page domain to get frame information
392+
await this._debugger.sendCommand('Page.enable');
393+
if (token.isCancellationRequested) {
394+
return undefined;
395+
}
396+
397+
// Get all frames including iframes
398+
const { frameTree } = await this._debugger.sendCommand('Page.getFrameTree') as { frameTree: FrameTreeNode };
399+
if (token.isCancellationRequested) {
400+
return undefined;
401+
}
402+
403+
const frameNodes = [frameTree];
404+
for (let i = 0; i < frameNodes.length; i++) {
405+
frameNodes.push(...frameNodes[i].childFrames ?? []);
406+
}
407+
408+
// Collect accessibility nodes from all frames
409+
const allNodes: AXNode[] = [];
410+
for (const { frame } of frameNodes) {
411+
try {
412+
const { nodes } = await this._debugger.sendCommand('Accessibility.getFullAXTree', { frameId: frame.id }) as { nodes: AXNode[] };
413+
allNodes.push(...nodes);
414+
if (token.isCancellationRequested) {
415+
return undefined;
416+
}
417+
} catch {
418+
// ignore
419+
}
420+
}
421+
422+
return convertAXTreeToMarkdown(this._uri, allNodes);
381423
} catch (error) {
382424
this.trace(`Accessibility tree extraction failed: ${error instanceof Error ? error.message : String(error)}`);
383425
return undefined;

0 commit comments

Comments
 (0)