Add per-character queue bindings with auto-start - #28
Merged
Conversation
Bind a queue to a character card; opening that character's chat starts the
queue and switches the connection profile before the first generation. This
was the "per-character default queues" item from the future-work list.
## Storage
extension_settings.roulette.characterQueues = { [avatarFilename]: queueId }
Characters are keyed by AVATAR FILENAME, never by this_chid. this_chid is an
index into the live characters array and shifts whenever a character is added,
deleted, or the list is re-sorted — persisting it would silently re-point
bindings at the wrong character. The avatar filename is what ST core and every
bundled extension (quick-reply, gallery, attachments, stats) uses for
per-character storage.
Group chats are excluded. getCurrentCharacter() returns null while a group is
selected, making every binding path inert there. Matches quick-reply's
per-character config, which bails on selected_group for the same reason: a
group has several members and no non-arbitrary answer to whose binding wins.
## Activation rules
The precedence logic is a pure function — decideAutoActivation() in
rotation.js — so it is testable without ST:
no character / no binding -> none
autoBindHandled already set -> none
chat already has a rotation -> mark-handled (never clobber it)
bound queue no longer exists -> clear-binding
otherwise -> start
The autoBindHandled latch (per-chat, in chat_metadata) is what makes
auto-start tolerable: stopRotation() sets it, so a rotation the user
deliberately stopped does not resurrect every time the chat is reopened.
reevaluateAutoActivation() clears it on purpose when a binding changes from
the UI, so binding a character while sitting in their chat takes effect
immediately rather than appearing to do nothing.
Ordering matters: the running-rotation check precedes the stale-queue check,
so we never mutate settings while a rotation the user cares about is in
flight.
## Events
CHAT_CHANGED -> maybe auto-start (chat_metadata is bound before this
event emits: script.js:7598 vs :7641)
APP_READY -> boot-time safety net. eventSource's autoFireAfterEmit
set covers APP_READY, so a late-registering listener
still fires. Not strictly needed — firstLoadInit()
runs initExtensions() before getCharacters() — but
free, and idempotent via the latch.
CHARACTER_DELETED -> purge the binding ({ id, character } payload)
CHARACTER_RENAMED -> follow it to the new avatar key (oldAvatar, newAvatar)
deleteQueue() prunes bindings pointing at the removed queue, inline in
state.js: routing it through characterBinding.js would form an import cycle.
## UI
Two surfaces over one map:
- Chamber tab gains a contextual "Auto-start for <character>" select, hidden
entirely in group chats. Uses implicit label association rather than a
fixed element id, so a fast close/reopen of the modal can't transiently
duplicate it.
- Queue cards gain a masks icon opening a searchable character multi-select
(src/ui/bindingPicker.js), plus a count chip. A character has at most one
queue, so the picker flags characters already bound elsewhere instead of
silently rebinding them. Rows are built with textContent — character names
are user-supplied.
New CSS is placed before the media queries, not appended, so the responsive
overrides still win on source order.
## Slash commands
/roulette-bind <queueName> bind the current character
/roulette-unbind remove the current character's binding
Both refuse in group chats with an explanatory toast.
## Tests
First automated coverage in the repo. src/rotation.js imports nothing, so
`npm test` runs it under plain node with no browser, no ST, and no mocks.
Twelve tests over slot sequencing, the noRepeatInRow guarantee, weighted
distribution, the generation-type filter, and the new precedence rules.
Verified non-vacuous by mutation: swapping the precedence order and disabling
the latch each fail exactly one test.
Docs: CLAUDE.md was stale at v1.0.0 — refreshed the version, repo tree
(sampling.js and widget.js were undocumented), hook points, verified ST
internals, and scope lists. README gains a Per-Character Queues section and a
corrected version table (it linked to a CHANGELOG.md that does not exist).
TESTING.md gains criteria 16-20. Manifest 1.2.0 -> 1.3.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5wRPLqtW32eqWY9hi9uFt
The suite added in the previous commit only ran if someone remembered to type `npm test` locally, which meant a broken scheduling core could reach main unnoticed. This wires it to CI so it runs on its own. Triggers on every pull request and on pushes to main. Single Node 22 runner, matching the version the suite was developed against. No install step: the extension declares no dependencies and `node --test` is built into Node, so there is nothing to fetch and no lockfile to cache. Verified against a clean checkout with no node_modules — 12/12 pass. `permissions: contents: read` keeps the job to the minimum it needs; it only ever reads the repository. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5wRPLqtW32eqWY9hi9uFt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements character-binding auto-start: users can bind a queue to a character so that opening that character's chat automatically starts the rotation. Bindings are keyed by avatar filename (ST's stable character identifier) and persist across chat reloads.
Summary
This release adds a new feature layer that lets users configure "when this character loads, run this queue automatically." The binding system is designed to stay out of the user's way: a deliberately stopped rotation won't restart on chat reload, a manually-started different queue won't be clobbered, and group chats are excluded (they have multiple members with no obvious winner).
Key Changes
Core scheduling logic (
src/rotation.js):decideAutoActivation()pure function that encodes the precedence rules for auto-start (no character context → nothing; already settled → nothing; rotation running → mark handled; stale binding → clear it; otherwise → start). Fully tested intests/rotation.test.mjs.emptyState()to includeautoBindHandledflag, which latches the auto-start decision per chat so a stopped rotation stays stopped across reloads.Character binding storage and queries (
src/characterBinding.js, new):getCurrentCharacter(),listCharacters(),getBoundQueueId(),setBinding(),clearBinding(),charactersBoundTo(),bindingCountForQueue(),purgeBindingsForCharacter(),remapBinding().Event wiring (
src/events.js):maybeAutoActivateForCharacter()to evaluate and apply auto-start on chat load (called from CHAT_CHANGED and APP_READY handlers).reevaluateAutoActivation()to re-open the auto-start question when the user changes a binding from the UI (so binding a character while sitting in their chat applies immediately, not on next reload).UI surfaces:
src/ui/tabs/chamber.js): Added a contextual row showing "Auto-start for [character]" with a dropdown to pick or clear a queue. Updates on character change and refreshes when bindings change.src/ui/bindingPicker.js, new): Multi-select modal opened from queue cards in the Queues tab. Shows all characters (including orphaned ones whose character was deleted), highlights conflicts (character already bound elsewhere), and persists changes only on save.src/ui/tabs/queues.js): Added a masks icon button on queue cards that opens the binding picker. Queue cards show a chip with binding count.Slash commands (
src/slashCommands.js):/roulette-bind <queueName>— bind the current character to a queue./roulette-unbind— remove the current character's binding.State (
src/state.js):characterQueues: {}to default settings, storing{ [avatarFilename]: queueId }.Styling (
style.css):Testing (
tests/rotation.test.mjs, new):Documentation:
https://claude.ai/code/session_01J5wRPLqtW32eqWY9hi9uFt