memory.md: record 'never free widget from own signal callback' pattern

This commit is contained in:
megaproxy 2026-05-16 00:35:59 +01:00
parent 4e09dea03a
commit da55bf312c

View file

@ -303,6 +303,7 @@ Same scope as locked in `~/claude/ideas/rimlike/plan.md`. Realistic timeline 3
- **Workbench Bill Editor shipped same day** (commit `bdd4352`). Tap a workbench → right-side panel with bill rows (mode toggle, target spin, pause, remove) + Add-bill popup filtered by `accepted_skill`. Mirrors `PawnDetailPanel` exactly: layer 18, right-anchored 360 px, procedural `_build_ui()`, sim_tick refresh of the current-bill status only (bill list rebuilt on add/remove/select to preserve scroll). Selection chain extended in `selection.gd`: pawn-first then workbench, mutual-exclusion via new `EventBus.workbench_selected/deselected` signals. Closes Phase 17 deferred item. Code-level verification: headless boot clean, no runtime errors. Visual MCP verification still pending (need editor running).
- **Delegation report — bill UI sprint.** Steps 1, 3, 4, 6 (~5 mechanical edits across 5 files) → `quick-edit` (Haiku, 1 dispatch). Step 5 (new ~432-LOC WorkbenchPanel script needing full mirror of PawnDetailPanel + 6 source files) → `gdscript-refactor` (Sonnet, 1 dispatch). Steps 2, 7 (selection chain + main mount, needed design judgment about mutual-exclusion and main.gd's typed-var pattern) handled on Opus. Strings table follow-up (`ui.bill.until_count` key missed by Sonnet, fallback was a hardcoded literal) caught and fixed on Opus before commit.
- **Pattern recorded — "main.gd typed-var pattern requires CanvasLayer.new() + set_script(), not SCRIPT.new()".** First mount attempt used `WORKBENCH_PANEL_SCRIPT.new()` — Godot 4's parser refused with "Cannot infer the type" because `Script.new()` returns generic `Object`. Switched to `var x := CanvasLayer.new(); x.set_script(SCRIPT)` matching the rest of main.gd. Cheap parse error to surface via `--headless --quit`, but worth noting: subagents writing UI mount glue need this idiom explicitly.
- **Pattern recorded — "never free a widget from within its own signal callback".** Bill editor crashed when user changed mode FOREVER → UNTIL_N. Root cause: OptionButton.item_selected lambda called `_populate_bills()` directly, which clears + rebuilds all bill rows — freeing the very OptionButton whose signal was still emitting. Same pattern in the Remove button. Fix: `call_deferred("_populate_bills")` so the rebuild runs on the next idle frame after the signal frame completes. Commit `4e09dea`. Applies to any UI where a child Control's signal handler mutates a parent container — always defer rebuilds.
## External references