Mark commands
Tell every mark of one agent what it is doing, from anywhere on the page.
Two registries, one provider
MarkStageProvider already keeps an appearance-keyed registry: how many engines run, and which marks mirror which. The command registry is its agent-keyed half. It records what each agent is doing — the thing an application actually has an opinion about — and every mark subscribed to that agent follows. A roster row, a chat header and a hover card can all talk about the same agent from different corners of the tree without lifting anything into shared React state, and without the re-render storm that would come with it.
useMarkCommands() returns the write side. It works without a provider too: the fallback registry simply has no subscribers other than the marks in the same tree.
Facts and holds
Two kinds of write, with different weight:
holdFacts(agentId, facts)publishes what is known —running,composing,activity. The state is derived from the facts at read time bycharacterStateForRoster, so the same facts meancomposingin a chat andidlein an avatar. A second call replaces the first — facts are compared by value, so an identical object from a re-render notifies nobody. Returns a release function that only clears the facts it set;releaseFactsdrops whatever is there.holdState(agentId, state, { priority, ttlMs })pins a state over whatever the facts would say. The highest priority wins, ties go to the most recent hold, and attlMsdrops the hold on its own — the right shape for a celebration that must not outstay its welcome. Returns a release function;releaseStatedrops every hold on the agent.
Reads go through useMarkCommand(agentId, surface), which subscribes one component to one agent and returns a MarkCommandSnapshot: the resolved state, the winning held state (or null when the state came from facts), the raw facts, the emphasis flag and a pokeSeq counter that increments on every poke so a host reacts to the change rather than the value.
Hooks that keep motion honest
Three small hooks the components use, exported for hosts building their own.
useMarkSettle(state, { settleMs })holds the last state until the input has stopped changing forsettleMs(SETTLE_MS, 1400 ms by default;0disables). Facts churn — a tool starts, returns in 80 ms, another starts — and rendering each one is a roster of faces flicking, which reads as a fault.usePokeCycle(ref)returns a function that runs the next flourish on the handle each call: spin, hop, confetti, spin again. Always spinning stops being fun on the second click. Pokes insidePOKE_COOLDOWN_MS(340 ms) are ignored, and the function returnsfalsewhen the ref has no engine — a mirroredStagedMarkshares its animation and cannot move alone, so the caller can hop the wrapper instead.useDeadline(ms, deps)isfalseuntilmshas passed since the dependencies last changed, thentrue. It saves an arrival animation its ownsetTimeoutand ref.
API
useMarkCommands
Returns MarkCommands:
useMarkCommand
useMarkCommand(agentId, surface = 'list') — subscribe to one agent and get its MarkCommandSnapshot.
useMarkSettle, usePokeCycle, useDeadline
See above. createMarkCommandRegistry() builds a standalone registry for tests or for a host that provides MarkCommandContext itself.
Next
- MarkStageProvider — share engines by appearance
- Desktop companion — alive for hours without being noisy