Capture the 'why beyond chat' the user articulated: chat is the proving ground; the real payoff is inventory/spell-bar/vendor/character-sheet/trade becoming data-driven assembly + thin controller. Notes what carries forward (the generic widget toolkit + the find-by-id controller pattern) vs what those windows still need (ListBox/Panel + Field drag-drop, the window-manager half of Plan 2, and per-domain item/container data). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
386 lines
21 KiB
Markdown
386 lines
21 KiB
Markdown
# D.2b — Widget generalization (LayoutDesc importer, Plan 2 widget piece) — design
|
||
|
||
**Date:** 2026-06-16
|
||
**Branch:** `claude/hopeful-maxwell-214a12` (D.2b retail-UI track)
|
||
**Status:** design — approved scope ("full registry, vitals last & gated"), pending spec review
|
||
**Predecessor:** the LayoutDesc importer, the vitals re-drive, and the chat-window re-drive
|
||
(`docs/superpowers/specs/2026-06-15-layoutdesc-importer-design.md`,
|
||
`docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md`,
|
||
`docs/research/2026-06-15-layoutdesc-format.md`,
|
||
`claude-memory/project_d2b_retail_ui.md`).
|
||
**Opening context:** the "GENERALIZATION PASS — START-COLD CONTEXT" note in
|
||
`claude-memory/project_d2b_retail_ui.md`.
|
||
|
||
---
|
||
|
||
## 1. Goal
|
||
|
||
Refactor the hand-named chat widgets (`UiChatView` / `UiChatInput` /
|
||
`UiChatScrollbar` / `UiChannelMenu`) and the inline Send/Max-Min `UiDatElement`
|
||
click-wiring into **generic, Type-registered widgets** built by
|
||
`DatWidgetFactory`, so that `ChatWindowController` (and, as the final gated step,
|
||
`VitalsController`) collapses to a thin **find-widget-by-id → bind-data/behavior**
|
||
controller — the acdream analogue of retail `gm*UI::PostInit`.
|
||
|
||
**The code is modern. The behavior is retail.** This pass changes the
|
||
*construction path* of widgets, not their on-screen behavior. The chat window
|
||
must stay visually and behaviorally identical through every step except the final
|
||
(gated) vitals rewire.
|
||
|
||
### 1.1 Why this is mostly already done
|
||
|
||
The trace that opened this work (re-confirmed in this design session) established
|
||
two facts that make the generalization a *registration* task, not a new mechanism:
|
||
|
||
1. **The importer's base-chain Type resolution is already retail-faithful.**
|
||
`ElementReader.Merge` resolves a Type-0 placement element up its
|
||
`BaseElement`/`BaseLayoutId` chain to the base's real registered Type
|
||
(`ElementReader.cs:137-140`). Every chat/vitals element therefore already
|
||
resolves to the retail class it would instantiate.
|
||
|
||
2. **Type 12 is `UIElement_Text` — a real behavioral class, not a "style
|
||
prototype to skip."** Verified directly in the decomp:
|
||
`UIElement::RegisterElementClass(0xc, UIElement_Text::Create)`
|
||
(`docs/research/named-retail/acclient_2013_pseudo_c.txt:115655`). The
|
||
`Type==12 → return null` rule in `DatWidgetFactory` is a *vitals-only Plan-1
|
||
expedient* (AP-37: skip the vitals number elements so they render via
|
||
`UiMeter.Label`), **not** a structural truth.
|
||
|
||
So the "wrinkle" feared in the start-cold note (Type-0/12 elements hiding their
|
||
real widget type) **dissolves**: the resolved Type is already correct. The factory
|
||
just needs to *register* generic widgets for those Types instead of skipping them
|
||
or dropping to `UiDatElement`.
|
||
|
||
### 1.2 Why this matters beyond chat (the strategic purpose)
|
||
|
||
Chat is the **proving ground**, not the destination. The payoff is that every
|
||
future panel — **inventory, spell bar, vendor, character sheet, trade, skills** —
|
||
becomes *assembled from dat data + a thin controller* instead of being hand-built
|
||
from scratch. That is exactly how retail did it (`gm*UI::PostInit` everywhere on a
|
||
shared `UIElement` toolkit), and it is the reason to do this pass carefully now.
|
||
|
||
**What this pass gives all future windows (the foundation):**
|
||
- The **generic widget toolkit** — `UiButton`, `UiField`, `UiScrollbar`, `UiText`,
|
||
`UiMenu` — built automatically by `DatWidgetFactory` from the dat layout.
|
||
- The **thin-controller pattern** — find-widget-by-id → bind-live-data — proven and
|
||
cemented on chat. Inventory's controller, vendor's controller, etc. all take the
|
||
same shape.
|
||
|
||
**What those specific windows additionally need (out of scope here; cheap once the
|
||
pattern exists):**
|
||
- **A few more widget Types** — inventory/vendor lists want `UiListBox` (Type 5)
|
||
and `UiPanel` (Type 8); item slots want **drag-drop**, which retail builds into
|
||
`UIElement_Field` (the decomp shows `Field` has `CatchDroppedItem` /
|
||
`MouseOverTop` drag-drop hooks — so drag-drop rides on the Field widget this pass
|
||
already builds). Each gets *registered when that window needs it* — which is
|
||
exactly why §3 bounds "full registry" to the Types chat+vitals use today rather
|
||
than speculatively building all 14 retail classes.
|
||
- **The window manager** — open/close/z-order/persist, drag-bars (Type 2),
|
||
resize-grips (Type 9). This is the *other* half of Plan 2 — a sibling piece to
|
||
this one — and lands alongside, because pop-up/stackable windows (inventory,
|
||
vendor) need it.
|
||
- **Per-domain data plumbing** — item icons, live container contents, vendor stock
|
||
lists. Game-state work, separate from the UI toolkit.
|
||
|
||
This pass is therefore the **reusable toolkit + assembly pattern** that makes those
|
||
later windows mostly-free to build. It is the load-bearing first half of the road
|
||
to inventory/vendor/spell-bar, not the whole road.
|
||
|
||
---
|
||
|
||
## 2. Retail reference (the registry + the PostInit pattern)
|
||
|
||
### 2.1 The Type → class registry (`UIElement::RegisterElementClass`)
|
||
|
||
Confirmed verbatim from `acclient_2013_pseudo_c.txt` (line numbers cited):
|
||
|
||
| Type | Retail class | Reg. line | | Type | Retail class | Reg. line |
|
||
|---|---|---|---|---|---|---|
|
||
| 1 | `UIElement_Button` | :125828 | | 9 | `UIElement_Resizebar` | :118938 |
|
||
| 2 | `UIElement_Dragbar` | :119926 | | 0xb (11) | `UIElement_Scrollbar` | :124137 |
|
||
| 3 | `UIElement_Field` (editable) | :126190 | | 0xc (12) | `UIElement_Text` | :115655 |
|
||
| 5 | `UIElement_ListBox` | :121788 | | 0xd (13) | `UIElement_Viewport` | :119126 |
|
||
| 6 | `UIElement_Menu` | :120163 | | 0xe (14) | `UIElement_Browser` | :118718 |
|
||
| 7 | `UIElement_Meter` | :123316 | | 0x10/0x11 | `ColorPicker`/`GroupBox` | :118396/:118177 |
|
||
| 8 | `UIElement_Panel` | :119820 | | — | **Type 0 and 4: NOT registered** | — |
|
||
|
||
Type 0 has no class of its own — a Type-0 element is a placement/override that
|
||
inherits its class from its base. That is exactly what `ElementReader.Merge`
|
||
already does.
|
||
|
||
### 2.2 The `gm*UI::PostInit` binding pattern (the controller target)
|
||
|
||
`gmVitalsUI::PostInit` (`acclient_2013_pseudo_c.txt:199170-199228`) and
|
||
`gmMainChatUI::PostInit` (`:212585-212636`) do, per child widget:
|
||
|
||
```
|
||
UIElement* e = UIElement::GetChildRecursive(this, 0x100000e6); // find by id
|
||
UIElement_Meter* m = e->vtable->DynamicCast(7); // cast to Type
|
||
this->m_pHealthMeter = m; // store
|
||
if (!m) { /* skip */ } // null-check
|
||
```
|
||
|
||
acdream analogue (already half-present in `ChatWindowController`):
|
||
|
||
```csharp
|
||
var send = layout.FindElement(SendId) as UiButton; // GetChildRecursive + DynamicCast
|
||
if (send is not null) send.OnClick = () => input.Submit(); // bind behavior
|
||
```
|
||
|
||
The faithful end-state is: **the factory builds every widget from the dat; the
|
||
controller only finds-by-id and binds data/callbacks** — it never constructs a
|
||
widget.
|
||
|
||
### 2.3 Empirically resolved Types of the chat elements (`LayoutDesc 0x21000006`)
|
||
|
||
Traced against the live dat (HIGH confidence; base ids in parentheses):
|
||
|
||
| Element | Resolves to | Retail class | Today |
|
||
|---|---|---|---|
|
||
| `0x10000014` channel menu | **6** (own Type 6, no base) | Menu | `UiDatElement` → controller replaces w/ `UiChannelMenu` |
|
||
| `0x10000012` scrollbar track | **11** (base `0x10000367` in `0x2100003E`) | Scrollbar | `UiDatElement` → controller replaces w/ `UiChatScrollbar` |
|
||
| `0x10000011` transcript | **12** (base `0x10000372` in `0x2100003F`) | Text | skipped → controller adds `UiChatView` |
|
||
| `0x10000016` input | **12** (base `0x10000372` in `0x2100003F`) | Text | skipped → controller adds `UiChatInput` |
|
||
| `0x10000019` send | **1** (base chain → `0x1000047F` Type 1) | Button | `UiDatElement` + `OnClick` |
|
||
| `0x1000046F` max/min | **1** (base `0x1000047F` Type 1 in `0x21000040`) | Button | `UiDatElement` + `OnClick` |
|
||
|
||
> **Plan-phase verification #1 (load-bearing):** the editable **input**
|
||
> `0x10000016` traced to **Type 12 (Text)**, the same base as the read-only
|
||
> transcript — surprising for an editable field (retail's editable text is
|
||
> Field=3). Element ids are layout-*local*, so the decomp's `ChatInterface`
|
||
> Field-id does **not** cross-map; re-dump `0x10000016`'s exact resolved Type and
|
||
> the `0x10000372` base prototype's Type before relying on it. The design is
|
||
> robust either way — see §4.3(a).
|
||
|
||
---
|
||
|
||
## 3. Approved scope
|
||
|
||
**Decision (this session):** *Full registry, chat-first, vitals rewire as the
|
||
final, separately-committed, separately-gated step.*
|
||
|
||
**In scope:**
|
||
- Register generic widgets for the Types the chat + vitals windows actually use:
|
||
**Button (1), Field (3), Menu (6), Scrollbar (11), Text (12)** — plus Meter (7)
|
||
already done.
|
||
- Delete the `Type==12 → return null` skip; Type 12 becomes `UiText`.
|
||
- Collapse `ChatWindowController.Bind` to a find-by-id binder (no widget
|
||
construction).
|
||
- **Final gated step:** rewire `VitalsController` to bind generic `UiText` for the
|
||
vitals numbers (retail-faithful: vitals numbers *are* `UIElement_Text`),
|
||
retiring `UiMeter.Label` for vitals.
|
||
|
||
**Explicitly NOT in scope ("full registry" is bounded to what these windows use):**
|
||
- The long tail retail also registers — `Panel` (8), `Dragbar` (2), `Resizebar`
|
||
(9), `ListBox` (5), `Viewport` (13), `Browser` (14), `ColorPicker` (16),
|
||
`GroupBox` (17). Those elements **continue to render correctly as
|
||
`UiDatElement`** (the universal fallback is non-negotiable). No
|
||
`UIElement_ColorPicker` port for a window that has no color picker. When a future
|
||
window needs one of these, it gets registered then.
|
||
- No new chat *features* (tabs/squelch/name-tags/word-wrap remain as the chat
|
||
re-drive deferred them — see that spec's §2).
|
||
- `UiMeter.Label` is **not deleted** — it stays for plugin/markup panels; vitals
|
||
simply stops using it.
|
||
|
||
---
|
||
|
||
## 4. Design
|
||
|
||
### 4.1 `DatWidgetFactory` — the faithful Type switch
|
||
|
||
`DatWidgetFactory.Create` grows from `{7 → UiMeter, _ → UiDatElement}` to:
|
||
|
||
```csharp
|
||
UiElement e = info.Type switch
|
||
{
|
||
1 => BuildButton(info, resolve, datFont), // UIElement_Button
|
||
3 => BuildField(info, resolve, datFont), // UIElement_Field (see §4.3a)
|
||
6 => BuildMenu(info, resolve, datFont), // UIElement_Menu
|
||
7 => BuildMeter(info, resolve, datFont), // UIElement_Meter (unchanged)
|
||
11 => BuildScrollbar(info, resolve), // UIElement_Scrollbar
|
||
12 => BuildText(info, resolve, datFont), // UIElement_Text
|
||
_ => new UiDatElement(info, resolve), // generic fallback (unchanged)
|
||
};
|
||
```
|
||
|
||
The rect/anchor/z-order propagation at the bottom of `Create` is unchanged. The
|
||
`Type==12 && StateMedia.Count==0` skip is **removed** — but a *pure base
|
||
prototype* (Type 12 with no own geometry that is only referenced via
|
||
`BaseLayoutId`, never placed) must still not draw. In practice such prototypes are
|
||
never top-level placed elements in `0x21000006`/`0x2100006C`; the importer only
|
||
builds placed elements. **Plan-phase verification #2:** confirm no Type-12
|
||
prototype is double-built after the skip is removed (the chat/vitals golden
|
||
fixtures catch this).
|
||
|
||
Each `BuildX` extracts the widget's dat-derived data (sprite ids per state, label
|
||
font) the same way `BuildMeter` extracts its 3-slice grandchild sprites. The
|
||
controller binds providers/callbacks afterward.
|
||
|
||
### 4.2 The generic widgets
|
||
|
||
Each generic widget extends `UiElement`, is constructed by the factory from
|
||
`ElementInfo`, and exposes **data providers + callbacks** for the controller to
|
||
bind. The chat-specific knowledge moves *out* of the widgets and *into* the
|
||
controller (faithful: retail's `gmMainChatUI`, not `UIElement_Menu`, owns the
|
||
talk-focus channel list).
|
||
|
||
| Generic widget | Type | Derived from | Generic surface (dat-built + provider-bound) | Controller binds |
|
||
|---|---|---|---|---|
|
||
| `UiScrollbar` | 11 | `UiChatScrollbar` (already 100% generic) | track/thumb/cap/arrow sprite ids from dat; `Model : UiScrollable` | `Model = transcript.Scroll` |
|
||
| `UiButton` | 1 | `UiDatElement`+`OnClick` | state sprites (Normal/Pressed/Disabled), `Label`, `LabelFont`, `LabelColor`, `OnClick`, `NaturalWidth()` autosize | `OnClick`, caption |
|
||
| `UiMenu` | 6 | `UiChannelMenu` | popup toggle, 2-col layout, 8-piece bevel, row highlight; `Items : IReadOnlyList<(string label, bool enabled, object payload)>`, `OnSelect : Action<object>`, `Selected`, `NaturalButtonWidth()` | populate 14 channel `Items`; map payload↔`ChatChannelKind`; `AvailabilityProvider` |
|
||
| `UiText` | 12 | `UiChatView` | scrollable + selectable multi-color line list, clipboard, dat-font; `LinesProvider : Func<IReadOnlyList<(string,Vector4)>>`; shares `UiScrollable` (`Scroll`) | `LinesProvider` → ChatVM + per-kind colors |
|
||
| `UiField` | 3 | `UiChatInput` | editable one-line: caret/selection/clipboard/history/auto-repeat/focus-sprite-swap; `Text`, `OnSubmit`, `MaxCharacters` | `OnSubmit` → `ChatCommandRouter` |
|
||
|
||
**Placement.** The generic widgets live in `src/AcDream.App/UI/` alongside
|
||
`UiMeter` (toolkit widgets). The factory in `src/AcDream.App/UI/Layout/`
|
||
references them. This matches the current split (`UiMeter` in `UI/`,
|
||
`UiDatElement` in `UI/Layout/`).
|
||
|
||
**Naming.** `UiX` mirrors retail `UIElement_X`. The old chat-prefixed names are
|
||
removed (or kept as thin obsolete aliases only if needed mid-migration).
|
||
|
||
### 4.3 The two wrinkles
|
||
|
||
**(a) The editable input (Type 12 vs Type 3).** Robust to either resolution:
|
||
- If `0x10000016` resolves to **Type 3** → factory builds `UiField` directly; the
|
||
controller only binds `OnSubmit`.
|
||
- If it resolves to **Type 12** → the dat element is a display Text in this
|
||
layout; the controller *replaces* it with a controller-placed `UiField` at its
|
||
rect (today's pattern for the track/menu). `UiField` exists as a registered
|
||
generic widget regardless; only *who places it* differs.
|
||
|
||
Editing behavior (caret/clipboard/history) is never purely dat-derivable, so the
|
||
input is always provider-bound — the open question only affects whether the
|
||
factory or the controller *instantiates* it.
|
||
|
||
**(b) Vitals rewire — the final gated step.** Removing the Type-12 skip means the
|
||
vitals number elements (Type-0 → base Type-12 Text) *could* build as real
|
||
`UiText`. Today they are **meter children, consumed** (the importer does not
|
||
recurse a meter's children — `LayoutImporter.cs:113`), rendered via
|
||
`UiMeter.Label`. The faithful move: `VitalsController` constructs/binds a `UiText`
|
||
for each number (matching retail `UIElement_Text` vitals numbers) and drops
|
||
`UiMeter.Label` for vitals.
|
||
|
||
This is **step 7 — the last commit, separately gated**, with its own fixture
|
||
update and the user's visual sign-off, because vitals shipped pixel-identical and
|
||
is fixture-locked (`vitals_2100006C.json`). If the rewire risks the pixel-identical
|
||
result, we **stop and keep the meter-label path** for vitals — a smaller,
|
||
documented divergence (AP-37 narrowed, not retired). The decision to land step 7
|
||
is the user's, made on the running client.
|
||
|
||
### 4.4 The thin controller (after step 6)
|
||
|
||
`ChatWindowController.Bind` collapses to: for each known element id, `FindElement(id)
|
||
as UiX`, null-check, bind data/callback. The reflow/maximize/resize-bar-drop logic
|
||
(`ChatWindowController.cs:155-297`) stays — it is window-layout policy, not widget
|
||
construction. The `BuildLines` / `WrapText` / `RetailChatColor` helpers stay (chat
|
||
data shaping). What *leaves* the controller: the construction of `UiChatView`,
|
||
`UiChatInput`, `UiChatScrollbar`, `UiChannelMenu` (now factory-built) — the
|
||
controller binds them instead.
|
||
|
||
---
|
||
|
||
## 5. Migration sequence (one widget per commit; build + test green each step)
|
||
|
||
Ordered least-risk → most-risk; the chat window is fully generalized before vitals
|
||
is touched. Each step: `dotnet build` green, `dotnet test` (AcDream.App.Tests)
|
||
green, its own commit naming the widget; the live chat window stays visually
|
||
identical through steps 1–6.
|
||
|
||
1. **`UiScrollbar`** (Type 11) — promote `UiChatScrollbar` (already generic);
|
||
register; factory builds it; controller binds `Model`.
|
||
2. **`UiButton`** (Type 1) — extract from `UiDatElement`+`OnClick`; register; Send +
|
||
Max/Min build from the dat.
|
||
3. **`UiMenu`** (Type 6) — generalize `UiChannelMenu`; register; controller
|
||
populates channel `Items` + maps payload↔`ChatChannelKind`.
|
||
4. **`UiText`** (Type 12) — generalize `UiChatView`; register; **delete the Type-12
|
||
skip**; controller binds transcript lines. Guard: verify vitals still renders
|
||
(its numbers are meter-consumed → no auto-double-draw) via the vitals fixture +
|
||
a live launch.
|
||
5. **`UiField`** (Type 3) — generalize `UiChatInput`; register; wire the input per
|
||
§4.3(a) (verification #1 resolves factory-built vs controller-placed).
|
||
6. **Thin the controller** — collapse `ChatWindowController.Bind` to pure
|
||
find-by-id binding now that the factory builds everything.
|
||
7. **Vitals rewire (gated)** — `VitalsController` binds `UiText` numbers; fixture
|
||
update + the user's visual sign-off. **Stop-and-confirm gate.**
|
||
|
||
---
|
||
|
||
## 6. Testing & conformance
|
||
|
||
- **Generic-widget unit tests** (pure, no GL/dat) — mostly *moved* from the
|
||
existing chat-widget tests, renamed to the generic widgets: caret↔pixel + history
|
||
(`UiField`), thumb ratio / page-delta (`UiScrollbar` via `UiScrollable`), menu
|
||
item-pick + availability (`UiMenu`), line wrap / selection / dat-font hit-test
|
||
(`UiText`).
|
||
- **Factory tests** — `DatWidgetFactoryTests` grows one assert per newly registered
|
||
Type → correct widget class.
|
||
- **New chat-layout golden fixture** `tests/AcDream.App.Tests/UI/Layout/fixtures/chat_21000006.json`
|
||
(peer of `vitals_2100006C.json`): the resolved chat tree — each element's id,
|
||
rect, resolved Type, sprite ids — asserting the factory builds the right widget
|
||
per element. This locks the generalization.
|
||
- **Vitals fixture `vitals_2100006C.json` stays green, untouched, through steps
|
||
1–6**; updated only at step 7, with visual sign-off.
|
||
- **Visual acceptance** — the user launches `ACDREAM_RETAIL_UI=1` and confirms the
|
||
chat window is unchanged through steps 1–6, and the vitals window is unchanged
|
||
after step 7.
|
||
|
||
---
|
||
|
||
## 7. Divergence-register impact
|
||
|
||
- **AP-37** (`DatWidgetFactory.cs`/`LayoutImporter.cs`: Type-0 text skipped + meter-
|
||
collapse + vitals numbers via `UiMeter.Label`): **amended as steps land** — the
|
||
"standalone Type-0 text elements are skipped / a dedicated dat-text widget is
|
||
Plan 2" clause is retired when `UiText` ships (step 4); the vitals-numbers-via-
|
||
`UiMeter.Label` clause is retired at step 7, or **narrowed** (not retired) if
|
||
step 7 is deferred. The meter-collapse clause (reuse `UiMeter` 3-slice vs porting
|
||
`UIElement_Meter::DrawChildren` over nested dat elements) **remains** — this pass
|
||
does not port `DrawChildren`.
|
||
- **IA-15** (the importer *is* the retail-UI render path): unchanged; reinforced
|
||
(more Types now data-driven).
|
||
- **AP-41** (scrollbar thumb single stretched sprite): **re-check at step 1** — the
|
||
controller already passes 3-slice cap ids (`ThumbTopSprite`/`ThumbBotSprite`); the
|
||
row may be retire-able when `UiScrollbar` lands.
|
||
- **New rows** only if a generic widget introduces a *new* approximation (e.g., a
|
||
`UiMenu` item model simpler than retail's hierarchical popup chain in
|
||
`UIElement_Menu::MakePopup`). Add the row in the same commit per register rule 1.
|
||
|
||
---
|
||
|
||
## 8. Acceptance criteria
|
||
|
||
- [ ] `DatWidgetFactory` registers Types 1, 3, 6, 11, 12 (+ 7) → generic widgets;
|
||
`_` still falls back to `UiDatElement`.
|
||
- [ ] The `Type==12 → null` skip is removed; no Type-12 element is double-built
|
||
(golden fixtures green).
|
||
- [ ] The four chat widgets are generic (no `ChatChannelKind` / chat-color /
|
||
command-routing knowledge inside a widget); `ChatWindowController` only finds-
|
||
by-id and binds.
|
||
- [ ] Chat window is visually + behaviorally identical to the shipped version
|
||
through steps 1–6 (user-confirmed).
|
||
- [ ] New `chat_21000006.json` golden fixture + moved generic-widget unit tests;
|
||
all green.
|
||
- [ ] Vitals window unchanged after step 7 (user-confirmed), or step 7 deferred
|
||
with AP-37 narrowed.
|
||
- [ ] Every generic widget cites its retail `UIElement_X` class + reg. line in a
|
||
code comment.
|
||
- [ ] Divergence register updated (AP-37 amended; AP-41 re-checked) in the same
|
||
commits.
|
||
- [ ] Roadmap / `claude-memory/project_d2b_retail_ui.md` updated when the pass lands.
|
||
|
||
---
|
||
|
||
## 9. Open items for the plan phase
|
||
|
||
1. **Verification #1 (load-bearing):** re-dump `0x10000016` (input) + the
|
||
`0x10000372` base prototype to confirm input resolved Type (3 vs 12) → decides
|
||
factory-built vs controller-placed `UiField` (§4.3a).
|
||
2. **Verification #2:** confirm no Type-12 base prototype double-builds once the
|
||
skip is removed (§4.1).
|
||
3. Confirm the `UiMenu` generic item model (`(label, enabled, payload)`) is enough
|
||
for the 14 talk-focus channels without losing the greyed/available distinction
|
||
the chat menu currently shows.
|
||
4. Decide whether to keep thin obsolete-aliases for the old chat widget names
|
||
during migration or rename in-place (prefer in-place; the names are internal).
|