fix(ai): dispatcher path runs post-pass cleanups (nav fill, role resolve, etc)

Visible regression in the GPT-5.5 food-app run: the bottom nav
shipped with no surface fill (floating icons on the cream root
background), even though `injectMissingNavSurfaceFill` was wired in
and verified to add a fill on top-level nav-role frames. Live doc
inspection showed `bottom-tab-bar` carrying `fill: undefined`
post-generation — the inject pass simply never ran.

Root cause: `orchestrator-sub-agent.ts` runs the dispatcher branch
(Strategy A `<op_tool>` element-tools AND Strategy B JSONL-in-
batch_design) and **early-returns before** reaching
`applyPostStreamingTreeHeuristics(rootId)` further down in the
function. That post-pass is what runs:
  - normalizeStrokeFillSchema
  - unwrapFakePhoneMockups
  - resolveTreeRoles + resolveTreePostPass
  - normalizeTreeLayout
  - stripRedundantSectionFills
  - injectMissingNavSurfaceFill
  - publish (forcePageResync)

Skipping it on the dispatcher path means EVERY sub-agent that emits
via element tools or JSONL fallback bypasses role resolution, layout
normalization, redundant-fill stripping, AND nav-surface injection.
The streaming path was the only branch that fired the cleanup.

Fix: call `applyPostStreamingTreeHeuristics(subtask.parentFrameId ??
plan.rootFrame.id)` right before the dispatcher branch returns, when
at least one node was inserted. The post-pass walks up to the page
root via `getParentOf()` for the inject step, so passing the section
root that the dispatcher inserted into is correct.

This also un-blocks several heuristics that depend on the full
subtree being in the store: button width / frame height equalization,
clipContent on cards-with-image-children, and theme detection on the
sub-agent's root (which feeds icon/text color defaults).
This commit is contained in:
Fini 2026-05-05 12:22:42 +08:00
parent 2d289eec5a
commit cfb5b70e15

View file

@ -570,6 +570,22 @@ async function executeSubAgent(
.filter((r) => r.status !== 'applied')
.map((r) => `[${r.toolName}] ${r.message}`)
.join('; ') || 'element-tool dispatch produced no nodes';
// Run the same tree-aware post-pass the streaming path runs
// below at line ~630. Without this, dispatcher-path inserts
// (Strategy A `<op_tool>` chains AND Strategy B JSONL-in-
// batch_design fallback) skip role resolution, layout
// normalization, redundant-fill stripping, AND nav-surface fill
// injection. The visible regression: sub-agents that emit a
// `bottom-tab-bar` / `top-app-bar` / etc. without an explicit
// fill ship floating-on-cream nav rows, and banner sections
// miss the height/clipContent fix-ups that depend on
// role-resolved children. `subtask.parentFrameId` is the
// section root the dispatcher inserted into; the post-pass
// walks up to the actual page root for the inject pass.
if (inserted.length > 0) {
const postPassRootId = subtask.parentFrameId ?? plan.rootFrame.id;
applyPostStreamingTreeHeuristics(postPassRootId);
}
return {
subtaskId: subtask.id,
nodes: inserted.length > 0 ? inserted : renderer.getInsertedNodes(),