Skip to content

WidgetForge

Summary: WidgetForge is the project's agent-drivable UMG authoring pipeline: declarative JSON specs compile into real WidgetBlueprints (WidgetForge.Apply), WidgetBlueprints export back into canonical specs (WidgetForge.Export), and widgets render headlessly to PNG for visual verification (WidgetForge.Render). A staleness gate makes human designers and agents safe co-authors of the same widgets: the widget asset is the source of truth, and Apply refuses to overwrite designer work it hasn't seen.

Why it exists

AI agents cannot drive the UMG Designer (drag-drop Slate UI), and UE Python cannot author widget trees (WidgetBlueprint.widget_tree is not reflected). The MCP bridge also blocks py console commands, so WidgetForge ships console commands backed by an editor-module C++ compiler (Source/ProjectEternalEditor/{Public,Private}/WidgetForge/). Prior-art survey (2026-07): three independent UE tools converged on the same JSON→regenerate architecture; none solved two-writer co-authoring. WidgetForge's model (export-first refresh + refuse-on-drift) follows the patterns that survived elsewhere (Slint, Android Studio layouts, W3C design tokens).

The co-authoring model (widget-as-truth)

Two writers touch the same widgets: a human in the UMG designer and an agent through specs. The contract that keeps them safe:

  • The widget asset is the source of truth. Specs in Tools/WidgetForge/specs/ are a full-fidelity, refreshable text mirror (and give binary UI assets reviewable git diffs).
  • Agents work Export-first: Export (refresh spec from widget) → edit spec → Apply.
  • The staleness gate makes the contract self-enforcing. Export stamps a tool-owned _meta block (sourceHash = hash of the widget's canonical tokenless export). Apply recomputes the hash of the current widget and refuses with status:"stale" on mismatch — a forgotten Export is a loud failure, never a silent clobber of designer edits. -Force overrides (audited as forcedOverwrite:true). After a successful Apply the spec's _meta is updated in place.
  • Apply rebuilds the widget tree from scratch (deterministic, idempotent) but never touches animations or the event graph; variable GUIDs are preserved by name, so bindings survive. Renames need "renamedFrom" on the node (one Apply) — GUIDs carry forward and animation bindings/graph references are fixed up; unmarked disappearing variables warn loudly.
  • Steady state: the designer opens any spec-managed WBP, edits anything, saves — done. The next agent Export folds those edits into the spec.

What can't round-trip (guarded, not silent): named-slot content (named_slots_unsupported refusal), tree-internal object pointers (export warns + skips). Everything else — any CPF_Edit property differing from its CDO, slots, classDefaults (diffed against the ParentClass CDO), tokens — round-trips byte-identically.

Commands

WidgetForge.Apply     <SpecJsonPath> [-Force]        # build/update WBP from spec (staleness-gated)
WidgetForge.Export    <AssetPath> <OutJsonPath> [<TokensPath>]   # widget -> canonical spec + _meta
WidgetForge.Render    <AssetPath> <W> <H> <OutPngPath>           # headless PNG (no PIE needed)
WidgetForge.RoundTrip <AssetPath> [<W> <H>]          # acceptance oracle: spec + pixel identity
WidgetForge.ApplyStyles <StylesJsonPath>             # CommonTextStyle-style Blueprint assets
WidgetForge.MakeFont  <FontFaceAssetPath> <OutFontAssetPath>   # wrap a FontFace in a runtime UFont

MakeFont exists because Python cannot write FCompositeFont (not script-exposed), so the TextureForge font pipeline needs a C++ hop to turn an imported UFontFace into a composite UFont asset.

Every call writes a JSON report to Saved/WidgetForge/last_result.json — always read it. Machine-readable status: ok, error, stale, pie_blocked, token_lint_failed, named_slots_unsupported, load_failed. All commands refuse during PIE (editor asset loading is disabled in play mode; pre-guard, Apply's create-path opened a modal "Overwrite Existing Object" dialog that silently blocked the game thread — the historical "MCP deadlock").

Apply wall-clock is ~0.9 s regardless of widget size (compile+save dominates; measured ×10 on a 5-widget and a 48-widget tree, 2026-07-02).

From an agent

The console commands are reachable headlessly three ways, all in Source/ProjectEternalEditor/:

  • MCP — ProjectEternalEditor.EternalAuthoringToolset exposes WidgetForgeApply, WidgetForgeExport and WidgetForgeRender as toolset tools (the report JSON rides in the call's Log); see Unreal MCP Guide. RoundTrip/ApplyStyles/MakeFont stay console-only.
  • Auto-export on compile — FWidgetForgeAutoExport (Private/WidgetForge/WidgetForgeAutoExport.h) binds when a widget asset is opened in the UMG editor and re-exports opted-in widgets to Tools/WidgetForge/specs/<AssetName>.spec.json on every compile; the per-asset opt-in toggle sits on the UMG editor toolbar and persists in UWidgetForgeEditorSettings (Public/WidgetForge/WidgetForgeEditorSettings.h, EditorPerProjectUserSettings: master switch bAutoExportOnCompile + AutoExportWidgets list). Export-only by design — Apply stays manual because that direction is destructive and staleness-gated.
  • Push watcher — Tools/WidgetForge/forge_push_watcher.py (auto-started by Content/Python/init_unreal.py) polls Tools/TextureForge/exports/.push/request.json; on a new request it imports the listed PNGs as UI textures, runs WidgetForge.Apply for every spec path in specs, and harvests each report into response.json. Requests that arrive during PIE wait until play ends.

Tokens and styles

Tools/WidgetForge/tokens.json is the design-token source ("$token" in specs). Semantic duplicates must be aliases ("era-arcanum": "$arcane"); Apply/Export lint the file and fail on ambiguous duplicates, unknown refs, and cycles. Export reverse-maps values to $token/hex. Shared text looks live in style Blueprint assets (TextStyles.styles.json via ApplyStyles) — a surface that is simultaneously designer-native (Details panel) and agent-native (JSON).

Format reference

The spec format itself (widget nodes, slots, shorthands, CSS→UMG conversion rules, hard-won caveats) is documented in Tools/WidgetForge/SPEC_FORMAT.md. Design history and rejected alternatives (merge-based Apply, spec-as-truth, denylists): ImplementationDocs/WidgetForge_DesignerCoAuthoring.plan.md.