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
_metablock (sourceHash= hash of the widget's canonical tokenless export). Apply recomputes the hash of the current widget and refuses withstatus:"stale"on mismatch — a forgotten Export is a loud failure, never a silent clobber of designer edits.-Forceoverrides (audited asforcedOverwrite:true). After a successful Apply the spec's_metais 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.EternalAuthoringToolsetexposesWidgetForgeApply,WidgetForgeExportandWidgetForgeRenderas toolset tools (the report JSON rides in the call'sLog); 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 toTools/WidgetForge/specs/<AssetName>.spec.jsonon every compile; the per-asset opt-in toggle sits on the UMG editor toolbar and persists inUWidgetForgeEditorSettings(Public/WidgetForge/WidgetForgeEditorSettings.h,EditorPerProjectUserSettings: master switchbAutoExportOnCompile+AutoExportWidgetslist). 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 byContent/Python/init_unreal.py) pollsTools/TextureForge/exports/.push/request.json; on a new request it imports the listed PNGs as UI textures, runsWidgetForge.Applyfor every spec path inspecs, and harvests each report intoresponse.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.