v0.104.0 — Timeline
Shipped 2026-09-21 — built on version/0.104.0, thirteen commits,
merged to main as 432d0ffc, tagged v0.104.0. The fourteenth of the
releases the near-term roadmap sequences from the
August 2026 heuristic evaluation; the roadmap page is
Timeline. Not the Time Machine, which is edit
history: where the map tracks where things are, the Timeline tracks when.
The help is The Timeline. No module release; Action
API 0.6.0.
Initiative was a client store relayed between whoever was connected —
persisted nowhere, so a reload asked the GM. Now initiative is the
map's, a TimedEffect runs on a map's Timeline from a tick for a
duration in rounds, indefinitely, or until a function says so (a save that
ends it, asked of the token acting), actions create and end effects through
api.timeline, and a vertical strip beside the chat draws it all —
ticks and rounds, the now-line, tokens at their initiative, effects as
coloured slices — with a drawer on the phone. Decisions TL-D1 (a
function's ask is asynchronous; only the asked token waits) and TL-D2
(indefinite effects survive initiative ending; counted ones freeze) at
their recommendations.
Initiative becomes state
maps.timeline(T.1) —{ active, entries, turn, round, movementRule }on both dialects (migration 0046), the model and its pure functions in@purple-cat/shared'stimeline/;GET/PATCH /api/maps/:id/timeline(a reader reads, an Editor writes), every write a map commit and atimelineviewstate announce.- The store is a cache (T.2) —
useTimelinereads the map,useInitiativeSyncapplies it to the store and writes a GM's changes back; the store'spersistand theinitiative:start / update / end / requestrelays are gone. A reload, a late joiner and a second GM tab read the map. - Ticks (T.3) — a tick is a round, an initiative and a tie index,
encoded as the spec's
round.initiative(3.14, ties in the next two places) and ordered bycompareTicks;advanceTimeline/endGroupTurnare the store's turn logic as pure functions;POST /timeline/advance(an Editor, or whoever may play the acting token; "turn advanced" / "round started" commits) is a player's End Turn, and the last relay but movement notes went with it.
TimedEffects
- The table and the routes (T.4) —
timed_effects(migration 0047): name, colour,startTick, a duration in rounds / indefinite / a function, the token it is on, how it ended; list / create / patch / end / delete under/api/maps/:id/effects, each a map commit (the effects are in the map's history set) and atimedEffectannounce. A newtimedEffectgrant entity inherits from the map with the creator as intrinsic Owner: a Collaborator starts, the one who started it or the GM changes and ends, a Viewer sees. - Expiry (T.5) — every tick (
writeTimelinesees it move) evaluates the running effects: counted ones by arithmetic; function durations in the script sandbox as(round, whoseTurn) => boolean, wherewhoseTurn.requestRoll(expr)is a pause, not anawait— the first call records the ask and delivers the v0.86.0 prompt through the same door a token action's does (services/rollRequests.ts, lifted out of the socket handler), and the answer runs the function again with the roll in hand. A throwing function leaves the effect running and files atimed-effect-failedMessage once. - Action API 0.6.0 (T.6) —
api.timeline.list / createEffect / editEffect / endEffectthrough a newmapContext.sendJson;urldefaults to the invoking action's document. Distinct colours (T.7) — the hue furthest from the running effects', behind the create default andGET /effects/next-color.
The strip
- Layout (T.8) —
layoutStore.timelineStrip(open/collapsed/auto: open while initiative runs or an effect exists, a 4px rail otherwise, until the user chooses), a fixed panel at the map's right edge left of the chat, the bottom bars clearing it. - The axis (T.9) — rows per initiative count with round headers, the now-line the scroll keeps at 15% and snaps back to on every tick, the recent past fading above it. Tokens (T.10) at their initiative in every round drawn, the current turn ringed; click selects and focuses, shift-click impersonates, the GM drags to reorder. Slices (T.11) stacking right-to-left, the hover card with what is left and the source, Edit / End now for the one who started it or the GM. The + (T.12) — the timed-effect dialogue (every field, click-to-set-start, rounds / until ended / until a function) and Add to initiative.
- Mobile (T.13) — the same axis as a drawer of the Map tab, taking Palette's slot while initiative runs (M-D1 revisited).
Finishing
- Buffs meet TimedEffects (T.14) —
BuffDurationgainstype: 'effect'witheffectId;buffDurationToEffect(rounds by the unit, concentration and save-ends as functions);api.timeline.startBuffso a buff an action creates gets its slice. The evaluator ofeffects[]is still the roadmap's. - Guards and help (T.15) —
maps.timelinewritten bywriteTimelinealone; the retired relays stay retired; the help page. - Verification — harness suites for the timeline, the advance, the effects, the tick (a sandboxed save-to-end asked and answered, a throw reported once), the palette and the Action API; contract tests for the strip's frame and axis, the mobile slot, the store as a cache; the full gate green on every commit. Verified live on the dev map: Roll Initiative persisted to the map and a reload read it back, Next turn moved the map's turn, an external PATCH ending initiative switched the open tab, and the strip drew three rounds, the now-line at Dorb's 18, the portraits, and Bless and Mage Armor as two lanes.
Traps met on the way
- The script sandbox is synchronous by design; a duration function cannot
await. The ask is a pause and a re-run, and the docs say so. versioned(entity, reason)reads a function reason after the handler:res.localsis how a route names a commit it decided during the request.- A new grant entity type touches seven places (the schema union, the loader, three spots in the permissions routes, the message subject union, the client's union) and a pin in the permissions editor's test.
- The Style Guide's no-forced-uppercase guard reads every CSS module; the
round labels lost their
text-transform. - The Chrome dev tab is hidden: Mantine menus and popovers never open there. The axis, the tokens and the slices were verified through the DOM; the menus and modals through their contracts.