Liking cljdoc? Tell your friends :D

com.blockether.vis.ext.channel-tui.boxed-table

Bordered, scrollable, single-selection data table for TUI dialogs.

Composes the lower-level table border/row primitives with scrollbar so callers don't repeat the same boilerplate (top border + header row + middle separator + N body rows + selection marker gutter + scrollbar) at every dialog site.

Geometry rules

Given a dialog bounds ({:left :inner-w}) the layout reserves:

col left+1 → table left border (flush to dialog edge) col left+3 → selection marker, INSIDE the first column cols left+1 .. R-1 → boxed table (own borders included) col R → scrollbar (right of right table border)

where R = left + inner-w. This guarantees the scrollbar never overpaints the table's right border.

The selection marker lives INSIDE the first column: the first data column is internally widened by p/SELECTION_WIDTH, its cell text is indented by that many cols, and the marker glyph is painted over the reserved gutter (matching the Ctrl+G navigator). Callers size their columns against the reported :table-content-w, which already excludes the marker reserve — no caller-side change needed.

Row layout (relative to caller-provided :top)

top+0 ┌── top border ──┐ top+1 │ header row │ top+2 ├── separator ──┤ top+3 │ first body row │ … top+2+body-h │ last body row │ top+3+body-h └── bottom ──┘ (only when :closed? true)

By default the table is open at the bottom; callers usually follow the body with a divider + status line and don't want a closing └┘ cap. Pass :closed? true to draw the closing border (matches the navigator-style boxed picker).

Bordered, scrollable, single-selection data table for TUI dialogs.

Composes the lower-level `table` border/row primitives with
`scrollbar` so callers don't repeat the same boilerplate (top
border + header row + middle separator + N body rows + selection
marker gutter + scrollbar) at every dialog site.

Geometry rules
--------------
Given a dialog `bounds` ({:left :inner-w}) the layout reserves:

  col `left+1`            → table `│` left border (flush to dialog edge)
  col `left+3`            → selection marker, INSIDE the first column
  cols `left+1 .. R-1`    → boxed table (own `│` borders included)
  col `R`                 → scrollbar (right of right table border)

where `R = left + inner-w`. This guarantees the scrollbar never
overpaints the table's right `│` border.

The selection marker lives INSIDE the first column: the first data
column is internally widened by `p/SELECTION_WIDTH`, its cell text
is indented by that many cols, and the marker glyph is painted over
the reserved gutter (matching the Ctrl+G navigator). Callers size
their columns against the reported `:table-content-w`, which already
excludes the marker reserve — no caller-side change needed.

Row layout (relative to caller-provided `:top`)
-----------------------------------------------
  top+0  ┌── top border ──┐
  top+1  │ header row     │
  top+2  ├── separator  ──┤
  top+3  │ first body row │
  …
  top+2+body-h │ last body row │
  top+3+body-h └── bottom ──┘  (only when `:closed? true`)

By default the table is open at the bottom; callers usually follow
the body with a divider + status line and don't want a closing
`└┘` cap. Pass `:closed? true` to draw the closing border (matches
the navigator-style boxed picker).
raw docstring

com.blockether.vis.ext.channel-tui.builtin-hooks

Built-in TUI channel contributions mounted on the channel-tui extension.

Channel-tui is itself an extension (:ext/name com.blockether.vis.ext.channel-tui), so it can declare its own :ext/channel-contributions like any external extension. This gives every first-party TUI surface (model display, reasoning level, codex verbosity) the same contribution path third-party extensions use.

Why register here instead of inline in footer.clj:

  1. The data flow is uniform - footer's extension-footer-segments treats first-party + third-party contributions identically. Settings UI's contributor toggle (in dialogs.clj) sees these as regular contributors that the user can hide.

  2. Other channels (Telegram, web, ...) reading (channel-contributions-for :tui) get the same model/provider data without channel-tui-specific calls.

  3. Provider extensions can later override / supplement these contributions with provider-specific contributions (e.g. anthropic could register a :anthropic/model-footer showing rate-limit headroom) without touching channel-tui core.

The contribution fns return CANONICAL IR (channel-agnostic). Channels translate IR to their surface.

Built-in TUI channel contributions mounted on the channel-tui extension.

Channel-tui is itself an extension (`:ext/name`
`com.blockether.vis.ext.channel-tui`), so it can declare its own
`:ext/channel-contributions` like any external extension. This gives every
first-party TUI surface (model display, reasoning level, codex
verbosity) the same contribution path third-party extensions use.

Why register here instead of inline in footer.clj:

  1. The data flow is uniform - footer's `extension-footer-segments`
     treats first-party + third-party contributions identically. Settings
     UI's contributor toggle (in dialogs.clj) sees these as
     regular contributors that the user can hide.

  2. Other channels (Telegram, web, ...) reading
     `(channel-contributions-for :tui)` get the same model/provider data
     without channel-tui-specific calls.

  3. Provider extensions can later override / supplement these
     contributions with provider-specific contributions (e.g. anthropic could
     register a `:anthropic/model-footer` showing rate-limit
     headroom) without touching channel-tui core.

The contribution fns return CANONICAL IR (channel-agnostic). Channels
translate IR to their surface.
raw docstring

com.blockether.vis.ext.channel-tui.chat

TUI-side projections over the canonical in-process gateway.

On startup the TUI creates a fresh :tui gateway session by default. Pass --session-id ID or --resume to pick up an existing one. Session data is persisted in ~/.vis/vis.mdb so you can come back to it.

TUI-side projections over the canonical in-process gateway.

On startup the TUI creates a fresh `:tui` gateway session by default.
Pass `--session-id ID` or `--resume` to pick up an existing one.
Session data is persisted in `~/.vis/vis.mdb` so you can come back to it.
raw docstring

com.blockether.vis.ext.channel-tui.cinema

Headless session → MP4 screencast. Replays a persisted session's transcript through the REAL TUI render pipeline (screen/render-frame! painting into a Lanterna VirtualTerminal — no TTY needed), and encodes the captured frames to .mp4:

pure-JVM H.264 via jcodec's AWTSequenceEncoder (each frame rendered to a BufferedImage with Java2D). Pixel-exact because WE draw the glyphs — no external player font drift. No native deps, GraalVM-friendly.

The replay is HUMANIZED rather than a flat scroll-through: every disclosure COLLAPSED, the session is re-enacted turn by turn as if it were happening live — a user turn is TYPED IN character by character; before each answer a WORK beat holds the live Vis is calling the provider… spinner in the assistant's place; then the finished answer JUMPS IN and SLOWLY scrolls into view. See session->frames.

The heavy TUI stack (screen, Lanterna) is reached via requiring-resolve so merely loading this namespace stays cheap.

Headless session → MP4 screencast. Replays a persisted session's transcript
through the REAL TUI render pipeline (`screen/render-frame!` painting into a
Lanterna `VirtualTerminal` — no TTY needed), and encodes the captured frames
to `.mp4`:

  pure-JVM H.264 via jcodec's `AWTSequenceEncoder` (each frame rendered to a
  `BufferedImage` with Java2D). Pixel-exact because WE draw the glyphs — no
  external player font drift. No native deps, GraalVM-friendly.

The replay is HUMANIZED rather than a flat scroll-through: every disclosure
COLLAPSED, the session is re-enacted turn by turn as if it were happening
live — a user turn is TYPED IN character by character; before each answer a
WORK beat holds the live `Vis is calling the provider…` spinner in the
assistant's place; then the finished answer JUMPS IN and SLOWLY scrolls into
view. See `session->frames`.

The heavy TUI stack (`screen`, Lanterna) is reached via `requiring-resolve`
so merely loading this namespace stays cheap.
raw docstring

com.blockether.vis.ext.channel-tui.click-regions

Live registry of clickable rectangles painted by the chat renderer. The render thread calls begin-frame! at the start of every full repaint, register!s one entry per painted chrome row, then commit-frame!s the staged set as the new live registry. The input thread lookups the entry under the mouse cursor on MOVE (hover) and CLICK_DOWN.

Why double-buffered (staging + published) instead of one atom:

  • register! runs many times during a paint. Reads from the input thread can land BETWEEN the per-row registrations of a single frame. With a single-buffer design the input thread would see a partially-filled vec and miss clicks on chrome rows that hadn't been painted yet - the very bug that made the header copy-id button feel "sometimes broken". The staged buffer is private to the render thread; commit-frame! publishes the WHOLE frame's regions in a single atomic swap, so lookup always sees a complete frame (the previous one until commit, the new one after).

  • The renderer paints into a Lanterna TextGraphics object that has no place to attach "and also - here are the clickable bits I just drew." We'd otherwise need a parallel return channel through every paint helper.

  • The set is small (tens of entries at most - the visible scrollback's worth of links); a linear scan on lookup is fine and removes the need for any spatial-index dance.

Coordinate convention: every region is stored in ABSOLUTE screen coordinates so the input handler can compare directly against MouseAction.getPosition().

Live registry of clickable rectangles painted by the chat
renderer. The render thread calls `begin-frame!` at the start of
every full repaint, `register!`s one entry per painted chrome
row, then `commit-frame!`s the staged set as the new live
registry. The input thread `lookup`s the entry under the mouse
cursor on `MOVE` (hover) and `CLICK_DOWN`.

Why double-buffered (staging + published) instead of one atom:

- `register!` runs many times during a paint. Reads from the
  input thread can land BETWEEN the per-row registrations of
  a single frame. With a single-buffer design the input thread
  would see a partially-filled vec and miss clicks on chrome
  rows that hadn't been painted yet - the very bug that made
  the header copy-id button feel "sometimes broken". The
  staged buffer is private to the render thread; `commit-frame!`
  publishes the WHOLE frame's regions in a single atomic swap,
  so `lookup` always sees a complete frame (the previous one
  until commit, the new one after).

- The renderer paints into a Lanterna `TextGraphics` object that
  has no place to attach "and also - here are the clickable
  bits I just drew." We'd otherwise need a parallel return
  channel through every paint helper.

- The set is small (tens of entries at most - the visible
  scrollback's worth of links); a linear scan on lookup is
  fine and removes the need for any spatial-index dance.

Coordinate convention: every region is stored in ABSOLUTE screen
coordinates so the input handler can compare directly against
`MouseAction.getPosition()`.
raw docstring

com.blockether.vis.ext.channel-tui.command-suggest

Pure slash-command discovery/filtering for the TUI prompt.

Pure slash-command discovery/filtering for the TUI prompt.
raw docstring

com.blockether.vis.ext.channel-tui.components

Reusable header/tab UI components.

Each drawable here is a self-contained fn that paints into a Lanterna TextGraphics and — when asked — registers its OWN click region, so the same widget can't drift between call sites (the bug that left the tab close painted in one place and made clickable in another, or not at all). Two kinds live here:

  • text layout helpers — truncate-with-ellipsis, center-padded
  • drawable components — close-button!, tab-cell!

Components own their visual contract AND their interaction contract (the click region's :kind), keeping header.clj a thin layout caller.

Reusable header/tab UI components.

Each drawable here is a self-contained fn that paints into a Lanterna
`TextGraphics` and — when asked — registers its OWN click region, so the
same widget can't drift between call sites (the bug that left the tab
close `✕` painted in one place and made clickable in another, or not at
all). Two kinds live here:

  - text layout helpers — `truncate-with-ellipsis`, `center-padded`
  - drawable components — `close-button!`, `tab-cell!`

Components own their visual contract AND their interaction contract
(the click region's `:kind`), keeping `header.clj` a thin layout caller.
raw docstring

com.blockether.vis.ext.channel-tui.core

Lightweight TUI channel registration.

Keep this namespace tiny: manifest discovery loads it on every Vis startup. The full Lanterna screen implementation is resolved only when the TUI channel actually runs.

Fast-fail validation: --session-id ID is parsed, looked up, and (on miss) reported with a friendly message + exit code 2 BEFORE the heavy com.blockether.vis.ext.channel-tui.screen namespace (Lanterna + ~14 sibling tui namespaces) is required. Catching the miss early on the lightweight path keeps vis channels tui --session-id <bad> from paying full TUI class-loading cost.

Correct hits still go through the normal screen channel-main, so all existing screen behavior - argument parsing, redirects, lifecycle - stays intact. Runtime semantics live below the TUI layer and are unaffected: this only changes WHEN screen.clj is required.

Lightweight TUI channel registration.

Keep this namespace tiny: manifest discovery loads it on every Vis startup.
The full Lanterna screen implementation is resolved only when the TUI
channel actually runs.

Fast-fail validation:
  `--session-id ID` is parsed, looked up, and (on miss) reported with
  a friendly message + exit code 2 BEFORE the heavy
  `com.blockether.vis.ext.channel-tui.screen` namespace (Lanterna +
  ~14 sibling tui namespaces) is required. Catching the miss early on
  the lightweight path keeps `vis channels tui --session-id <bad>`
  from paying full TUI class-loading cost.

  Correct hits still go through the normal screen channel-main, so all
  existing screen behavior - argument parsing, redirects, lifecycle -
  stays intact. Runtime semantics live below the TUI layer
  and are unaffected: this only changes WHEN screen.clj is required.
raw docstring

com.blockether.vis.ext.channel-tui.file-suggest

Inline @ file-mention suggestions for the TUI composer — the SAME affordance the web composer already has, so both channels share one behaviour instead of a modal on one side and an inline picker on the other.

Ranking is powered by fff (internal.file-picker/fuzzy-file-rows) — the very same engine behind the find_files tool and the gateway /v1/sessions/:sid/suggest service — so @fpick fuzzily finds file_picker.clj (typo-tolerant subsequence match ranked by frecency), not just a literal substring.

The trigger rules mirror the web/JS verbatim so writing a literal @ is never endangered:

  • the @ must begin a word (start of input or right after whitespace), so foo@bar, user@host, decorators never pop the picker;
  • @@ escapes to a literal @ and suppresses the popup;
  • selection is advisory — nothing is rewritten unless the user picks.

fff owns a FRESH in-memory index instance that is opened OFF the render thread and cached for the session; per-keystroke search on the open instance is sub-millisecond, so filtering stays instant. The instance is NEVER closed while a search may run (fff's native search SIGSEGVs on a closed handle); a periodic rebuild swaps a new instance in and closes the superseded one only after a grace period.

Inline `@` file-mention suggestions for the TUI composer — the SAME
affordance the web composer already has, so both channels share one
behaviour instead of a modal on one side and an inline picker on the
other.

Ranking is powered by fff (`internal.file-picker/fuzzy-file-rows`) — the
very same engine behind the `find_files` tool and the gateway
`/v1/sessions/:sid/suggest` service — so `@fpick` fuzzily finds
`file_picker.clj` (typo-tolerant subsequence match ranked by frecency),
not just a literal substring.

The trigger rules mirror the web/JS verbatim so writing a literal `@`
is never endangered:

- the `@` must begin a word (start of input or right after whitespace),
  so `foo@bar`, `user@host`, decorators never pop the picker;
- `@@` escapes to a literal `@` and suppresses the popup;
- selection is advisory — nothing is rewritten unless the user picks.

fff owns a FRESH in-memory index instance that is opened OFF the render
thread and cached for the session; per-keystroke `search` on the open
instance is sub-millisecond, so filtering stays instant. The instance is
NEVER closed while a search may run (fff's native search SIGSEGVs on a
closed handle); a periodic rebuild swaps a new instance in and closes the
superseded one only after a grace period.
raw docstring

com.blockether.vis.ext.channel-tui.footer

Dedicated one-row status footer rendered below the input box.

Codex-style three-region layout:

[LEFT]                    [CENTER]                    [RIGHT]
glm-5.1 (balanced)              total tok 12000→800 (cached 8000)  $0.04

Each region holds a list of {:text :fg :bold? :region :priority} spans separated by ' / ' in muted color. The full segment list is built up-front and shrunk by dropping the highest :priority number (least important) until it fits the available width.

Footer deliberately avoids transient run-state and cancellation banners. Those live in the assistant bubble and host notifications; this row keeps slow-changing identity + budget bits.

Run-state (spinner, iteration counter, elapsed time, current phase) lives EXCLUSIVELY in the assistant bubble's progress->text block. Putting it in the footer too was a duplicate - same ⠋ 11.2s showing twice on screen. The footer keeps slow-changing identity + budget bits; the bubble keeps the live activity story («Vis is thinking (iter 3)... 4.1s / Esc to cancel»).

The first footer row carries repository context on the right: repo/branch, one compact changed-file count, and ahead/behind counts when an upstream is configured. The second footer row carries provider budgets and cumulative usage under that git context. Git status is cached briefly so repainting the TUI does not shell out to git on every frame.

Every numeric format uses Locale/ROOT so a Polish JVM doesn't produce mixed 5,8k next to English k. The previous footer embedded the status into the input box's bottom border via embed-in-bar, which forced single-color rendering and was the reason run-state had to live inside the assistant bubble; this namespace replaces that whole path.

Dedicated one-row status footer rendered below the input box.

Codex-style three-region layout:

    [LEFT]                    [CENTER]                    [RIGHT]
    glm-5.1 (balanced)              total tok 12000→800 (cached 8000)  $0.04

Each region holds a list of `{:text :fg :bold? :region :priority}`
spans separated by ' / ' in muted color. The full segment list is
built up-front and shrunk by dropping the highest `:priority`
number (least important) until it fits the available width.

Footer deliberately avoids transient run-state and cancellation
banners. Those live in the assistant bubble and host notifications;
this row keeps slow-changing identity + budget bits.

Run-state (spinner, iteration counter, elapsed time, current
phase) lives EXCLUSIVELY in the assistant bubble's `progress->text`
block. Putting it in the footer too was a duplicate - same
`⠋ 11.2s` showing twice on screen. The footer keeps slow-changing
identity + budget bits; the bubble keeps the live activity story
(«Vis is thinking (iter 3)... 4.1s / Esc to cancel»).

The first footer row carries repository context on the right:
repo/branch, one compact changed-file count, and ahead/behind counts
when an upstream is configured. The second footer row carries provider
budgets and cumulative usage under that git context. Git status is
cached briefly so repainting the TUI does not shell out to git on every frame.

Every numeric format uses `Locale/ROOT` so a Polish JVM doesn't
produce mixed `5,8k` next to English `k`. The previous footer
embedded the status into the input box's bottom border via
`embed-in-bar`, which forced single-color rendering and was the
reason run-state had to live inside the assistant bubble; this
namespace replaces that whole path.
raw docstring

com.blockether.vis.ext.channel-tui.header

Dedicated header band painted above the messages area.

Three-region layout:

[LEFT]                    [CENTER]                    [RIGHT]
✓ Copied!                 Session title          d8d6a0a1
(notification/status)     (or fallback placeholder)   (id target)
  • LEFT: latest active host notification (com.blockether.vis.core/notify!), otherwise live channel status. The session title does NOT live here.
  • CENTER: session title from app-db (:title). When the session has no title yet, falls back to a placeholder so the row never looks broken on a fresh run.
  • RIGHT: short session id (first 8 chars of the UUID) as the clickable affordance that drops the FULL UUID onto the system clipboard. No notifications or channel statuses render here.

Pure draw: reads :title and :session from app-db, the active notifications list from vis.core/notifications, writes cells, registers ONE click region for the copy affordance.

Repaint: the banner updates as notifications come and go. screen.clj registers a watcher on screen mount that bumps the render version for any change, so a (notify! ...) from anywhere nudges this band to repaint immediately.

Dedicated header band painted above the messages area.

Three-region layout:

    [LEFT]                    [CENTER]                    [RIGHT]
    ✓ Copied!                 Session title          d8d6a0a1
    (notification/status)     (or fallback placeholder)   (id target)

- LEFT: latest active host notification (`com.blockether.vis.core/notify!`),
  otherwise live channel status. The session title does NOT live here.
- CENTER: session title from app-db (`:title`). When the
  session has no title yet, falls back to a placeholder so
  the row never looks broken on a fresh run.
- RIGHT: short session id (first 8 chars of the UUID) as the clickable
  affordance that drops the FULL UUID onto the system clipboard. No
  notifications or channel statuses render here.

Pure draw: reads `:title` and `:session` from app-db, the
active notifications list from `vis.core/notifications`, writes
cells, registers ONE click region for the copy affordance.

Repaint: the banner updates as notifications come and go.
`screen.clj` registers a watcher on screen mount that bumps the
render version for any change, so a `(notify! ...)` from anywhere
nudges this band to repaint immediately.
raw docstring

com.blockether.vis.ext.channel-tui.highlight

Tree-sitter syntax highlighting for TUI code fences.

The eval / code fences the model emits are colored by parsing them with the bundled tree-sitter grammars — the SAME com.blockether/tree-sitter-language-pack the structural editors use — and painting each node by the grammar's OWN highlights.scm capture scheme. That makes the classification accurate (a ; inside a string is not a comment, a number inside a symbol is not a number) and GENERAL: Clojure, Python, JavaScript, JSON, … every fence we render is colored off each grammar's canonical highlight rules.

The heavy lifting — parse, a real tree-sitter query interpreter (field-scoped captures, alternations, #match?/#eq? predicates), per-byte capture labeling, ANSI run coalescing — lives in the pack's JVM-native Highlighter (Java, on the hot path), NOT in Clojure. This namespace is a thin edge: it maps a fence :lang to a grammar, hands the pack our capture→SGR theme, and caches the result. The SGR codes we pick are the ones render/ansi-code->fg already maps to theme fg slots (string/number/keyword/special/comment plus function→warning-fg and type→success-fg), so no painter change is needed and the escapes stay zero-width (column alignment on verbatim fences is untouched).

Fail-open: if the native pack isn't loadable (e.g. a bare unit-test JVM without the platform lib on the classpath) every entry point returns nil and the caller falls back to plain, uncolored text.

Tree-sitter syntax highlighting for TUI code fences.

The eval / code fences the model emits are colored by *parsing* them with the
bundled tree-sitter grammars — the SAME `com.blockether/tree-sitter-language-pack`
the structural editors use — and painting each node by the grammar's OWN
`highlights.scm` capture scheme. That makes the classification accurate (a `;`
inside a string is not a comment, a number inside a symbol is not a number)
and GENERAL: Clojure, Python, JavaScript, JSON, … every fence we render is
colored off each grammar's canonical highlight rules.

The heavy lifting — parse, a real tree-sitter query interpreter (field-scoped
captures, alternations, `#match?`/`#eq?` predicates), per-byte capture
labeling, ANSI run coalescing — lives in the pack's JVM-native `Highlighter`
(Java, on the hot path), NOT in Clojure. This namespace is a thin edge: it
maps a fence `:lang` to a grammar, hands the pack our capture→SGR theme, and
caches the result. The SGR codes we pick are the ones `render/ansi-code->fg`
already maps to theme fg slots (string/number/keyword/special/comment plus
function→warning-fg and type→success-fg), so no painter change is needed
and the escapes stay zero-width (column alignment on verbatim fences is
untouched).

Fail-open: if the native pack isn't loadable (e.g. a bare unit-test JVM
without the platform lib on the classpath) every entry point returns nil and
the caller falls back to plain, uncolored text.
raw docstring

com.blockether.vis.ext.channel-tui.input

Keyboard / paste / clipboard surface for the TUI channel.

Clipboard read/write goes through OS shell helpers only: pbcopy / pbpaste on macOS (always present since 10.0), wl-copy / wl-paste on Wayland, xclip / xsel on X11.

Keyboard / paste / clipboard surface for the TUI channel.

Clipboard read/write goes through OS shell helpers only: `pbcopy` /
`pbpaste` on macOS (always present since 10.0), `wl-copy` /
`wl-paste` on Wayland, `xclip` / `xsel` on X11.
raw docstring

com.blockether.vis.ext.channel-tui.keymap

Single source of truth for the TUI's keyboard shortcuts.

Shortcuts are CTRL chords, identical on every platform (macOS / Windows / Linux). The old Alt/Option chords were removed: stock macOS terminals send Option+letter as a special character, never an Alt modifier, so those chords silently did nothing. Ctrl always reaches the app.

ONE registry, ONE exception: every vis-side chord is defined HERE — verbs in bindings, structural keys (palette / help / quit / picker-reorder) as the constants below. The SOLE exception is the Emacs EDITING chords (C-a/C-e/C-b/C-f/C-p/C-n/C-k/C-u/C-w/C-d), which live in lanterna's TextEditKeymap so they work in EVERY input (prompt + dialogs); they are never re-declared here, and no vis chord may reuse one of those letters (enforced by keymap-test).

Chord labels use ONE format everywhere: Emacs notation, lower-cased — C-c, C-g, C-l, and C-x <key> (plain second key) for the prefixed vis commands (e.g. C-x m, C-x s). chord / label-for / palette-chord all emit this shape, so a hint never drifts between Ctrl+X and C-x.

Tiers:

  • palette-chord (C-x p) opens the searchable command palette, which can run EVERY app verb. It is the discoverable entry point. (A PLAIN second key after C-x always reaches the app — unlike Ctrl+S/Ctrl+M — so the palette is C-x p, not C-x C-p.)
  • prefix-commands are the C-x <key> plain-second-key shortcuts for the named verbs (model, reasoning, length, search, attach, voice, dirs, resources, help) — the C-x prefix keeps them off the editing letters, and a PLAIN second key keeps them clear of unusable Ctrl bytes (Ctrl+S=flow control, Ctrl+M=Enter).
  • bindings (direct Ctrl chords) is EMPTY: every Ctrl letter is an Emacs editing key, so vis verbs all live behind the C-x prefix or in the palette.

Every surface — the input dispatcher, the pickers, footer hints, the help overlay, the clickable header chips — reads this namespace, so a shortcut is defined once and stays in sync.

Single source of truth for the TUI's keyboard shortcuts.

Shortcuts are CTRL chords, identical on every platform (macOS / Windows /
Linux). The old Alt/Option chords were removed: stock macOS terminals send
Option+letter as a special character, never an Alt modifier, so those chords
silently did nothing. Ctrl always reaches the app.

ONE registry, ONE exception: every vis-side chord is defined HERE — verbs in
`bindings`, structural keys (palette / help / quit / picker-reorder) as the
constants below. The SOLE exception is the Emacs EDITING chords
(C-a/C-e/C-b/C-f/C-p/C-n/C-k/C-u/C-w/C-d), which live in lanterna's
`TextEditKeymap` so they work in EVERY input (prompt + dialogs); they are
never re-declared here, and no vis chord may reuse one of those letters
(enforced by `keymap-test`).

Chord labels use ONE format everywhere: Emacs notation, lower-cased —
`C-c`, `C-g`, `C-l`, and `C-x <key>` (plain second key) for the prefixed vis
commands (e.g. `C-x m`, `C-x s`). `chord` / `label-for` / `palette-chord` all
emit this shape, so a hint never drifts between `Ctrl+X` and `C-x`.

Tiers:
- `palette-chord` (C-x p) opens the searchable command palette, which
  can run EVERY app verb. It is the discoverable entry point. (A PLAIN
  second key after C-x always reaches the app — unlike Ctrl+S/Ctrl+M — so
  the palette is `C-x p`, not `C-x C-p`.)
- `prefix-commands` are the `C-x <key>` plain-second-key shortcuts for the
  named verbs (model, reasoning, length, search, attach, voice, dirs,
  resources, help) — the C-x prefix keeps them off the editing letters, and a
  PLAIN second key keeps them clear of unusable Ctrl bytes (Ctrl+S=flow
  control, Ctrl+M=Enter).
- `bindings` (direct Ctrl chords) is EMPTY: every Ctrl letter is an Emacs
  editing key, so vis verbs all live behind the C-x prefix or in the palette.

Every surface — the input dispatcher, the pickers, footer hints, the help
overlay, the clickable header chips — reads this namespace, so a shortcut is
defined once and stays in sync.
raw docstring

com.blockether.vis.ext.channel-tui.limits-fmt

TUI aliases for the channel-neutral limits row formatters.

The real implementation lives in com.blockether.vis.internal.limits-format (hoisted from here so the web channel renders the SAME compact quota summaries on its provider cards). This namespace keeps the TUI-local require sites (footer.clj, provider.clj) and the existing test suite stable.

TUI aliases for the channel-neutral limits row formatters.

The real implementation lives in
`com.blockether.vis.internal.limits-format` (hoisted from here so
the web channel renders the SAME compact quota summaries on its
provider cards). This namespace keeps the TUI-local require sites
(`footer.clj`, `provider.clj`) and the existing test suite stable.
raw docstring

com.blockether.vis.ext.channel-tui.primitives

Low-level drawing primitives wrapping Lanterna TextGraphics. All rendering code should use these instead of raw Java interop.

Width-math contract

String width here means display columns the terminal will paint, NOT Java .length() and NOT (count s). Those count UTF-16 code units; a single emoji like 📁 is two code units but two columns, a CJK glyph like 日 is one code unit but two columns, a flag sequence 🇵🇱 is four code units, one grapheme, two columns. Mix the two and your alignment drifts.

Use display-width and truncate-cols for any string that could contain non-ASCII. Plain count/subs are still allowed for ASCII internals (box-drawing strings we authored, single-glyph keystroke labels, etc.) where the answer is identical and the call is a hot path.

Low-level drawing primitives wrapping Lanterna TextGraphics.
All rendering code should use these instead of raw Java interop.

Width-math contract
===================

String width here means **display columns the terminal will paint**,
NOT Java `.length()` and NOT (count s). Those count UTF-16 code units;
a single emoji like 📁 is two code units but two columns,
a CJK glyph like 日 is one code unit but two columns, a flag
sequence 🇵🇱 is four code units, one grapheme,
two columns. Mix the two and your alignment drifts.

Use `display-width` and `truncate-cols` for any string that could
contain non-ASCII. Plain `count`/`subs` are still allowed for ASCII
internals (box-drawing strings we authored, single-glyph keystroke
labels, etc.) where the answer is identical and the call is a hot path.
raw docstring

com.blockether.vis.ext.channel-tui.provider

TUI provider management dialogs - model picker, model manager, provider router. Config I/O and data helpers live in tui/config.clj.

The channel-neutral brain — status probing, limits, live model catalogs, presets, persistence shapes — lives in com.blockether.vis.internal.providers (exposed through vis.core) and is SHARED with the web channel. This namespace owns only the lanterna interaction layer.

GitHub Copilot OAuth: a hard dep. The TUI ships with the vis-provider-github-copilot jar on its classpath; the device-flow fns are required directly. (The previous dynaload indirection has been removed: explicit beats clever.)

TUI provider management dialogs - model picker, model manager, provider router.
Config I/O and data helpers live in tui/config.clj.

The channel-neutral brain — status probing, limits, live model
catalogs, presets, persistence shapes — lives in
`com.blockether.vis.internal.providers` (exposed through `vis.core`)
and is SHARED with the web channel. This namespace owns only the
lanterna interaction layer.

GitHub Copilot OAuth: a hard dep. The TUI ships with the
`vis-provider-github-copilot` jar on its classpath; the device-flow
fns are required directly. (The previous `dynaload` indirection has
been removed: explicit beats clever.)
raw docstring

com.blockether.vis.ext.channel-tui.render-ir

TUI walker over canonical answer-IR (com.blockether.vis.internal.render/->ast).

Pure data: IR -> vector of lines. Each line is a vector of styled runs. The screen layer turns runs into ANSI / Lanterna cells.

Why a dedicated walker (and not ir/render :plain):

  • We need styling metadata per character run (bold / italic / inline code background / link target) so the TUI can paint colours and attach click + selection regions.
  • We need word-wrap at an arbitrary terminal width with hanging indent inside lists and quotes.
  • We need stable per-node identifiers so selection / hover / click can map a glyph back to its IR node.

Canonical-IR invariants we rely on (see internal.render docstring):

  • text only inside :span / raw bodies of :code / :c / :kbd;
  • no '\n' inside :span;
  • hard breaks are explicit [:br {}];
  • attrs map present on every vector node.

Run shape: {:text String ; never empty, never contains '\n' :style #{:bold :italic :code :dim :link :heading :marker :quote} :href String? ; present iff :link in style :node any?} ; opaque node identity for click/select

Public API: (ir->lines ir width) ; total walker (ir->lines ir width opts) Opts: :heading-prefix? bool ; render '#'-style markers (default false; bold suffices) :code-fence? bool ; render ``` lines around code blocks (default false) :max-lines int ; hard cap (default unlimited)

TUI walker over canonical answer-IR (`com.blockether.vis.internal.render/->ast`).

Pure data: IR -> vector of lines. Each line is a vector of styled
runs. The screen layer turns runs into ANSI / Lanterna cells.

Why a dedicated walker (and not `ir/render :plain`):
- We need styling metadata per character run (bold / italic / inline
  code background / link target) so the TUI can paint colours and
  attach click + selection regions.
- We need word-wrap at an arbitrary terminal width with hanging
  indent inside lists and quotes.
- We need stable per-node identifiers so selection / hover / click
  can map a glyph back to its IR node.

Canonical-IR invariants we rely on (see internal.render docstring):
- text only inside `:span` / raw bodies of `:code` / `:c` / `:kbd`;
- no '\n' inside `:span`;
- hard breaks are explicit `[:br {}]`;
- attrs map present on every vector node.

Run shape:
  {:text   String          ; never empty, never contains '\n'
   :style  #{:bold :italic :code :dim :link :heading :marker :quote}
   :href   String?         ; present iff :link in style
   :node   any?}           ; opaque node identity for click/select

Public API:
  (ir->lines ir width)             ; total walker
  (ir->lines ir width opts)
Opts:
  :heading-prefix? bool   ; render '#'-style markers (default false; bold suffices)
  :code-fence?     bool   ; render ``` lines around code blocks (default false)
  :max-lines       int    ; hard cap (default unlimited)
raw docstring

com.blockether.vis.ext.channel-tui.scroll

Messages-area scroll state — one tagged value, no scattered flags.

The old model smeared scroll across four fields with two different storage scopes (:messages-scroll workspace-local; the animation target + two intent booleans top-level), plus a nil-vs-concrete overload on :messages-scroll. Every handler had to hand-maintain their consistency, and forgetting one dissoc produced the "flash to the top, then scroll to the bottom" jump.

This namespace replaces all of it with ONE workspace-local value, :scroll, that is a tagged variant:

{:mode :follow} ;; stick to the bottom (auto-track) {:mode :follow, :pos p} ;; …easing down toward the bottom {:mode :at, :offset n} ;; parked at row n (user scrolled up) {:mode :at, :offset n, :pos p} ;; …easing toward row n

Two orthogonal concerns, cleanly separated:

  • INTENT — :mode (+ :offset). :follow means "desired = bottom"; :at means "desired = offset". The user's scroll-up intent is just :at — no separate :scroll-pinned-up? flag.
  • DISPLAY — :pos, the eased on-screen row. Present only while an animation is in flight (and held at the bottom in :follow so the NEXT content growth has somewhere to ease FROM instead of teleporting). Absent ⇒ snapped exactly to desired.

Every transition REPLACES the whole value, so nothing can dangle. ease is called once per render frame; in :follow mode the desired row simply IS the growing bottom, so streaming content eases in for free — there is no special "animated follow" path any more.

Pure: no re-frame, no atoms. state.clj events are thin wrappers and screen.clj reads layout-offset / animating? to drive the loop.

Messages-area scroll state — one tagged value, no scattered flags.

The old model smeared scroll across four fields with two different
storage scopes (`:messages-scroll` workspace-local; the animation
target + two intent booleans top-level), plus a `nil`-vs-concrete
overload on `:messages-scroll`. Every handler had to hand-maintain
their consistency, and forgetting one `dissoc` produced the
"flash to the top, then scroll to the bottom" jump.

This namespace replaces all of it with ONE workspace-local value,
`:scroll`, that is a tagged variant:

  {:mode :follow}                ;; stick to the bottom (auto-track)
  {:mode :follow, :pos p}        ;;   …easing down toward the bottom
  {:mode :at, :offset n}         ;; parked at row n (user scrolled up)
  {:mode :at, :offset n, :pos p} ;;   …easing toward row n

Two orthogonal concerns, cleanly separated:

- INTENT  — `:mode` (+ `:offset`). `:follow` means "desired = bottom";
  `:at` means "desired = offset". The user's scroll-up intent is just
  `:at` — no separate `:scroll-pinned-up?` flag.
- DISPLAY — `:pos`, the eased on-screen row. Present only while an
  animation is in flight (and held at the bottom in `:follow` so the
  NEXT content growth has somewhere to ease FROM instead of
  teleporting). Absent ⇒ snapped exactly to desired.

Every transition REPLACES the whole value, so nothing can dangle.
`ease` is called once per render frame; in `:follow` mode the desired
row simply IS the growing bottom, so streaming content eases in for
free — there is no special "animated follow" path any more.

Pure: no re-frame, no atoms. `state.clj` events are thin wrappers and
`screen.clj` reads `layout-offset` / `animating?` to drive the loop.
raw docstring

com.blockether.vis.ext.channel-tui.scrollbar

Single source of truth for vertical scrollbars in the TUI.

One namespace owns geometry, drawing, hit-test, wheel handling and click/drag → scroll math. EVERY scrollbar — chat messages, slash menu, settings, file picker, provider/model cards, sessions, every other dialog — goes through here. No more bespoke thumb math per call site, no more drift between painter and hit-test layers.

Why one cell tall thumbs

Terminal.app renders stacked U+2588 FULL BLOCK cells as a ladder of separate boxes when the window is tall, which reads as multiple scroll positions instead of one current-position marker. So the visible thumb is intentionally one row high; the thumb's row index inside the track encodes the scroll fraction.

Single source of truth for vertical scrollbars in the TUI.

One namespace owns geometry, drawing, hit-test, wheel handling and
click/drag → scroll math. EVERY scrollbar — chat messages, slash
menu, settings, file picker, provider/model cards, sessions, every
other dialog — goes through here. No more bespoke thumb math per
call site, no more drift between painter and hit-test layers.

Why one cell tall thumbs
------------------------
Terminal.app renders stacked U+2588 FULL BLOCK cells as a ladder of
separate boxes when the window is tall, which reads as multiple
scroll positions instead of one current-position marker. So the
visible thumb is intentionally one row high; the thumb's row index
inside the track encodes the scroll fraction.
raw docstring

com.blockether.vis.ext.channel-tui.selection

Pure helpers for app-side mouse text selection in the fullscreen TUI.

This is not native terminal selection. When mouse reporting is enabled the terminal sends drag/release events to Vis, so Vis tracks the visible cells, highlights the selected range, and copies that range on release.

Pure helpers for app-side mouse text selection in the fullscreen TUI.

This is not native terminal selection. When mouse reporting is enabled the
terminal sends drag/release events to Vis, so Vis tracks the visible cells,
highlights the selected range, and copies that range on release.
raw docstring

com.blockether.vis.ext.channel-tui.state

Re-frame-like state management for the TUI. Single app-db atom, pure event handlers, side effects via reg-fx.

Re-frame-like state management for the TUI.
Single app-db atom, pure event handlers, side effects via reg-fx.
raw docstring

com.blockether.vis.ext.channel-tui.table

Reusable fixed-width table primitives for TUI dialogs/pickers.

Reusable fixed-width table primitives for TUI dialogs/pickers.
raw docstring

com.blockether.vis.ext.channel-tui.tabs

Per-place tab-set persistence.

The set of open tabs is the thing you want back when you return to a place: launch vis in a directory, restore the tabs you had there. The sessions themselves already persist in the DB (keyed by session id); this only records, per launch directory, WHICH sessions are open as tabs, their order, and which one was active.

Stored as a small EDN sidecar under ~/.vis/tabs/<place>.edn — no schema, no migration, entirely owned by this channel. Shape:

{:active "<session-id>" ; the focused tab (or nil) :sessions ["<session-id>" …]} ; tab order, left to right

Per-place tab-set persistence.

The set of open tabs is the thing you want back when you return to a
place: launch vis in a directory, restore the tabs you had there. The
sessions themselves already persist in the DB (keyed by session id); this
only records, per launch directory, WHICH sessions are open as tabs, their
order, and which one was active.

Stored as a small EDN sidecar under `~/.vis/tabs/<place>.edn` — no schema,
no migration, entirely owned by this channel. Shape:

  {:active  "<session-id>"      ; the focused tab (or nil)
   :sessions ["<session-id>" …]} ; tab order, left to right
raw docstring

com.blockether.vis.ext.channel-tui.terminal-image

Inline terminal image rendering — Kitty graphics protocol / iTerm2 inline images.

The pixel-free logic (capability detection, intrinsic dimension sniffing, cell-box sizing, escape encoding, base64/PNG transcoding) now lives in the lanterna fork's Java class com.googlecode.lanterna.terminal.image.TerminalImage. This namespace is the thin Clojure adapter that keeps vis's map-shaped API ({:images …}, {:w :h}, {:cols :rows}) plus the attachment-aware paste probe, which reaches back into vis internals and so stays here.

The escape strings are emitted DIRECTLY to the tty AFTER Lanterna's delta refresh (the screen loop owns that), placed over rows the renderer reserved as blanks. Lanterna never sees the graphics bytes, so its cell diff stays intact and the image survives subsequent delta frames.

Inline terminal image rendering — Kitty graphics protocol / iTerm2 inline
images.

The pixel-free logic (capability detection, intrinsic dimension sniffing,
cell-box sizing, escape encoding, base64/PNG transcoding) now lives in the
lanterna fork's Java class
`com.googlecode.lanterna.terminal.image.TerminalImage`. This namespace is the
thin Clojure adapter that keeps vis's map-shaped API (`{:images …}`,
`{:w :h}`, `{:cols :rows}`) plus the attachment-aware paste probe, which
reaches back into vis internals and so stays here.

The escape strings are emitted DIRECTLY to the tty AFTER Lanterna's delta
refresh (the screen loop owns that), placed over rows the renderer
reserved as blanks. Lanterna never sees the graphics bytes, so its cell
diff stays intact and the image survives subsequent delta frames.
raw docstring

com.blockether.vis.ext.channel-tui.theme

Lanterna adapter for internal com.blockether.vis.internal.theme tokens.

Lanterna adapter for internal `com.blockether.vis.internal.theme` tokens.
raw docstring

com.blockether.vis.ext.channel-tui.virtual

Virtualized chat-panel layout - the React-window pattern for our Lanterna scrollback.

── Why this exists ──────────────────────────────────────────────────────── The pre-virtualisation render path projected (i.e. ran format-answer-with-thinking over) EVERY assistant message on EVERY frame, even ones scrolled far off-screen. For finalized bubbles the outer format-answer-with-thinking LRU made each per-frame call ~315 ns once warm, but cold-opening a long session paid the full ~500 ms format cost per big trace bubble before the FIRST frame ever made it to the terminal. Session 954bf315 (2 x ~500ms bubbles -> ~870 ms cold paint) was the live trigger: the screen stayed bg-fill until both big bubbles formatted, which read as "frozen TUI on open".

── The shape of the fix ───────────────────────────────────────────────────

  1. Cheap row-height ESTIMATE per message, derived from the trace shape (iteration count, code-form count, answer length) without touching the markdown tokenizer or the wrap-text engine.
  2. layout plans the next paint: estimates every message's height, pins the auto-bottom scroll position, and - only for messages whose viewport interval is non-empty - runs the FULL projection
    • real bubble-height measurement. Off-screen bubbles never trigger format-answer-with-thinking.
  3. The result carries the visible subset (with screen-row coordinates) plus a refined total-h and eff-scroll for scrollbar geometry. Callers paint visible directly.

── What this is NOT ───────────────────────────────────────────────────────

  • Not a windowing library - we already have the bubble painter. This namespace is pure layout planning.
  • Not a height-precision oracle - the estimates are intentionally rough (within ~2x). Off-screen accuracy only nudges the scrollbar thumb; visible bubbles always paint at their REAL height because they go through the full projection pass.
  • Not a cache - the cache layers live in render.clj (format-answer-with-thinking, bubble-height, format-answer-markdown). This namespace just decides which messages get to consult those caches each frame.

── Caveat: scrollbar drift on first scroll up ───────────────────────────── When the user scrolls UP into a region whose estimates were wrong, the next layout will re-project those messages and refine their heights. The scrollbar thumb may shift one row as the total-h sum corrects. Mirrors react-window's VariableSizeList behaviour; eye-acceptable in practice.

(set! *warn-on-reflection* true) and *unchecked-math* :warn-on-boxed clean across this namespace per the repo's GraalVM ratchet.

Virtualized chat-panel layout - the React-window pattern for our
Lanterna scrollback.

── Why this exists ────────────────────────────────────────────────────────
The pre-virtualisation render path projected (i.e. ran
`format-answer-with-thinking` over) EVERY assistant message on
EVERY frame, even ones scrolled far off-screen. For finalized
bubbles the outer `format-answer-with-thinking` LRU made each
per-frame call ~315 ns once warm, but cold-opening a long
session paid the full ~500 ms format cost per big trace
bubble before the FIRST frame ever made it to the terminal.
Session 954bf315 (2 x ~500ms bubbles -> ~870 ms cold paint)
was the live trigger: the screen stayed bg-fill until both big
bubbles formatted, which read as "frozen TUI on open".

── The shape of the fix ───────────────────────────────────────────────────
1. Cheap row-height ESTIMATE per message, derived from the trace
   shape (iteration count, code-form count, answer length) without
   touching the markdown tokenizer or the wrap-text engine.
2. `layout` plans the next paint: estimates every message's height,
   pins the auto-bottom scroll position, and - only for messages
   whose viewport interval is non-empty - runs the FULL projection
   + real `bubble-height` measurement. Off-screen bubbles never
   trigger `format-answer-with-thinking`.
3. The result carries the visible subset (with screen-row
   coordinates) plus a refined `total-h` and `eff-scroll` for
   scrollbar geometry. Callers paint `visible` directly.

── What this is NOT ───────────────────────────────────────────────────────
* Not a windowing library - we already have the bubble painter.
  This namespace is pure layout planning.
* Not a height-precision oracle - the estimates are intentionally
  rough (within ~2x). Off-screen accuracy only nudges the
  scrollbar thumb; visible bubbles always paint at their REAL
  height because they go through the full projection pass.
* Not a cache - the cache layers live in `render.clj`
  (`format-answer-with-thinking`, `bubble-height`,
  `format-answer-markdown`). This namespace just decides which
  messages get to consult those caches each frame.

── Caveat: scrollbar drift on first scroll up ─────────────────────────────
When the user scrolls UP into a region whose estimates were
wrong, the next layout will re-project those messages and refine
their heights. The scrollbar thumb may shift one row as the
total-h sum corrects. Mirrors `react-window`'s
`VariableSizeList` behaviour; eye-acceptable in practice.

`(set! *warn-on-reflection* true)` and `*unchecked-math*
:warn-on-boxed` clean across this namespace per the repo's GraalVM
ratchet.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close