fix(vom): bug fix

This commit is contained in:
Ljy-0827
2026-09-08 10:54:33 +08:00
parent 905ba6a8a6
commit aeece8d7e4
5 changed files with 384 additions and 56 deletions
+91 -43
View File
@@ -88,6 +88,7 @@ const MAX_CUSTOM_RECOVERY_AREA = 180_000;
const MAX_RECOVERABLE_SINGLE_INTENT_AREA = 360_000;
const MAX_HANDLE_CONTEXT_ITEMS = 3;
const MAX_SURFACE_ITEMS = 12;
const MAX_VISUAL_SURFACES = 8;
const MAX_SCOPE_LINES = 40;
const RECOVERY_HANDLER_ATTRS = ["onclick", "onmousedown", "onkeydown", "onkeyup", "onkeypress"];
const NATIVE_TAGS = new Set(["button", "input", "select", "textarea"]);
@@ -794,40 +795,81 @@ interface RenderState {
duplicateRefNames: Set<string>;
surfaceMap: Map<number, CondSurface>;
scopeMap: Map<number, ActiveScopeBlock>;
visualSurfaces: Map<number | null, VomVisualSurface[]>;
visualSurfaces: VomVisualSurface[];
}
function groupedByParent<T extends { parentId: number | null }>(
items: T[],
): Map<number | null, T[]> {
const grouped = new Map<number | null, T[]>();
for (const item of items) {
const siblings = grouped.get(item.parentId) ?? [];
siblings.push(item);
grouped.set(item.parentId, siblings);
}
return grouped;
function visualSurfaceArea(surface: VomVisualSurface): number {
return Math.max(0, surface.visibleRect.w) * Math.max(0, surface.visibleRect.h);
}
function emitVisualEntries(parentId: number | null, depth: number, state: RenderState): void {
if (state.stopped) return;
for (const surface of state.visualSurfaces.get(parentId) ?? []) {
if (depth > state.maxDepth) {
state.truncated = true;
function compareVisualSurfaces(a: VomVisualSurface, b: VomVisualSurface): number {
const aLabeled = cleaned(a.label) !== undefined;
const bLabeled = cleaned(b.label) !== undefined;
return (
Number(bLabeled) - Number(aLabeled) ||
visualSurfaceArea(b) - visualSurfaceArea(a) ||
a.frameId.localeCompare(b.frameId) ||
a.visibleRect.y - b.visibleRect.y ||
a.visibleRect.x - b.visibleRect.x ||
a.backendNodeId - b.backendNodeId
);
}
function visualSurfaceLine(surface: VomVisualSurface, ref: string): string {
const label = cleaned(surface.label) ?? `${surface.renderingKind} visual surface`;
const layers = surface.memberCount > 1 ? `; layers=${surface.memberCount}` : "";
const { x, y, w, h } = surface.visibleRect;
const bounds = [x, y, w, h].map(Math.round).join(",");
return ` @${ref} surface ${JSON.stringify(label)} [bounds=${bounds}; rendering=${surface.renderingKind}${layers}; visual-only; requires=image-understanding; use: bsk screenshot --ref @${ref}]`;
}
function selectVisualSurfaces(
surfaces: readonly VomVisualSurface[],
nodesById: ReadonlyMap<number, VomNode>,
): { selected: VomVisualSurface[]; eligibleCount: number } {
const selected: VomVisualSurface[] = [];
let eligibleCount = 0;
for (const surface of surfaces) {
if (surface.parentId !== null && !nodesById.has(surface.parentId)) continue;
eligibleCount += 1;
const insertionIndex = selected.findIndex(
(candidate) => compareVisualSurfaces(surface, candidate) < 0,
);
if (insertionIndex < 0) {
if (selected.length < MAX_VISUAL_SURFACES) selected.push(surface);
continue;
}
selected.splice(insertionIndex, 0, surface);
if (selected.length > MAX_VISUAL_SURFACES) selected.pop();
}
return { selected, eligibleCount };
}
function emitVisualAppendix(state: RenderState): void {
if (state.stopped) return;
const { selected, eligibleCount } = selectVisualSurfaces(state.visualSurfaces, state.nodesById);
if (eligibleCount === 0) return;
const header = "[visual surfaces]";
let headerEmitted = false;
let emitted = 0;
for (const surface of selected) {
const ref = `e${state.nextRef}`;
const label = cleaned(surface.label) ?? `${surface.renderingKind} visual surface`;
const layers = surface.memberCount > 1 ? `; layers=${surface.memberCount}` : "";
const line = `${" ".repeat(depth)}@${ref} surface ${JSON.stringify(label)} [rendering=${surface.renderingKind}${layers}; visual-only; requires=image-understanding; use: bsk screenshot --ref @${ref}]`;
const nextTokens = state.tokens + estimateTokens(line);
if (nextTokens > state.maxTokens) {
state.truncated = true;
state.stopped = true;
return;
const line = visualSurfaceLine(surface, ref);
const nextTokens =
state.tokens + estimateTokens(line) + (headerEmitted ? 0 : estimateTokens(header));
if (nextTokens > state.maxTokens) break;
if (!headerEmitted) {
state.lines.push(header);
state.tokens += estimateTokens(header);
headerEmitted = true;
}
state.lines.push(line);
state.tokens = nextTokens;
state.tokens += estimateTokens(line);
const label = cleaned(surface.label) ?? `${surface.renderingKind} visual surface`;
state.refs.push({
ref,
backendNodeId: surface.backendNodeId,
@@ -840,6 +882,20 @@ function emitVisualEntries(parentId: number | null, depth: number, state: Render
line: state.lines.length - 1,
});
state.nextRef += 1;
emitted += 1;
}
const omitted = eligibleCount - emitted;
if (omitted > 0) {
state.truncated = true;
if (headerEmitted) {
const summary = ` … ${omitted} additional visual surfaces omitted`;
const nextTokens = state.tokens + estimateTokens(summary);
if (nextTokens <= state.maxTokens) {
state.lines.push(summary);
state.tokens = nextTokens;
}
}
}
}
@@ -872,34 +928,24 @@ function emitActiveScopeBlock(scope: ActiveScopeBlock, depth: number, state: Ren
}
}
type RenderTask =
| { kind: "node"; node: VomNode; depth: number }
| { kind: "visual"; parentId: number | null; depth: number };
function pushRenderChildren(
stack: RenderTask[],
stack: Array<{ node: VomNode; depth: number }>,
children: readonly VomNode[],
depth: number,
): void {
for (let index = children.length - 1; index >= 0; index -= 1) {
stack.push({ kind: "node", node: children[index], depth });
stack.push({ node: children[index], depth });
}
}
function renderTree(children: Map<number | null, VomNode[]>, state: RenderState): void {
const stack: RenderTask[] = [{ kind: "visual", parentId: null, depth: 1 }];
const stack: Array<{ node: VomNode; depth: number }> = [];
pushRenderChildren(stack, children.get(null) ?? [], 1);
while (stack.length > 0 && !state.stopped) {
const task = stack.pop() as RenderTask;
if (task.kind === "visual") {
emitVisualEntries(task.parentId, task.depth, state);
continue;
}
const { node, depth } = task;
const { node, depth } = stack.pop() as { node: VomNode; depth: number };
if (!shouldRender(node)) {
stack.push({ kind: "visual", parentId: node.id, depth });
pushRenderChildren(stack, children.get(node.id) ?? [], depth);
continue;
}
@@ -942,10 +988,8 @@ function renderTree(children: Map<number | null, VomNode[]>, state: RenderState)
if (scope) emitActiveScopeBlock(scope, depth + 1, state);
if ((ref && shouldSkipRedundantRefChildren(node)) || shouldSkipRedundantChildren(node, state)) {
emitVisualEntries(node.id, depth + 1, state);
continue;
}
stack.push({ kind: "visual", parentId: node.id, depth: depth + 1 });
pushRenderChildren(stack, children.get(node.id) ?? [], depth + 1);
}
}
@@ -976,7 +1020,7 @@ function renderNodes(
duplicateRefNames: duplicateReferenceNames(nodes),
surfaceMap: new Map(surfaces.map((surface) => [surface.triggerId, surface])),
scopeMap: new Map(activeScopeBlocks.map((scope) => [scope.triggerId, scope])),
visualSurfaces: groupedByParent(visualSurfaces),
visualSurfaces,
};
renderTree(children, state);
@@ -1198,7 +1242,10 @@ function renderDoubleLayer(scene: VomScene, layer: BlockingLayer, options: VomOp
scene.activeScopeBlocks,
scene.visualSurfaces?.filter((surface) => visibleSurface(surface.parentId)),
);
state.lines.push(renderPageOcclusionLine(hiddenCount));
const occlusionLine = renderPageOcclusionLine(hiddenCount);
state.lines.push(occlusionLine);
state.tokens += estimateTokens(occlusionLine);
emitVisualAppendix(state);
return {
text: state.lines.join("\n"),
@@ -1227,6 +1274,7 @@ export function renderVom(scene: VomScene, options: VomOptions = {}): VomResult
scene.activeScopeBlocks,
scene.visualSurfaces,
);
emitVisualAppendix(state);
return {
text: state.lines.join("\n"),