Process-wide feature-toggle registry.
Replaces the two parallel toggle plumbings we had drifting apart:
:show-thinking, :show-iterations,
:show-silent) wired into state/default-settings,if (some-flag …) … else … checks scattered through
internal/render.clj and channel-tui's render layer.A toggle has stable metadata (id, label, description, default,
owner) and a current ON/OFF value. Anyone — internal modules,
extensions, channels — registers their toggles into the same
registry; any caller flips a toggle through the same set!. The
TUI settings dialog walks the registry to render the list, so
adding a new toggle from an extension shows up in the user's UI
without any TUI patch.
Persistence is opt-in: register-toggle! accepts :persist? true
and on set! the wrapper writes {:toggles {id value}} into the
machine store ~/.vis/state.yml via vis.config/save-config!.
Hand-authored vis.yml / config.yml may ALSO declare a toggles:
block. Ids are plain snake_case strings there (reasoning_level: deep),
identical to the registered id. coerce-config-value maps YAML strings
onto each toggle's type, so a config-declared toggle behaves exactly like
a UI flip. Hydration happens at process start (call
hydrate-from-config! once after config/load-config-raw).
Contract:
reasoning_level,
shell, ...) — no keywords, namespaces, slashes, or kebab-case.
YAML config uses the same string verbatim (reasoning_level: deep).enabled? is cheap (single atom deref + string lookup), called
per-paint per-row by the render layer; do not turn it into a
function-call indirection.state is left alone so a
user override survives a reload.Process-wide feature-toggle registry.
Replaces the two parallel toggle plumbings we had drifting apart:
- hard-coded TUI booleans (`:show-thinking`, `:show-iterations`,
`:show-silent`) wired into `state/default-settings`,
- per-render `if (some-flag …) … else …` checks scattered through
`internal/render.clj` and channel-tui's render layer.
A toggle has stable metadata (`id`, label, description, default,
owner) and a current ON/OFF value. Anyone — internal modules,
extensions, channels — registers their toggles into the same
registry; any caller flips a toggle through the same `set!`. The
TUI settings dialog walks the registry to render the list, so
adding a new toggle from an extension shows up in the user's UI
without any TUI patch.
Persistence is opt-in: `register-toggle!` accepts `:persist? true`
and on `set!` the wrapper writes `{:toggles {id value}}` into the
machine store `~/.vis/state.yml` via `vis.config/save-config!`.
Hand-authored `vis.yml` / `config.yml` may ALSO declare a `toggles:`
block. Ids are plain snake_case strings there (`reasoning_level: deep`),
identical to the registered id. `coerce-config-value` maps YAML strings
onto each toggle's type, so a config-declared toggle behaves exactly like
a UI flip. Hydration happens at process start (call
`hydrate-from-config!` once after `config/load-config-raw`).
Contract:
- Toggle ids are non-blank snake_case strings (`reasoning_level`,
`shell`, ...) — no keywords, namespaces, slashes, or kebab-case.
YAML config uses the same string verbatim (`reasoning_level: deep`).
- `enabled?` is cheap (single atom deref + string lookup), called
per-paint per-row by the render layer; do not turn it into a
function-call indirection.
- Defaults are immutable once registered. Re-registering the
same id is allowed (idempotent boot path) and merges over
prior metadata; the live VALUE in `state` is left alone so a
user override survives a reload.Toggle ids forced ON for the current dynamic scope, regardless of the global
value. sub_loop children bind this (Clojure binding-conveyance carries it
into parallel futures) so a dispatched agent always has the shell + harness
layers, even when the user left them OFF globally. RUNTIME gating only —
value-of (settings UI, persistence) stays the global truth.
Toggle ids forced ON for the current dynamic scope, regardless of the global value. `sub_loop` children bind this (Clojure binding-conveyance carries it into `parallel` futures) so a dispatched agent always has the shell + harness layers, even when the user left them OFF globally. RUNTIME gating only — `value-of` (settings UI, persistence) stays the global truth.
(add-listener! f)Register a no-arg-or-event listener fn. Returns a dispose! thunk
the caller invokes when their consumer goes away (channel close,
extension reload, ...).
Register a no-arg-or-event listener fn. Returns a `dispose!` thunk the caller invokes when their consumer goes away (channel close, extension reload, ...).
(choices-of id)Vec of legal choices for an :enum toggle. Empty when id is
unregistered or registered as :boolean.
Vec of legal choices for an `:enum` toggle. Empty when `id` is unregistered or registered as `:boolean`.
(clear-state!)Wipe live overrides and listeners. Registry is untouched. Used by
tests; production callers should prefer reset-to-default!.
Wipe live overrides and listeners. Registry is untouched. Used by tests; production callers should prefer `reset-to-default!`.
(coerce-config-value id v)Coerce a raw config value (from hand-written vis.yml, where YAML has no
keyword literal, or from the machine state.yml round-trip) onto the
REGISTERED toggle's type so a config-declared toggle behaves like a UI flip:
:boolean — real booleans pass through; strings map by truthy token
(true/1/yes/on → true, anything else → false).
:enum — a string matches the choice whose name equals it,
case-insensitively (reasoning_level: deep → :deep); an
already-legal value passes through unchanged.
Unregistered / untyped ids return the value unchanged (hydrate then drops
the orphan).
Coerce a raw config value (from hand-written `vis.yml`, where YAML has no
keyword literal, or from the machine `state.yml` round-trip) onto the
REGISTERED toggle's type so a config-declared toggle behaves like a UI flip:
`:boolean` — real booleans pass through; strings map by truthy token
(`true`/`1`/`yes`/`on` → true, anything else → false).
`:enum` — a string matches the choice whose `name` equals it,
case-insensitively (`reasoning_level: deep` → `:deep`); an
already-legal value passes through unchanged.
Unregistered / untyped ids return the value unchanged (hydrate then drops
the orphan).(cycle-value! id)Advance an :enum toggle one step through its registered
:choices. Wraps at the end. Throws on boolean toggles.
Advance an `:enum` toggle one step through its registered `:choices`. Wraps at the end. Throws on boolean toggles.
(enabled? id)Boolean cast of (value-of id), OR forced ON in the current dynamic scope
(*forced-on*). Fail-closed: returns false when id is not registered and
not forced. Hot-path — one set lookup + one atom deref.
Boolean cast of `(value-of id)`, OR forced ON in the current dynamic scope (`*forced-on*`). Fail-closed: returns `false` when `id` is not registered and not forced. Hot-path — one set lookup + one atom deref.
(has-orphan-keys? persisted)True when a persisted, string-keyed toggles map carries ids that are no
longer registered — the signature of stale cruft from an earlier build
(e.g. the keyword-id era, when :shell/enabled serialised to the bare
enabled). Callers rewrite a fresh snapshot to converge state.yml.
True when a persisted, string-keyed `toggles` map carries ids that are no longer registered — the signature of stale cruft from an earlier build (e.g. the keyword-id era, when `:shell/enabled` serialised to the bare `enabled`). Callers rewrite a fresh `snapshot` to converge state.yml.
Sentinel that records whether the canonical internal toggles have been installed (idempotent).
Sentinel that records whether the canonical internal toggles have been installed (idempotent).
(hydrate-from-config! config-map)Bulk-apply values from the string-keyed YAML toggles map. Keyword-keyed
internal maps remain accepted for callers that do not originate at YAML.
Bulk-apply values from the string-keyed YAML `toggles` map. Keyword-keyed internal maps remain accepted for callers that do not originate at YAML.
(register-toggle! spec)Register one toggle. spec must satisfy :toggle/spec.
Re-registering the same :id is idempotent: metadata MERGES, the
live VALUE in state is preserved (user overrides survive reload).
Returns the canonical registered spec.
Register one toggle. `spec` must satisfy `:toggle/spec`. Re-registering the same `:id` is idempotent: metadata MERGES, the live VALUE in `state` is preserved (user overrides survive reload). Returns the canonical registered spec.
(register-toggles! specs)Convenience: register a sequence of specs in order, returns the vec of canonical specs.
Convenience: register a sequence of specs in order, returns the vec of canonical specs.
(registered-toggles)Vec of every registered toggle's normalized spec, in registration insertion order. Stable for the TUI settings dialog.
Vec of every registered toggle's normalized spec, in registration insertion order. Stable for the TUI settings dialog.
(reset-to-default! id)Drop the user override for id so resolution falls back to the
registered default. Notifies listeners when the effective value
changes.
Drop the user override for `id` so resolution falls back to the registered default. Notifies listeners when the effective value changes.
(set-enabled! id value)Boolean alias of set-value! for the TUI dialog — keeps the
common toggle-flip call sites readable. Refuses :enum toggles
so an accidental boolean-flip on a multi-value toggle surfaces
loudly; use cycle-value! / set-value! for those.
Boolean alias of `set-value!` for the TUI dialog — keeps the common toggle-flip call sites readable. Refuses `:enum` toggles so an accidental boolean-flip on a multi-value toggle surfaces loudly; use `cycle-value!` / `set-value!` for those.
(set-value! id value)Set id to value and notify listeners. Returns the new value.
Validation matches the registered :type:
:boolean — coerce to boolean.
:enum — must be one of :choices; otherwise throws
:vis.toggles/invalid-value so the bug surfaces
at the call site instead of later in render.
Set `id` to `value` and notify listeners. Returns the new value.
Validation matches the registered `:type`:
`:boolean` — coerce to boolean.
`:enum` — must be one of `:choices`; otherwise throws
`:vis.toggles/invalid-value` so the bug surfaces
at the call site instead of later in render.(snapshot)Return a map {id value} of EVERY persistable toggle's effective
value, intended for serialisation. Skips toggles whose
:persist? is false. Boolean toggles are coerced to boolean;
enum toggles surface their raw choice value. Orphans from a
previously-installed extension are dropped. Keys are SORTED so the
serialised block is stable and diff-friendly, never a hash jumble.
Return a map `{id value}` of EVERY persistable toggle's effective
value, intended for serialisation. Skips toggles whose
`:persist?` is false. Boolean toggles are coerced to boolean;
enum toggles surface their raw choice value. Orphans from a
previously-installed extension are dropped. Keys are SORTED so the
serialised block is stable and diff-friendly, never a hash jumble.(toggle-for-channel? channel spec)True when a toggle should appear in channel's settings UI. A toggle
with no :channels set is channel-neutral (shown everywhere); one with
a :channels set shows only in the listed channels. Keeps a channel's
Settings free of OTHER channels' controls (e.g. the web has no use for
the TUI's mouse-selection-copy or transcript-display toggles).
True when a toggle should appear in `channel`'s settings UI. A toggle with no `:channels` set is channel-neutral (shown everywhere); one with a `:channels` set shows only in the listed channels. Keeps a channel's Settings free of OTHER channels' controls (e.g. the web has no use for the TUI's mouse-selection-copy or transcript-display toggles).
(toggle-id? v)True only for canonical toggle ids: plain lower-case snake_case strings. Keywords, namespaces/slashes, kebab-case, uppercase, blanks, and leading underscores are rejected at the registry boundary.
True only for canonical toggle ids: plain lower-case snake_case strings. Keywords, namespaces/slashes, kebab-case, uppercase, blanks, and leading underscores are rejected at the registry boundary.
(toggle-spec id)Lookup the registered spec for id, or nil.
Lookup the registered spec for `id`, or nil.
(toggle-visible? spec)True when a toggle should appear in a settings UI: no :visible-fn
means always visible; a throwing predicate fails OPEN (shown) so a
broken predicate can never hide a control the user needs.
True when a toggle should appear in a settings UI: no `:visible-fn` means always visible; a throwing predicate fails OPEN (shown) so a broken predicate can never hide a control the user needs.
(toggles-for-channel channel)visible-toggles further scoped to channel via toggle-for-channel?.
Channels render THIS instead of visible-toggles so each Settings UI
only shows controls it actually honours.
`visible-toggles` further scoped to `channel` via `toggle-for-channel?`. Channels render THIS instead of `visible-toggles` so each Settings UI only shows controls it actually honours.
(type-of id):boolean / :enum / nil for unknown.
`:boolean` / `:enum` / nil for unknown.
(value-of id)Resolve the live value for id. Lookup order:
state,nil if the toggle isn't registered.Returns the raw value (boolean for :boolean toggles, any value
from :choices for :enum toggles). enabled? is the
boolean-cast convenience for the common boolean path.
Resolve the live value for `id`. Lookup order: 1. live override in `state`, 2. registered default, 3. `nil` if the toggle isn't registered. Returns the raw value (boolean for `:boolean` toggles, any value from `:choices` for `:enum` toggles). `enabled?` is the boolean-cast convenience for the common boolean path.
(visible-toggles)registered-toggles filtered to what settings UIs should SHOW —
provider-specific knobs declare a :visible-fn so e.g. the OpenAI
Codex verbosity cycle only appears when a Codex provider is actually
configured, and :settings? false toggles (e.g. reasoning-effort, which
has its own Ctrl+R control) stay out of the Settings dialog entirely.
State ops always work on the FULL registry; visibility is a presentation
concern only.
`registered-toggles` filtered to what settings UIs should SHOW — provider-specific knobs declare a `:visible-fn` so e.g. the OpenAI Codex verbosity cycle only appears when a Codex provider is actually configured, and `:settings? false` toggles (e.g. reasoning-effort, which has its own Ctrl+R control) stay out of the Settings dialog entirely. State ops always work on the FULL registry; visibility is a presentation concern only.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |