fix(ai): unwrap redundant section-root when Type 0 plan has 1 subtask

Why: for Type 0 component plans (Notification Card / Profile Card / …)
the orchestrator pre-inserts a page rootFrame named after the component,
then the sub-agent emits its own section-root frame as the only child.
Result is a visible "Notification Card → Notification Card" double wrap
in the layers panel and a wasted layout depth that does nothing visual.
The double wrap was confirmed in the 2026-05-09 end-to-end test of the
notification-card prompt: depth-0 = orchestrator rootFrame (role=card),
depth-1 = sub-agent wrapper (also role=card), actual children at depth-2.

What: new unwrapSingleComponentSectionRoot pass added as Phase 4c right
after the mobile-status-bar dedup (mutually exclusive: that runs only on
mobile, this runs only on component-shaped plans). Conservative match —
only fires when:
  - plan.subtasks.length === 1, AND
  - plan.rootFrame is narrow (≤480) and auto-height (<480 or 0), AND
  - the orchestrator rootFrame has exactly 1 frame child, AND
  - that child's id has the sub-agent section-root suffix
    (`-root` / `-section`) OR the child copied the parent's name.

When the conditions hold, hoist the wrapper's children up via
store.moveNode (preserving order) and remove the wrapper. Multi-section
pages, dashboards, and mobile screens are untouched — early-out on the
plan.subtasks.length / width / height checks.

1070 / 1070 AI tests still pass; unit-testing this against the live
Zustand store is awkward, the integration verification will land via
the next end-to-end notification-card run.
This commit is contained in:
Fini 2026-05-09 20:59:32 +08:00
parent cf00c7c590
commit 094cec7d8d

View file

@ -103,6 +103,50 @@ function removeDuplicateStatusBars(rootNodes: FrameNode[]): void {
}
}
/**
* Type 0 component plans always have exactly 1 subtask, and the sub-agent
* conventionally emits a "section root frame" inside the orchestrator's
* pre-inserted page rootFrame. For multi-section pages that wrapper is the
* section container; for a Type 0 single-component design it's a redundant
* "Notification Card → Notification Card" double wrap that shows up in the
* layers panel and inflates layout depth for no visual benefit.
*
* This pass runs only for component-shaped plans (narrow + auto-height).
* If the orchestrator rootFrame ends up with exactly one child frame whose
* id was assigned the sub-agent section-root suffix (`-root` / `-section`)
* OR whose name copies the parent name, hoist that child's children up to
* the rootFrame and delete the wrapper. Conservative on any other shape —
* never touches multi-section pages.
*/
function unwrapSingleComponentSectionRoot(rootNodes: FrameNode[], plan: OrchestratorPlan): void {
if (plan.subtasks.length !== 1) return;
if (plan.rootFrame.width > 480) return;
if (typeof plan.rootFrame.height === 'number' && plan.rootFrame.height >= 480) return;
const store = useDocumentStore.getState();
for (const rn of rootNodes) {
const root = store.getNodeById(rn.id);
if (!root || root.type !== 'frame' || !root.children || root.children.length !== 1) continue;
const wrapper = root.children[0];
if (wrapper.type !== 'frame') continue;
const wrapperChildren =
'children' in wrapper && Array.isArray(wrapper.children) ? wrapper.children : [];
if (wrapperChildren.length === 0) continue;
const wrapperName = ('name' in wrapper ? (wrapper as { name?: string }).name : '') ?? '';
const wrapperId = wrapper.id;
const looksLikeSectionRoot =
wrapperId.endsWith('-root') ||
wrapperId.endsWith('-section') ||
(root.name && wrapperName === root.name);
if (!looksLikeSectionRoot) continue;
// Snapshot children ids in order before mutation, then move each one
// up to the rootFrame and remove the wrapper. Re-read the wrapper node
// between moves so the indices stay consistent with the live tree.
const childIds = wrapperChildren.map((c) => c.id);
childIds.forEach((cid, i) => store.moveNode(cid, root.id, i));
store.removeNode(wrapper.id);
}
}
function isAIDuplicateStatusBar(node: PenNode): boolean {
if (node.type !== 'frame') return false;
if ('role' in node && (node as { role?: string }).role === 'status-bar') return false;
@ -1140,6 +1184,12 @@ export async function executeOrchestration(
removeDuplicateStatusBars(rootNodes);
}
// -- Phase 4c: Unwrap single-component section root (Type 0) --
// Runs only for component-shape plans (narrow + auto-height) and
// is a no-op for everything else, so mutually exclusive with the
// mobile dedup above. Must also run BEFORE height adjustment.
unwrapSingleComponentSectionRoot(rootNodes, plan);
// Height adjustment runs after duplicate removal for both animated and
// non-animated paths so the frame size reflects the cleaned node tree.
if (dashboardColumnIds) {