v0.104.4 — Token stacks
Shipped 2026-09-21 — built on version/0.104.4, six commits, merged to
main as 99d9b244, tagged v0.104.4. Tokens that share a cell:
telling them apart, pulling them apart, and never making a stack by
accident. From a play session's worth of pasted bugbears.
The warning
"Encountered two children with the same key, token_…" — createToken
appended the REST response to the token cache, while the token:created
broadcast (which the creator also receives) had already added the same
token through a handler that did de-duplicate. Broadcast first, then
response: the token twice, and the token layer warned on every render until
the next refetch. upsertCreatedToken replaces by id.
Stacks
A stack is two or more tokens on the token layer at the same rounded cell with the same footprint. A rider is never a stack member: it shares its mount's cell on purpose, and the two move as one.
- Hover a stacked token and the action buttons give way to N tokens in this location — Select one (a list, eight rows before it scrolls; a name impersonates) and Separate all.
- Separate all —
separateTokens: each mover in turn takes the nearest free position to where it is (by distance, ties clockwise from north), free meaning no cell of its footprint is one any other token on the map holds, stacked or not, plus the movers already placed. The first stays when its cell is free. Terrain layers do not occupy. Applied through the batch move, so initiative's movement rule and mount groups apply as a drag would. - The multi-select panel lists the selection as singles and stacks
(
stack:with members indented), a select (impersonate, leaving the selection) and a delete (the rest stay selected) on every token row, Separate all on every stack, and "…and N more" as an expando.
Never a stack by accident
- Paste (⌘V, and the menu's Paste) spreads its copies from the cursor with the same rule — the first lands there, the rest around it.
- Duplicate (the menu's, and the new ⌘D / Ctrl-D) builds the copies at their sources and spreads them; one copy becomes the current token, several the selection. A modified WASD key no longer pans the camera.
- The validation failure —
actions: expected string, received undefined, once per action — was the list's action stubs (§6.5.C, nocode) sent to a create route that requires it.hydrateTokensForCreatefetches the full token first, on the copy shortcut, the menu's Copy, Duplicate, Copy/Move to map and both context panels' transfers.
Verified live on the dev map: a three-token stack showed the panel, Select one impersonated, Separate all left the hovered token and moved the other two; the stubbed Harlowe reproduced the 400 with thirteen field errors and her hydrated copy created with all thirteen; ⌘D on the stack made three copies on three distinct free cells with their actions. The multi-select panel's rendering rests on its contract test (the hidden Chrome tab draws no Mantine modal).