Round 2 of adversarial review found a real bug in the fix the previous
round's own review prompted: mutating the live project.variables array (and
its row objects) in place before calling setVariables() meant projects.js's
remember() -- which deep-clones the project for the undo stack BEFORE
applying the new value -- captured the already-edited state as the "before"
snapshot. Undo after any variable edit or removal was a silent no-op.
Fixed by never mutating what project.variables already points at: every
commit now reads project.variables fresh and hands setVariables a brand new
array, which is also what closes the SAME stale-closure race the in-place
version was built to fix in the first place (redraw-mid-edit still reads
whatever the previous commit already saved, since it reads live rather than
from a snapshot taken at render time). Also fixed core/prompt_build.py's
docstring, which claimed the opposite of what resolve-variables-last
actually does and what its own test already pinned.
A trigger->replacement library (core/shortcuts.py, a close port of v4's
apply_prompt_shortcuts/resolve_variables), a Settings section to manage it,
autocomplete on the real prompt textarea (composer.autocomplete.default, so
it outranks the Constructor modal it lives inside), and a stateless
/api/prompt/expand that combines anchor + shortcuts + $variables + postfix
at the moment a run is queued -- scene.text itself stays exactly what was
typed. Project gained anchor/postfix/variables editing UI on its own tab.
Along the way: a real layout bug in the Properties column (every composer
stack can flex-shrink below its own content, and once the Project tab grew
past a couple of rows the column started squeezing its own children's
content into each other and, at the outer level, into the sibling Generation
section below) -- fixed with flex-shrink:0 at each level that needs it,
caught by a Playwright regression test.