Configuration: paths, JVM lifecycle, provider presets, svar-native coercion, config file I/O, and the active-provider state every channel reads through.
Two halves:
~/.vis/: state.yml (machine-written), vis.mdb/, vis.log.
init! / init-cli! / shutdown! redirect stdout/stderr into
the log file and bring up Telemere's file handler.active-config atom holds the
currently-selected provider config; current-config,
active-provider, active-model, provider-ids,
has-provider? are the read API. reload-config! re-reads
from disk.The ->svar-provider helper resolves :api-key lazily by calling
the registered provider's :provider/get-token-fn, so the
token-refresh policy stays inside each provider implementation
instead of leaking up here.
Configuration: paths, JVM lifecycle, provider presets, svar-native
coercion, config file I/O, and the active-provider state every
channel reads through.
Two halves:
- On-disk config under `~/.vis/`: `state.yml` (machine-written), `vis.mdb/`, `vis.log`.
`init!` / `init-cli!` / `shutdown!` redirect stdout/stderr into
the log file and bring up Telemere's file handler.
- Live process state: the `active-config` atom holds the
currently-selected provider config; `current-config`,
`active-provider`, `active-model`, `provider-ids`,
`has-provider?` are the read API. `reload-config!` re-reads
from disk.
The `->svar-provider` helper resolves `:api-key` lazily by calling
the registered provider's `:provider/get-token-fn`, so the
token-refresh policy stays inside each provider implementation
instead of leaking up here.(->svar-model model)(->svar-model _provider-id model)Coerce a model representation to svar-native {:name str}.
Coerce a model representation to svar-native `{:name str}`.
(->svar-provider provider)Coerce a provider map to svar-native shape (:id, :api-key,
:base-url, :api-style, :models, optional :responses-path,
optional :llm-headers).
svar's make-router calls normalize-provider which auto-resolves
:base-url from svar's KNOWN_PROVIDERS table for built-in
providers, so we forward :base-url ONLY when the provider map
has one explicitly (vis-only providers like :github-models,
user overrides, or OAuth-supplied URLs). For known providers
svar fills in the URL itself - stop fighting it.
When :api-key is nil, look the provider up in the global
provider registry (registry.clj) and call its
:provider/get-token-fn to resolve a usable token. Each provider
implementation handles its own auth lifecycle (OAuth refresh,
env-var fallback, provider-specific headers, ...) so this fn stays
provider-agnostic and never references a concrete provider ns by
name.
Coerce a provider map to svar-native shape (`:id`, `:api-key`, `:base-url`, `:api-style`, `:models`, optional `:responses-path`, optional `:llm-headers`). svar's `make-router` calls `normalize-provider` which auto-resolves `:base-url` from svar's `KNOWN_PROVIDERS` table for built-in providers, so we forward `:base-url` ONLY when the provider map has one explicitly (vis-only providers like `:github-models`, user overrides, or OAuth-supplied URLs). For known providers svar fills in the URL itself - stop fighting it. When `:api-key` is nil, look the provider up in the global provider registry (registry.clj) and call its `:provider/get-token-fn` to resolve a usable token. Each provider implementation handles its own auth lifecycle (OAuth refresh, env-var fallback, provider-specific headers, ...) so this fn stays provider-agnostic and never references a concrete provider ns by name.
(active-model)Return the primary model name string, or nil.
Return the primary model name string, or nil.
(active-provider)Return the first (primary) provider from config, or nil.
Return the first (primary) provider from config, or nil.
(baked-token pid)The access token ->svar-provider last baked into a router for provider pid
(nil if none). This is the token the live router's requests actually send, so
it's the correct REJECTED token on a 401 — unlike the current on-file token,
which a peer tab/process may already have rotated to something fresh.
The access token `->svar-provider` last baked into a router for provider `pid` (nil if none). This is the token the live router's requests actually send, so it's the correct REJECTED token on a 401 — unlike the current on-file token, which a peer tab/process may already have rotated to something fresh.
(config-problems)Model-readable, per-top-level-key reasons the currently merged live config
fails the contract (config-spec/explain-problems over load-config-raw),
or [] when it is valid. Loads leniently so this never throws even while the
config on disk is broken — it is the diagnostic surfaced as config_error.
Model-readable, per-top-level-key reasons the currently merged live config fails the contract (`config-spec/explain-problems` over `load-config-raw`), or [] when it is valid. Loads leniently so this never throws even while the config on disk is broken — it is the diagnostic surfaced as `config_error`.
(current-config)Return the current provider config. Loads from disk on first call.
Return the current provider config. Loads from disk on first call.
Default :search :always-exclude patterns (.gitignore syntax) guarding
the subtrees an :include-gitignored-paths overlay re-includes:
machine-generated dirs nobody wants surfaced even inside a rescued vendored
repo. Setting :always-exclude in config REPLACES this list (vectors
replace on merge, like everywhere else in config).
Default `:search :always-exclude` patterns (`.gitignore` syntax) guarding the subtrees an `:include-gitignored-paths` overlay re-includes: machine-generated dirs nobody wants surfaced even inside a rescued vendored repo. Setting `:always-exclude` in config REPLACES this list (vectors replace on merge, like everywhere else in config).
(display-label pid)Human-readable label for a provider id. Never persisted.
Human-readable label for a provider id. Never persisted.
(extension-env-overrides)Persisted extension environment overrides from ~/.vis/state.yml
under :environment. Keys are environment variable names as
strings. Values are strings. This does NOT mutate the process
environment; extension code should call extension-env-value when
it wants config-over-env resolution.
Persisted extension environment overrides from `~/.vis/state.yml` under `:environment`. Keys are environment variable names as strings. Values are strings. This does NOT mutate the process environment; extension code should call `extension-env-value` when it wants config-over-env resolution.
(extension-env-status name)Return source and value metadata for an extension-declared env var.
:source is one of :config, :env, or :unset.
Return source and value metadata for an extension-declared env var. `:source` is one of `:config`, `:env`, or `:unset`.
(extension-env-value name)Resolve an extension-declared env var as config override -> real env. Blank/missing values return nil.
Resolve an extension-declared env var as config override -> real env. Blank/missing values return nil.
(first-run?)True on a genuine FIRST run: no provider configured AND no global machine store
(~/.vis/state.yml) has ever been written. Distinguishes the full welcome
(brand-new user) from a returning user who merely has no provider right now
(e.g. removed their only one).
True on a genuine FIRST run: no provider configured AND no global machine store (`~/.vis/state.yml`) has ever been written. Distinguishes the full welcome (brand-new user) from a returning user who merely has no provider right now (e.g. removed their only one).
(frame-buffered-tty-out raw)Wrap the raw tty stream so a whole repaint reaches the terminal as ONE atomic write instead of one write(2) syscall PER CELL.
Lanterna's refreshByDelta calls putString/setCursorPosition per
changed cell and only flushes once at the end of refresh. On a raw
FileOutputStream every one of those calls is its own syscall straight
to the tty, so the terminal renders PARTIAL frames mid-repaint: a fold
toggle that shifts the transcript reads as a whole-screen flicker and a
transient content jump. Buffering until flush collapses the frame to
one write, and the DEC 2026 bracket makes the terminal hold rendering
until the frame is complete even when the kernel chunks the write.
Everything vis writes to the tty outside Lanterna (SGR-mouse /
bracketed-paste toggles, OSC 11 background, the :bell fx, the panic
PrintStream) already flushes explicitly, so nothing can sit in the
buffer across frames.
Wrap the raw tty stream so a whole repaint reaches the terminal as ONE atomic write instead of one write(2) syscall PER CELL. Lanterna's `refreshByDelta` calls `putString`/`setCursorPosition` per changed cell and only `flush`es once at the end of `refresh`. On a raw `FileOutputStream` every one of those calls is its own syscall straight to the tty, so the terminal renders PARTIAL frames mid-repaint: a fold toggle that shifts the transcript reads as a whole-screen flicker and a transient content jump. Buffering until `flush` collapses the frame to one write, and the DEC 2026 bracket makes the terminal hold rendering until the frame is complete even when the kernel chunks the write. Everything vis writes to the tty outside Lanterna (SGR-mouse / bracketed-paste toggles, OSC 11 background, the `:bell` fx, the panic PrintStream) already flushes explicitly, so nothing can sit in the buffer across frames.
(init!)Redirect System/out and System/err to the log file. Lanterna uses tty-in / tty-out for terminal I/O. Call from the TUI entry point.
Redirect System/out and System/err to the log file. Lanterna uses tty-in / tty-out for terminal I/O. Call from the TUI entry point.
(init-cli!)Logging init for non-TUI processes. Same redirects as init! but without the shutdown hook (CLI commands run to completion and exit).
Logging init for non-TUI processes. Same redirects as init! but without the shutdown hook (CLI commands run to completion and exit).
(invalidate-config-cache!)Drop the load-config-raw memo. Called on every config WRITE, because two
writes inside one filesystem mtime tick could otherwise stamp identically.
Drop the `load-config-raw` memo. Called on every config WRITE, because two writes inside one filesystem mtime tick could otherwise stamp identically.
(load-config)Load the validated YAML config and adapt its finite schema keys to internal
keyword-keyed domain maps. load-config-raw retains the original string keys.
Load the validated YAML config and adapt its finite schema keys to internal keyword-keyed domain maps. `load-config-raw` retains the original string keys.
(load-config-raw)Load raw config as the deep-merge of four YAML sources — later sources win, nested maps merge, scalar/vector values replace:
~/.vis/config.yml (or .yaml / vis.yml / vis.yaml) — hand-written
global base~/.vis/state.yml — machine-written global store (OAuth tokens, TUI-added
providers); wins over the hand-written base<cwd>/vis.yml (or vis.yaml) — visible project root, the committed team
config<cwd>/.vis/config.yml (or .yaml) — hidden project overlay; the NESTED
overlay wins over the root file (personal beats committed)Memoized against the sources' mtime+size (see config-raw-cache).
Load raw config as the deep-merge of four YAML sources — later sources win, nested maps merge, scalar/vector values replace: 1. `~/.vis/config.yml` (or `.yaml` / `vis.yml` / `vis.yaml`) — hand-written global base 2. `~/.vis/state.yml` — machine-written global store (OAuth tokens, TUI-added providers); wins over the hand-written base 3. `<cwd>/vis.yml` (or `vis.yaml`) — visible project root, the committed team config 4. `<cwd>/.vis/config.yml` (or `.yaml`) — hidden project overlay; the NESTED overlay wins over the root file (personal beats committed) Memoized against the sources' mtime+size (see `config-raw-cache`).
(load-global-config-raw)Load the machine-written global store as a config map (or nil): ~/.vis/state.yml,
the YAML file Vis read-modify-writes. Machine-owned on purpose — kept out of the
hand-written YAML merge so the RMW cycle never clobbers user files.
Load the machine-written global store as a config map (or nil): `~/.vis/state.yml`, the YAML file Vis read-modify-writes. Machine-owned on purpose — kept out of the hand-written YAML merge so the RMW cycle never clobbers user files.
(load-global-yaml-config-raw)Load only the hand-written global YAML tier: the first existing of
~/.vis/config.yml / config.yaml / vis.yml / vis.yaml, or nil. This
hand-written base is deep-merged UNDER the machine-written ~/.vis/state.yml
store (state.yml wins per key), keeping user-authored config separate from
the RMW machine file.
Load only the hand-written global YAML tier: the first existing of `~/.vis/config.yml` / `config.yaml` / `vis.yml` / `vis.yaml`, or nil. This hand-written base is deep-merged UNDER the machine-written `~/.vis/state.yml` store (`state.yml` wins per key), keeping user-authored config separate from the RMW machine file.
(load-project-config-raw)Load the hidden project overlay tier: the first existing of
<invocation-cwd>/.vis/config.yml / .vis/config.yaml, or nil. Skipped when
the overlay dir resolves to the global ~/.vis store, so running Vis from
$HOME never aliases a global file as a project overlay.
Load the hidden project overlay tier: the first existing of `<invocation-cwd>/.vis/config.yml` / `.vis/config.yaml`, or nil. Skipped when the overlay dir resolves to the global `~/.vis` store, so running Vis from $HOME never aliases a global file as a project overlay.
(load-project-root-config-raw)Load the visible project-root tier: the first existing of
<invocation-cwd>/vis.yml / vis.yaml, or nil.
Load the visible project-root tier: the first existing of `<invocation-cwd>/vis.yml` / `vis.yaml`, or nil.
(model-name model)Extract the model name string from a model (string or {:name str}).
Extract the model name string from a model (string or `{:name str}`).
(provider-base-url provider)Resolve base-url for a provider: explicit field on the provider map first (so user-supplied URLs win), then the merged catalog.
Resolve base-url for a provider: explicit field on the provider map first (so user-supplied URLs win), then the merged catalog.
(provider-configured?)True when at least one provider is configured (global or project config).
The single predicate entry points use to branch onboarding vs normal start —
never trips the resolve-config throw.
True when at least one provider is configured (global or project config). The single predicate entry points use to branch onboarding vs normal start — never trips the `resolve-config` throw.
(provider-ids)Set of configured provider :id keywords.
Set of configured provider `:id` keywords.
(provider-model-visible? provider-id model-id)True when svar's provider-scoped model filters allow this model id.
True when svar's provider-scoped model filters allow this model id.
(provider-presets)All known provider presets, sorted for the 'Add Provider' picker.
All known provider presets, sorted for the 'Add Provider' picker.
(provider-template pid)Preset descriptor for a provider id, merged from a provider extension's metadata and svar's catalog. Returns nil for unknown or intentionally removed ids.
Preset descriptor for a provider id, merged from a provider extension's metadata and svar's catalog. Returns nil for unknown or intentionally removed ids.
(remove-config-provider! provider-id)(remove-config-provider! provider-id source)Remove every persisted provider entry for provider-id from the string-keyed
machine config, preserving unrelated keys.
Remove every persisted provider entry for `provider-id` from the string-keyed machine config, preserving unrelated keys.
(resolve-config)(resolve-config explicit-config)Resolve provider config: explicit -> merged YAML config. Throws when nothing is available.
Resolve provider config: explicit -> merged YAML config. Throws when nothing is available.
(resolve-db-spec)(resolve-db-spec explicit-db-spec)Resolve DB spec: explicit -> JVM property -> environment -> validated YAML -> default.
Resolve DB spec: explicit -> JVM property -> environment -> validated YAML -> default.
Access token last baked into a built router, keyed by provider id.
->svar-provider records the token it resolved via :provider/get-token-fn
here at router-build time. On a 401 the runtime reads it back as the REJECTED
token — the exact token the failing request sent — so single-flight refresh
reuse won't hand that same dead token straight back (see loop.clj OAuth 401
recovery). Correct across multi-tab/multi-process rotation, where the current
ON-FILE token may already be a peer's fresh one.
Access token last baked into a built router, keyed by provider id. `->svar-provider` records the token it resolved via `:provider/get-token-fn` here at router-build time. On a 401 the runtime reads it back as the REJECTED token — the exact token the failing request sent — so single-flight refresh reuse won't hand that same dead token straight back (see loop.clj OAuth 401 recovery). Correct across multi-tab/multi-process rotation, where the current ON-FILE token may already be a peer's fresh one.
(router-opts config)Extracts svar/make-router opts from a Vis config map.
Reads the :router block from the merged YAML config:
{:router
{:rate-limit {:same-provider-delays-ms [2000 3000 6000]
:fallback-after-ms 30000
:respect-retry-after? true
:fallback-provider? true}
:network {:timeout-ms 300000 :idle-timeout-ms 45000}
:budget {:max-tokens 1000000 :max-cost 5.0}}}
Returns {} when no :router block is present so svar's built-in
defaults win. Unknown keys are dropped — only the keys svar's
make-router knows about flow through.
See com.blockether.svar.internal.router/make-router for the
authoritative key reference.
Extracts `svar/make-router` opts from a Vis config map.
Reads the `:router` block from the merged YAML config:
```clojure
{:router
{:rate-limit {:same-provider-delays-ms [2000 3000 6000]
:fallback-after-ms 30000
:respect-retry-after? true
:fallback-provider? true}
:network {:timeout-ms 300000 :idle-timeout-ms 45000}
:budget {:max-tokens 1000000 :max-cost 5.0}}}
```
Returns `{}` when no `:router` block is present so svar's built-in
defaults win. Unknown keys are dropped — only the keys svar's
`make-router` knows about flow through.
See `com.blockether.svar.internal.router/make-router` for the
authoritative key reference.(runtime-config v)Adapt an already-validated string-keyed YAML map to Vis' internal domain maps.
Only the finite keys in runtime-keywords become keywords. User-defined map keys
remain strings, and parsing/validation never uses this adapter.
Adapt an already-validated string-keyed YAML map to Vis' internal domain maps. Only the finite keys in `runtime-keywords` become keywords. User-defined map keys remain strings, and parsing/validation never uses this adapter.
(save-config! config)(save-config! config source)Persist configuration to ~/.vis/state.yml using the string-keyed YAML contract.
Callers may supply internal keyword-keyed domain maps; validation always runs on
the exact string-keyed map that is written.
Persist configuration to `~/.vis/state.yml` using the string-keyed YAML contract. Callers may supply internal keyword-keyed domain maps; validation always runs on the exact string-keyed map that is written.
(save-extension-env-var! name value)Persist or clear one extension env override in ~/.vis/state.yml.
Blank/nil value removes the override, revealing the process env
value again if one exists. Preserves all other config keys.
Persist or clear one extension env override in `~/.vis/state.yml`. Blank/nil `value` removes the override, revealing the process env value again if one exists. Preserves all other config keys.
(search-overlay)Return the search overlay as an internal keyword-keyed map, or nil when unset. The source configuration remains string-keyed and spec-validated.
Return the search overlay as an internal keyword-keyed map, or nil when unset. The source configuration remains string-keyed and spec-validated.
(shutdown!)Flush and stop all telemere handlers. Call after the TUI screen stops.
Flush and stop all telemere handlers. Call after the TUI screen stops.
Machine-owned RMW config store ~/.vis/state.yml (YAML). Vis read-modify-writes
this exact file — OAuth tokens, TUI-added providers, extension env overrides — so
it is kept SEPARATE from the hand-written ~/.vis/config.yml tier: the RMW cycle
must never fold (and thus clobber) a user's hand-written YAML.
Machine-owned RMW config store `~/.vis/state.yml` (YAML). Vis read-modify-writes this exact file — OAuth tokens, TUI-added providers, extension env overrides — so it is kept SEPARATE from the hand-written `~/.vis/config.yml` tier: the RMW cycle must never fold (and thus clobber) a user's hand-written YAML.
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 |