-
Notifications
You must be signed in to change notification settings - Fork 35
feat(read_page): add opt-in AX compact and delta output #1087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
631a3fb
db8c2f8
80f4ac2
7a33b57
a2fc036
75ec1a5
f12a1b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -125,11 +125,45 @@ const definition: MCPToolDefinition = { | |
| enum: ['none', 'dom'], | ||
| description: 'AX mode only: use "dom" to explicitly fall back to DOM output if AX output exceeds the output budget. Default: none.', | ||
| }, | ||
| compact: { | ||
| type: 'boolean', | ||
| description: 'AX mode only: return a compact AX snapshot that keeps actionable/ref-bearing nodes, value/state nodes, and ancestors. Default: false.', | ||
| }, | ||
| }, | ||
| required: ['tabId'], | ||
| }, | ||
| }; | ||
|
|
||
|
|
||
| function compactAXLines(lines: string[]): string[] { | ||
| const keep = new Set<number>(); | ||
| const stack: Array<{ indent: number; index: number }> = []; | ||
|
|
||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
| const indent = line.match(/^ */)?.[0].length ?? 0; | ||
| while (stack.length > 0 && stack[stack.length - 1].indent >= indent) { | ||
| stack.pop(); | ||
| } | ||
|
|
||
| const actionableOrValuable = | ||
| line.includes('[ref_') || | ||
| line.includes(' = "') || | ||
| /\((focused|disabled|checked|selected|expanded)/.test(line); | ||
|
|
||
| if (actionableOrValuable) { | ||
| keep.add(i); | ||
| for (const ancestor of stack) { | ||
| keep.add(ancestor.index); | ||
| } | ||
| } | ||
|
|
||
| stack.push({ indent, index: i }); | ||
| } | ||
|
|
||
| return lines.filter((_, index) => keep.has(index)); | ||
| } | ||
|
|
||
| interface AXNode { | ||
| nodeId: number; | ||
| backendDOMNodeId?: number; | ||
|
|
@@ -188,6 +222,7 @@ const handler: ToolHandler = async ( | |
| }; | ||
| } | ||
| const axOverflowFallback = (args.fallback as string | undefined) || 'none'; | ||
| const compactAX = args.compact === true; | ||
| if (axOverflowFallback !== 'none' && axOverflowFallback !== 'dom') { | ||
| return { | ||
| content: [{ type: 'text', text: `Error: Invalid fallback "${axOverflowFallback}". Must be "none" or "dom".` }], | ||
|
|
@@ -871,11 +906,31 @@ const handler: ToolHandler = async ( | |
| formatNode(root, 0); | ||
| } | ||
|
|
||
| const output = lines.join('\n'); | ||
| const outputLines = compactAX ? compactAXLines(lines) : lines; | ||
| const output = outputLines.join('\n'); | ||
| const outputCharCount = output.length; | ||
|
Comment on lines
+909
to
+911
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| const includePaginationAx = args.includePagination !== false; | ||
| const axPaginationSection = includePaginationAx ? formatPaginationSection(await detectPagination(page, tabId)) : ''; | ||
|
|
||
| if (charCount > MAX_OUTPUT) { | ||
| const compression = args.compression as string | undefined; | ||
| if (compression === 'delta') { | ||
| const snapshotStore = SnapshotStore.getInstance(); | ||
| const axCacheTabId = `${tabId}:ax${compactAX ? ':compact' : ''}`; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The AX delta cache key only uses Useful? React with 👍 / 👎. |
||
| const previous = snapshotStore.get(sessionId, axCacheTabId); | ||
| if (previous) { | ||
| const delta = snapshotStore.computeDelta(previous, output, axPageStats.url); | ||
| snapshotStore.set(sessionId, axCacheTabId, output, axPageStats.url); | ||
|
Comment on lines
+921
to
+922
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| if (delta.isDelta) { | ||
| return { | ||
| content: [{ type: 'text', text: pageStatsLine + delta.content.replace('[DOM Delta', '[AX Delta') + axPaginationSection }], | ||
| }; | ||
|
Comment on lines
+923
to
+926
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| } | ||
| } else { | ||
| snapshotStore.set(sessionId, axCacheTabId, output, axPageStats.url); | ||
| } | ||
| } | ||
|
|
||
| if (outputCharCount > MAX_OUTPUT) { | ||
| // Large AX output should not trigger a second full DOM traversal unless | ||
| // the caller explicitly opts into that fallback. Otherwise preserve AX | ||
| // intent and return the bounded/truncated AX representation. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In compact AX mode, compaction is applied only after the full traversal output is collected, but traversal itself still stops once
charCountexceedsMAX_OUTPUTinformatNode. On large pages with many non-actionable/no-ref nodes, this means traversal can terminate early and omit later actionable nodes even though the compacted output would have fit the budget. The newcompact: trueoption therefore does not reliably preserve actionable content on long trees.Useful? React with 👍 / 👎.