fix(chat): round 4 — no user-visible meta text, real /help groups, indicator buttons toggle

Item 3 (#364): every honesty marker is now gone from user-visible /help
text. AllegianceOverview/HouseOverview's "[IMPLEMENTED]" tags and trailing
"Subcommands NOT marked..." sentences, and Day/Log/Render/Motd's appended
"NOT YET IMPLEMENTED in acdream" tails, are removed; the underlying retail
text is corrected/completed against the pseudo-C's own pristine
consolidated data dumps (Log and Motd had been silently truncated; Render
was entirely acdream-authored and is replaced with the real retail usage
string). The three PARTIAL /help group topics (channels/chatting/commands)
are now COMPLETE verbatim listings: HelpStupidChannelHack's three
"vtable slot" operands, previously believed undecodable, are the same
pooled/mislabeled-data artifact this campaign has hit before (AP-113's
precedent) — reading the function's own disassembly for the push imm32
preceding each constructor call resolves all three directly. messagetypes
is now a real ported construction (IsLegalChannel's 14-id whitelist +
LogTextTypeToString's name table + the exact join/wrap format) instead of
an acdream summary. Register row AP-184 retired.

Item 5: the main window's 1/2/3/4 indicator buttons now toggle their
floating chat window on click, per the user's retail memory overruling
the earlier decomp-only reading. UIElement_Button::HandleButtonClick has
its own generic click-driven action dispatch (property 0x12) reaching the
same DoVisibilityToggleAction the Alt+1..4 keybinds use; the button
fixture confirms this half is genuinely armed, but the floating-window
fixture authors no matching listener-registration property, so the
generic mechanism has no proven target in the data on hand. Per
CLAUDE.md, the user's retail memory is the axiom regardless:
ChatWindowController.BindIndicatorClicks wires each indicator's click
through the same ToggleFloatingChatWindow chokepoint the keybinds use,
as explicit user-directed retail behavior. SetIndicatorOpen stays the
sole writer of the Selected mirror so the visual stays consistent
through the click round trip.

Full reconciliation in docs/research/2026-08-09-chat-retail-window-shell.md
§1.4. Campaign plan gets the round-4 findings section; items 1+2
(text-style) are under parallel research, item 4 passed, item 6 deferred
to the settings track.

Suite: 12,579 passed / 4 skipped / 0 failed (Release, complete solution),
up from baseline 12,553/4/0 — net +26 tests, zero regressions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 18:46:34 +02:00
parent 8b166f3ea2
commit 5b54387b8e
10 changed files with 1129 additions and 239 deletions

View file

@ -382,32 +382,48 @@ user gate round 3; closed in the goal-window follow-up).
## #364 — Three `/help` group topics still partial: HelpStupidChannelHack unresolved
**Status:** OPEN — filed 2026-08-09, Campaign CH user-gate round 2, item 3.
**Status:** CLOSED 2026-08-10 — Campaign CH round 4. The blocker in the
original filing (below) was a wrong belief, not a real limitation:
`ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`'s three
"vtable slot" operands are the SAME pooled/mislabeled-data artifact this
file's register entry AP-113 already documented elsewhere — real DATA
pointers into `.rdata`, not vtable dispatch. Reading the function's own
disassembly for the `push imm32` immediately preceding each
`PStringBase::PStringBase` constructor call (instead of trusting Binary
Ninja's line-grouped rendering, which hides the true instruction order)
resolves all three operands directly: the function builds
`"@" + tag + " - Sends a broadcast to your " + ChannelName + ".\n"`, where
`tag` is one character sliced out of a shared wide literal `U"fvpca"`
(reading a WIDE string through a NARROW `char*` truncates at the first
zero high byte — the "hack" the function's own retail name calls out) and
`ChannelName` comes from `ChannelSystem::GetChannelName`'s own literal
switch table (also read directly: "Allegiance", "Co-vassals", "Monarch",
"Patron", "Vassals", "Fellowship"). `ChannelsGroupDetail`,
`ChattingGroupDetail` (whose "@reply" entry also needed
`HelpReply@0x00577A50`'s Summary-branch decoded — it unconditionally
concatenates reply+pr+mr, a genuine retail quirk ported as found), and
`CommandsGroupDetail` (`HelpAllGroup`, a straight-line concatenation of
every other group's Detail branch, including a CONFIRMED retail
saveui/loadui duplicate) are now COMPLETE verbatim listings, matching the
four (death/status/text/allegiances) already complete. Register row
AP-184 RETIRED with the full citation trail. The 5 remaining
`ByVerb`-only channel one-liners (fellowship/monarch/patron/vassals/
covassal, as standalone `/help f`-style lookups rather than group-listing
entries) are UNCHANGED — their own standalone help registration was never
confirmed independently of this mechanism, so they are deliberately left
as acdream summaries rather than spliced in speculatively.
**Original filing (2026-08-09, Campaign CH user-gate round 2, item 3):**
The user caught `/help death` printing an acdream meta-message instead of
retail's real listing; all 7 `ClientCommunicationSystem::HelpXxxGroup`
nodes were re-extracted verbatim from the PDB-paired binary via a
generalized `tools/pdb-extract/sweep_weenie_strings.py --ascii-only`.
4 of 7 (death/status/text/allegiances) are now COMPLETE verbatim listings
(`RetailCommandHelpTable.DeathGroupDetail` etc.). 3 remain PARTIAL
(`ChannelsGroupDetail`, `ChattingGroupDetail`, `CommandsGroupDetail`):
their detail text is built (in full or in part) by
`ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which
constructs its output from three string fragments Binary Ninja
mis-attributes to unrelated vtable slots (the classic BN pooled/
mislabeled-data artifact — `&ClientCommunicationSystem::\`vftable'.
RecvNotice_StartBarberNotice` etc. are NOT real vtable dispatch)
concatenated around a live `ChannelSystem::GetChannelName` call —
genuinely not decodable from a static string sweep. Each partial group
keeps its own summary line (fully verbatim) and an explicit UNVERIFIED
note citing the address; `chatting` additionally resolves 7 of its 8
detail entries (only the 5 HelpStupidChannelHack-delegated channel-alias
lines are missing). The same mechanism also blocks 5 of ~35 channel
one-liners in `ByVerb` (fellowship/monarch/patron/vassals/covassal
family), which stay acdream-authored summaries. Resolving
HelpStupidChannelHack requires reading the three mislabeled string
fragments directly out of `.rdata` by address (not by BN symbol name) and
confirming the channel-name substring logic — a focused follow-up, not
guessed.
4 of 7 (death/status/text/allegiances) were COMPLETE verbatim listings
(`RetailCommandHelpTable.DeathGroupDetail` etc.); 3 remained PARTIAL
(`ChannelsGroupDetail`, `ChattingGroupDetail`, `CommandsGroupDetail`) with
an explicit UNVERIFIED note, believed genuinely not decodable from a
static string sweep — see the CLOSED note above for why that turned out
to be wrong.
**Campaign:** `docs/plans/2026-08-09-chat-parity-campaign.md` (Campaign CH,
user gate round 2).

File diff suppressed because one or more lines are too long

View file

@ -48,11 +48,19 @@ CODE-COMPLETE 2026-08-10** under this session's hard constraints (no
subagents, no client launches) — see its ledger row and Slices bullet.
**CH6c (opacity) landed CODE-COMPLETE 2026-08-10**, same constraints —
see its ledger row and Slices bullet; the full three-sub-slice CH6 window
shell is now CODE-COMPLETE. Status stays CODE-COMPLETE pending the next
user gate round (still needed for CH6a's own visual confirmation, CH6b's
keybind/mirror/filter behavior, CH6c's focus-driven fade and Settings
slider, round 3's fixes, and a final in-client visual pass on everything
fixed so far).
shell is now CODE-COMPLETE. **User gate round 4 ran 2026-08-10 and found
six findings; see "User gate — round 4" below.** Items 3 (user-visible
`/help` meta-markers leaking into live output, closing #364) and 5 (the
main window's 1/2/3/4 indicator buttons now toggle their floating window
on click, per the user's retail memory) are fixed in this round's commit;
item 4 passed with no action needed; items 1+2 (text-style findings) were
under parallel read-only research this session, concurrent with this
commit's item 3+5 work; item 6 is deferred to the settings track. Status
stays CODE-COMPLETE pending the next user gate round (still needed for
CH6a's own visual confirmation, CH6b's keybind/mirror/filter behavior,
CH6c's focus-driven fade and Settings slider, round 3's fixes, round 4's
item 3+5 fixes, and a final in-client visual pass on everything fixed so
far).
**Why now:** first track of the alpha-release program (chat is the most
visible daily surface for the friend-alpha). User-directed 2026-08-09.
@ -289,7 +297,8 @@ implementer per slice against a pinned contract (per
| CH6b floating windows 14 | `22020ef2`, reworked `1aa77099` | 12,420 passed / 4 skipped / 0 failed | REJECT (docs/research/2026-08-10-ch6ab-review-findings.md) → reworked `1aa77099` — SHOULD-FIXES 2/3/4/5 + NITs 1-5 applied | pending — no client launches this session (hard constraint); needs the next connected round for keybind/mirror/filter visual confirmation, plus the new 0x2100005B fixture's resolved-type assumptions |
| CH6c opacity | `a819687c` | 12,459 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — BLOCKER (out-of-box `DefaultOpacity` 0.5→1.0), AP-190 reworded + two new decomp-verified clauses (retail's per-tick ease, retail's entry-field-specific focus predicate), NITs (`UiElement.cs` stale comment, `WindowUnregistered` detach, post-Dispose `Set*` guards, `DrawString`/outline-pass alpha tests) | pending — needs the next connected round for visual confirmation (window fade on focus change, Settings slider live-apply) |
| Goal-window #363/#367 interface-text seam | `09453eca` | 12,542 passed / 4 skipped / 0 failed | implementer-only, no subagent review this session (hard constraint) | pending — needs the next connected round to confirm the SpewBox now flashes for the reclassified refusals (see "Goal-window follow-up" below) |
| Consolidated review — retail `/help` Detail extraction, seam wiring test | this commit | 12,553 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — SHOULD-FIX 1 (42 of 47 catalog leaf verbs given retail Detail_HelpType text, 4 confirmed-null, 1 honest UNVERIFIED, retiring the class doc's overclaim), SHOULD-FIX 2 (`ComposedChatViewModelWiresOnInterfaceTextToSpewBox` — the a5a7eb4f defect class had no test), SHOULD-FIX 3 (AP-113 RETIRED — Lifestone's and Marketplace's own bespoke bad-args refusal text recovered byte-exact), SHOULD-FIX 4 (register header's stale 0.5/1.0 sentence corrected), NITs (a)-(d) (`HeadlessDiagnosticWriter` instead of `Console.WriteLine`, bounded non-quiescent-pump liveness diagnostic, hydration test doc-comment contradiction, 0x26 fallback dispatches on its own `Type`) | pending — needs the next connected round to confirm `/help <verb>` now shows retail's exact wording (see "Consolidated review" below) |
| Consolidated review — retail `/help` Detail extraction, seam wiring test | `f7a6f46b` | 12,553 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — SHOULD-FIX 1 (42 of 47 catalog leaf verbs given retail Detail_HelpType text, 4 confirmed-null, 1 honest UNVERIFIED, retiring the class doc's overclaim), SHOULD-FIX 2 (`ComposedChatViewModelWiresOnInterfaceTextToSpewBox` — the a5a7eb4f defect class had no test), SHOULD-FIX 3 (AP-113 RETIRED — Lifestone's and Marketplace's own bespoke bad-args refusal text recovered byte-exact), SHOULD-FIX 4 (register header's stale 0.5/1.0 sentence corrected), NITs (a)-(d) (`HeadlessDiagnosticWriter` instead of `Console.WriteLine`, bounded non-quiescent-pump liveness diagnostic, hydration test doc-comment contradiction, 0x26 fallback dispatches on its own `Type`) | pending — needs the next connected round to confirm `/help <verb>` now shows retail's exact wording (see "Consolidated review" below) |
| User gate round 4 — items 3+5 (no meta text, indicator click-toggle) | this commit | 12,579 passed / 4 skipped / 0 failed | implementer-only, no subagent review this session (hard constraint) | pending — needs the next connected round to confirm `/help channels`/`chatting`/`commands`/`messagetypes` show complete retail text and clicking each indicator button toggles its floating window (see "User gate — round 4" below) |
### CH4 closeout (2026-08-09)
@ -1071,3 +1080,32 @@ connected round should confirm `/help die`, `/help lifestone`,
show retail's exact wording instead of the old acdream summaries, and that
`/help index`/`/clist`/`/on`/`/off` now show "Unknown command" via the
SpewBox.
## User gate — round 4 (2026-08-10)
The user tested the goal-window build live and reported six findings. A
parallel read-only research agent investigated items 1+2 (text-style
presentation) concurrently with this session's items 3+5 fix work; this
session owned all builds/commits (hard constraint: one commit).
| # | User finding (condensed) | Disposition |
|---|---|---|
| 1 | Text-style/presentation finding under the on-screen interface-text and/or `/help` surfaces. | **Research in flight** — a parallel read-only research agent investigated this concurrently with items 3+5; not fixed in this commit. Follow up in the next session once its findings land. |
| 2 | A second text-style/presentation finding, same surface family as item 1. | **Research in flight** — same parallel agent as item 1. |
| 3 | User-visible meta-markers ("IMPLEMENTED", "acdream has not yet extracted…"-style notices) leaking into live `/help` output. | **FIXED this commit, closes ISSUES.md #364.** Every honesty marker is now gone from user-visible text: `AllegianceOverview`/`HouseOverview`'s `[IMPLEMENTED]` tags and trailing "Subcommands NOT marked…" sentences, and `Day`/`Log`/`Render`/`Motd`'s appended "NOT YET IMPLEMENTED in acdream" tails are all removed, with the underlying retail text corrected/completed against the pseudo-C's own pristine consolidated data dumps (`Log` and `Motd` had also been silently truncated; `Render` had been entirely acdream-authored and is replaced with the real retail usage string). The three PARTIAL `/help` group topics (channels/chatting/commands) are now COMPLETE verbatim listings — `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`'s three "vtable slot" operands, previously believed undecodable, are the same pooled/mislabeled-data artifact this campaign has hit before (AP-113's precedent); reading the function's own disassembly for the `push imm32` preceding each constructor call resolves all three directly. `messagetypes` is now a real ported construction (`LogTextTypeEnumMapper::IsLegalChannel`'s 14-id whitelist + `LogTextTypeToString`'s name table + the exact join/wrap format) instead of an acdream summary. Register row AP-184 RETIRED. See `RetailCommandHelpTable`'s class remarks and `RetailCommandHelpTableTests` (51 tests, all passing) for the full citation trail. |
| 4 | (Not detailed to this session — reported as passed.) | **User-passed**, no action needed this round. |
| 5 | The main window's 1/2/3/4 indicator buttons don't open/close the floating chat windows on click — the user's retail memory says clicking should toggle them. | **FIXED this commit.** CH6b's decomp grep (`gmMainChatUI::ListenToElementMessage @0x004CDA80` has no click case for these ids) was TRUE but incomplete — it never checked `UIElement_Button`'s own generic click handler. `UIElement_Button::HandleButtonClick @0x00471E50` reads an Enum property (`0x12`) off the button itself and, if present, routes through `ICIDM`'s action map to `UIElementManager::DoVisibilityToggleAction @0x0045B660` (the SAME function the `Alt+1..4` keybinds reach), which broadcasts element message `0x31` to every element registered as a listener for that action id (via property `0x24`, read once by `UIElement::Initialize`) — the receiving element's generic `UIElement::ListenToElementMessage` base-class handler then toggles its own visibility per its OWN property `0x58`. The committed fixture (`chat_2100006f.json`) confirms the button HALF is genuinely armed: all four indicators carry a real Enum-kind property `0x12` = `0x10000514`-`0x10000517`. But the floating-window fixture (`chat_floaty_2100005b.json`) authors NO matching property `0x24`/`0x58` anywhere, so nothing in the shipped DAT registers a floating chat window as that action's listener — the generic mechanism is real and armed on the button side but has no proven target in the data available to us. Per CLAUDE.md, the user's retail memory is the axiom regardless: `ChatWindowController.BindIndicatorClicks` (new) wires each indicator's click to the SAME `ToggleFloatingChatWindow(windowId)` chokepoint the keybinds use, explicitly as USER-DIRECTED retail behavior. `SetIndicatorOpen` stays the sole writer of the `Selected` mirror (`SuppressSelfToggle` stays `true`) so the visual stays consistent through the click round trip. Full reconciliation in `docs/research/2026-08-09-chat-retail-window-shell.md` §1.4 (the `@0x004CDA80` citation stays true as a statement about that one function; the CONCLUSION is corrected). New tests in `ChatLayoutConformanceTests.cs` exercise the click round trip and confirm keybind and click drive the same chokepoint. |
| 6 | A settings-surface finding. | **Deferred to the settings track** — out of this commit's scope; no code change this round. |
Suite: 12,579 passed / 4 skipped / 0 failed (Release, complete solution —
`AcDream.slnx`), up from the goal-window baseline 12,553/4/0 — net +26
tests, zero regressions. Evidence: this commit's diff +
`tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs`
(51 tests) + `tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs`
(new/updated indicator-button tests). No subagent review this session (hard
constraint); no connected user gate for this commit's own changes (hard
constraint — no client launches) — the next connected round should confirm
`/help channels`/`/help chatting`/`/help commands`/`/help messagetypes` now
show complete retail text with no meta-notice, and that clicking each of the
four chat-window indicator buttons now opens/closes its floating window
while the button's lit/unlit state stays correct.

View file

@ -184,19 +184,123 @@ exactly two element ids: `0x1000046f` (max/min, dispatching
(`idMessage == 7`, checked against `this->m_pCCS` / a `0x1000000b` attribute
read). There is no case, anywhere in that function or its base-class fallback
(`ChatInterface::ListenToElementMessage`, called unconditionally at the
function's tail), for `0x10000522``0x10000525`. **Clicking a chat-window
indicator button does NOTHING in retail — reading (a), pure indicator, is
correct; reading (b) is refuted.** The `0x24` input-action-property theory
in reading (b) does not even apply to a mouse click on the button itself: that
property only wires *keyboard* dispatch (`UIElementManager::
DoVisibilityToggleAction` in §1.3), not a button's own `UIElement` click
message, which routes to its LISTENING PARENT — and that parent's handler has
no case for these four ids. acdream ports this exactly: the four indicator
buttons (`ChatWindowController._indicatorButtons`) carry no `OnClick` at all;
`ChatWindowController.SetIndicatorOpen` is their only writer, called only from
`RetailUiRuntime.OnWindowVisibilityChanged` in response to the floating
window's own visibility changing (keybind or otherwise) — a pure one-directional
mirror, matching retail exactly.
function's tail), for `0x10000522``0x10000525`. **This citation is still a
true statement about `gmMainChatUI::ListenToElementMessage` — see the round-4
correction below for why it is the wrong function to have grepped.**
---
**ROUND-4 CORRECTION (2026-08-10) — the user's own retail memory ("clicking
opens/closes the window") overruled the CONCLUSION above, and re-attacking the
question with that as the starting axiom (per CLAUDE.md: the user's retail
memory is the axiom, not a hypothesis to be argued down) found the exact
mechanism the CH6b pass missed.**
The CH6b pass's mistake was scope, not accuracy: it proved
`gmMainChatUI::ListenToElementMessage` has no click case for these ids, then
reasoned "a button's click message… routes to its LISTENING PARENT" and
stopped there. **That's wrong — `UIElement_Button` (Type 1, the class every
one of these four indicators actually is) overrides its OWN click handling
and never asks its parent window first:**
```
UIElement_Button::HandleButtonClick @0x00471E50
00471e65 if (UIElement::GetAttribute_Enum(this, 0x12, &actionId)) // reads its OWN property
00471e72 if (actionId != 1)
00471e95 build an InputEvent(actionId)
00471eb2 ICIDM::GetActionMap()… dispatch through the action map
```
Property `0x12` here is the SAME kind of "input action" enum as `0x24` — not
the parent-window dispatch §1.4's original text assumed didn't apply to
clicks. This IS the generic mechanism a click uses to reach
`UIElementManager::DoVisibilityToggleAction @0x0045B660` (§1.3's own citation,
previously assumed keybind-only) → `BroadcastElementMessage(target, 0x31,
actionId, 0)` for every element registered under that action id via
`RegisterElementForInputAction` (§1.3's property-`0x24` registration —
confirmed as the ONLY call site of `RegisterElementForInputAction` in the
whole binary) → the RECEIVING element's generic base-class handler:
```
UIElement::ListenToElementMessage @0x00462340
00462447 case 8: // idMessage - 0x29 == 8, i.e. raw idMessage 0x31
0046244f GetAttribute_Enum(this, 0x58, &mode) // reads the RECEIVER's OWN property
00462459 if (mode == 1) SetVisible(!currentlyVisible) // toggle
0046245c else if (mode == 2) SetVisible(1) // force-show
0046245f else if (mode == 3) SetVisible(0) // force-hide
```
Neither `gmMainChatUI`, `gmFloatyChatUI`, nor `ChatInterface` overrides
`ListenToElementMessage` for raw idMessage `0x31` (confirmed by reading all
three switches directly — none has a case landing on it), so EVERY window
falls through to this base-class handler unconditionally. This is a complete,
generic, working "click toggles a registered listener's visibility" system —
exactly the "authored button behavior" this reconciliation task hypothesized
— and it is NOT gated on keyboard input the way the original §1.3 text
assumed; `UIElementManager::KeyPressEvent`'s call to `DoVisibilityToggleAction`
is simply ONE caller among the several that can reach it (the button's own
`HandleButtonClick` is another).
**What the authored DATA shows, checked directly against the committed
fixtures:**
- `chat_2100006f.json` — all four indicator elements (`0x10000522`-`0x10000525`)
DO author an Enum-kind property `0x12` (confirmed by `Kind: 0` = Enum in the
fixture's own property dump, matching `LayoutImporter.ConvertProperty`'s
`EnumBaseProperty → UiPropertyKind.Enum` mapping exactly), with values
`0x10000514`-`0x10000517` in element-id order. This is a real, present,
correctly-typed action id — the button-click half of the generic mechanism
is genuinely armed.
- `chat_floaty_2100005b.json` — the floating chat window's own fixture
authors **NO Enum-kind property `0x24` anywhere** (its only hit on property
number 36 decimal is `Kind: 4` = Integer, an unrelated attribute — not the
registration property) and **no property `0x58` at all**. Since
`RegisterElementForInputAction` has exactly one call site in the entire
binary (the property-`0x24` handler in `UIElement::Initialize`; no class
anywhere calls it directly from code), nothing in the shipped floating chat
window LayoutDesc ever registers it as a listener for ANY action id.
`DoVisibilityToggleAction` would look up the button's action id, find zero
registered listeners, and silently return — the click would fire a real
message with no receiver.
- The four action-id VALUES (`0x10000514`-`0x10000517`) are also not
chat-specific: the identical four numbers, in the identical order, are
`gmFriendsUI::PostInit`'s own child-element ids for its Add/Remove/Tell
buttons and friends listbox (`UIElement::GetChildRecursive(this,
0x10000514)` etc., confirmed by direct read). This is almost certainly a
coincidence of Turbine's global per-dat-file asset-id allocator (ids are
assigned client-wide, not scoped per panel), not a cross-reference —
nothing in `gmFriendsUI` registers for input actions either.
**Conclusion: the generic UI action system is real, it exists, and the
buttons genuinely arm their half of it — but the authored DATA available to
us (both committed fixtures, generated from the installed DAT) does not wire
a target for it.** This is consistent with, not a refutation of, the original
CH6b grep of `gmMainChatUI::ListenToElementMessage` — that citation was
looking in the wrong function, but its NEGATIVE RESULT (retail's window-level
message handlers never claim these clicks) still holds; the generic
mechanism, if it does connect the dots in real retail, does so entirely
below the level either grep could see. **Per CLAUDE.md, the user's retail
memory is the axiom regardless: acdream now wires each indicator's click to
toggle its floating window through the SAME `ToggleFloatingChatWindow`
chokepoint the `Alt+1..4` keybinds use
(`ChatWindowController.BindIndicatorClicks`, called by `RetailUiRuntime`
right after mounting the main chat window) — explicitly as USER-DIRECTED
retail behavior, not a claim that the generic-action-system data path has
been proven end-to-end.** `SetIndicatorOpen` stays the ONLY writer of the
indicator's `Selected` mirror (`UiButton.SuppressSelfToggle` stays `true`);
the click drives the real toggle, and the mirror reports the outcome back —
so the visual stays consistent through the round trip even though the write
is now two-way at the FEATURE level.
---
acdream ports this exactly: the four indicator buttons
(`ChatWindowController._indicatorButtons`) now carry a real `OnClick`
(`ChatWindowController.BindIndicatorClicks`, round 4); the mirror half is
unchanged — `ChatWindowController.SetIndicatorOpen` is still the ONLY writer
of `Selected`, still called from `RetailUiRuntime.OnWindowVisibilityChanged`
in response to the floating window's own visibility changing, regardless of
what triggered it (click, keybind, or a restored layout).
### 1.5 Closing a floaty window from its own title bar