Skip to content

Add per-character queue bindings with auto-start - #28

Merged
hyperion-001 merged 2 commits into
mainfrom
claude/st-roulette-new-feature-e5mmu7
Aug 18, 2026
Merged

Add per-character queue bindings with auto-start#28
hyperion-001 merged 2 commits into
mainfrom
claude/st-roulette-new-feature-e5mmu7

Conversation

@hyperion-001

Copy link
Copy Markdown
Contributor

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):

  • Added 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 in tests/rotation.test.mjs.
  • Extended emptyState() to include autoBindHandled flag, 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):

  • Centralized module for all character-identity handling, keyed by avatar filename (matching ST core and bundled extensions like quick-reply).
  • Exports: getCurrentCharacter(), listCharacters(), getBoundQueueId(), setBinding(), clearBinding(), charactersBoundTo(), bindingCountForQueue(), purgeBindingsForCharacter(), remapBinding().
  • Handles lifecycle: CHARACTER_DELETED and CHARACTER_RENAMED events are wired to keep bindings from rotting when characters are deleted or renamed.

Event wiring (src/events.js):

  • Added maybeAutoActivateForCharacter() to evaluate and apply auto-start on chat load (called from CHAT_CHANGED and APP_READY handlers).
  • Added 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).
  • Wired CHARACTER_DELETED and CHARACTER_RENAMED to keep the binding map in sync.

UI surfaces:

  • Chamber tab (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.
  • Binding picker (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.
  • Queues tab (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):

  • Added characterQueues: {} to default settings, storing { [avatarFilename]: queueId }.

Styling (style.css):

  • Added styles for the binding row, dropdown, picker modal, and queue-card chip.

Testing (tests/rotation.test.mjs, new):

  • 197 lines of unit tests covering slot sequencing, no-repeat guarantee, and auto-activation precedence rules. Uses a deterministic PRNG so weighted-random assertions don't flake.

Documentation:

  • Updated CLAUDE.md with binding lifecycle, event wiring, and state storage notes.
  • Updated README.md with binding UI and rules table.
  • Updated TESTING.md with four new acceptance criteria (auto-start, no clobber/resurrect, binding lifecycle, group chat exclusion).
  • Updated manifest.json and package.json to version 1.3.0.

https://claude.ai/code/session_01J5wRPLqtW32eqWY9hi9uFt

claude added 2 commits August 18, 2026 15:10
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
@hyperion-001
hyperion-001 merged commit bca2576 into main Aug 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants