(adaptive-content-height rows requested)Clamp a dialog's REQUESTED content height so the box sizes to its own content instead of the shared footprint.
nil requested -> the shared full-height footprint (default-content-height).
Spacious logo / welcome screens and long browsers opt in this way.min-adaptive-content-h and the terminal-bounded
dialog-max-height, so short dialogs are compact and long ones still scroll.Clamp a dialog's REQUESTED content height so the box sizes to its own content instead of the shared footprint. - `nil` requested -> the shared full-height footprint (`default-content-height`). Spacious logo / welcome screens and long browsers opt in this way. - a number -> clamped between `min-adaptive-content-h` and the terminal-bounded `dialog-max-height`, so short dialogs are compact and long ones still scroll.
(band-questions screen g {:keys [left inner-w hint-row text-w] :as region})Everything a band can ASK, bound to its own region once:
:read! one line of text on the hint row — [label] / [label opts]
:choose! WHICH one, single-key — [title choices], returns the :id
:confirm! y/n — [question]
:transient! ANOTHER transient over the SAME band region — [spec]
:read-option the :read-option a spec with OPTION items hands tr/run!
A transient that opens a transient, and a question asked on the hint row instead of in a window, is how magit asks a second thing without a second frame — every band in the TUI does both through this map.
Everything a band can ASK, bound to its own `region` once: `:read!` one line of text on the hint row — `[label]` / `[label opts]` `:choose!` WHICH one, single-key — `[title choices]`, returns the `:id` `:confirm!` y/n — `[question]` `:transient!` ANOTHER transient over the SAME band region — `[spec]` `:read-option` the `:read-option` a spec with OPTION items hands `tr/run!` A transient that opens a transient, and a question asked on the hint row instead of in a window, is how magit asks a second thing without a second frame — every band in the TUI does both through this map.
(choice-mark exclusive? checked?)The status glyph a choice row wears in front of its label. An EXCLUSIVE choice
takes the shared ●/○ pair the settings rows and the footer already speak — pick
one and the other drops; an INCLUSIVE one takes the [✓]/[ ] box — pick as
many as apply. One place, so "choose one" and "choose any" can never end up
looking alike.
The status glyph a choice row wears in front of its label. An EXCLUSIVE choice takes the shared ●/○ pair the settings rows and the footer already speak — pick one and the other drops; an INCLUSIVE one takes the `[✓]`/`[ ]` box — pick as many as apply. One place, so "choose one" and "choose any" can never end up looking alike.
(clear-screen! screen)Fill the entire screen with terminal background. Call before sub-dialogs to cleanly replace the current dialog (wizard step pattern).
Fill the entire screen with terminal background. Call before sub-dialogs to cleanly replace the current dialog (wizard step pattern).
(command-palette! screen)(command-palette! screen extra-commands)(command-palette! screen extra-commands ctx)Show the searchable command palette. Returns the FULL chosen command map
(so the caller's run-command! can read :id and any slash keys), or nil
on Esc. extra-commands are the engine slash roots appended after the
built-ins. Opened with C-x C-p (Emacs C-x prefix + Ctrl+P).
ctx ({:has-turns? bool}) gates context-only verbs via
palette-commands-for — without turns the Fork Session entries are not
listed at all.
Show the searchable command palette. Returns the FULL chosen command map
(so the caller's `run-command!` can read `:id` and any slash keys), or nil
on Esc. `extra-commands` are the engine slash roots appended after the
built-ins. Opened with C-x C-p (Emacs C-x prefix + Ctrl+P).
`ctx` (`{:has-turns? bool}`) gates context-only verbs via
[[palette-commands-for]] — without turns the Fork Session entries are not
listed at all.(confirm-dialog! screen title message)Show Y/N confirmation with side-by-side buttons. Returns true/false, nil on Esc.
Show Y/N confirmation with side-by-side buttons. Returns true/false, nil on Esc.
(copy-dialog! screen messages)Show copy dialog for chat messages. Space toggles, A toggles all, Enter copies selected, Esc cancels.
Show copy dialog for chat messages. Space toggles, A toggles all, Enter copies selected, Esc cancels.
(default-content-height rows)Shared content height every dialog uses, derived from rows.
Clamped to a common modal footprint so dialogs keep equal height.
Shared content height every dialog uses, derived from `rows`. Clamped to a common modal footprint so dialogs keep equal height.
(default-content-width cols)Shared content width every dialog uses, derived from cols. Clamped
between the theme's dialog min/max widths and bounded by the terminal so
the box never paints off-screen.
Shared content width every dialog uses, derived from `cols`. Clamped between the theme's dialog min/max widths and bounded by the terminal so the box never paints off-screen.
(dialog-bounds cols rows content-w content-h)Pure geometry twin of draw-dialog-chrome! (explicit width+height arity):
the box rectangle a content-w×content-h dialog occupies, computed WITHOUT
painting. Lets a component measure its full layout — and reconcile a scroll
window — before any drawing happens. Returns the SAME shape the chrome does
({:left :top :right :bottom :inner-w :inner-h}), from the same golden math.
Pure geometry twin of `draw-dialog-chrome!` (explicit width+height arity):
the box rectangle a `content-w`×`content-h` dialog occupies, computed WITHOUT
painting. Lets a component measure its full layout — and reconcile a scroll
window — before any drawing happens. Returns the SAME shape the chrome does
({:left :top :right :bottom :inner-w :inner-h}), from the same golden math.(dialog-layout bounds)(dialog-layout {:keys [top bottom]} content-count)Compute content area layout. When content-count is provided and smaller than
the available height, content is vertically centered within the frame.
Layout: border -> title bar -> top separator -> CONTENT -> bottom separator -> hint -> border.
Compute content area layout. When `content-count` is provided and smaller than the available height, content is vertically centered within the frame. Layout: border -> title bar -> top separator -> CONTENT -> bottom separator -> hint -> border.
(draft-transient! screen content-top draft-rows)(draft-transient! screen content-top draft-rows pressed)The DRAFT band: create, switch and abandon a draft workspace without ever
leaving the session. Returns the choice the screen executes — :trunk,
:draft, :new (with the typed :label and :clean?) or :abandon (with
:workspace-id and :reason) — or nil when the human backed out at any
step.
Creating, switching and abandoning are three separate keys: c/d fork a
draft and name it inline, s opens the switch band, k abandons one. No
step opens a window.
pressed is one of those commands named by a SLASH instead of by a key
(drafts/slash-band): /draft new IS d, already pressed. A command this
band does not offer right now (/draft resume with no drafts) opens the band
itself rather than firing something the human was never shown.
The DRAFT band: create, switch and abandon a draft workspace without ever leaving the session. Returns the choice the screen executes — `:trunk`, `:draft`, `:new` (with the typed `:label` and `:clean?`) or `:abandon` (with `:workspace-id` and `:reason`) — or nil when the human backed out at any step. Creating, switching and abandoning are three separate keys: `c`/`d` fork a draft and name it inline, `s` opens the switch band, `k` abandons one. No step opens a window. `pressed` is one of those commands named by a SLASH instead of by a key (`drafts/slash-band`): `/draft new` IS `d`, already pressed. A command this band does not offer right now (`/draft resume` with no drafts) opens the band itself rather than firing something the human was never shown.
(drain-modal-paste! screen)After a bracketed-paste START keystroke is seen, drain screen until
PASTE_END and return the pasted text (PUA markers stripped). Lets any
modal text input accept clipboard paste without re-implementing the
paste state machine. Returns "" on a clipboard that yields no chars.
After a bracketed-paste START keystroke is seen, drain `screen` until PASTE_END and return the pasted text (PUA markers stripped). Lets any modal text input accept clipboard paste without re-implementing the paste state machine. Returns "" on a clipboard that yields no chars.
(draw-checkbox-item! g left row inner-w selected? checked? label)MULTI-choice LIST row — cursor glyph, a [✓]/[ ] box, then the label. The
cursor glyph and the checkbox glyph carry independent meaning: the first says
"this row is the cursor", the second says "this option is currently on".
Anchored at (inc left) so the marker sits right at the dialog's inner edge
(see draw-list-item!).
MULTI-choice LIST row — cursor glyph, a `[✓]`/`[ ]` box, then the label. The cursor glyph and the checkbox glyph carry independent meaning: the first says "this row is the cursor", the second says "this option is currently on". Anchored at `(inc left)` so the marker sits right at the dialog's inner edge (see `draw-list-item!`).
(draw-dialog-chrome! g cols rows title content-h)(draw-dialog-chrome! g cols rows title content-w content-h)Draw dialog background, shadow, border, and title.
Three arities:
(g cols rows title content-h) - shared default width; the box HEIGHT is
sized to content-h via adaptive-content-height. Pass nil as
content-h for the shared full-height footprint.(g cols rows title content-w content-h) - fully explicit. Use
only when a dialog genuinely needs a non-default width.Returns {:left :top :right :bottom :inner-w :inner-h}.
Draw dialog background, shadow, border, and title.
Three arities:
- `(g cols rows title content-h)` - shared default width; the box HEIGHT is
sized to `content-h` via `adaptive-content-height`. Pass `nil` as
`content-h` for the shared full-height footprint.
- `(g cols rows title content-w content-h)` - fully explicit. Use
only when a dialog genuinely needs a non-default width.
Returns {:left :top :right :bottom :inner-w :inner-h}.(draw-dialog-close-button! g box-right title-row)Paint a clickable X close button at a dialog's top-right title row and
record its click bounds (thread-local) so read-modal-input! can turn a
click into Escape. Every dialog inherits it via draw-dialog-chrome!.
Lights up to the red pill (close-button-hover-fg + bold) when the
thread-local close-hover flag is set - the same affordance the header and
help/tasks overlay close buttons use - so modal X buttons are no longer
static.
Paint a clickable X close button at a dialog's top-right title row and record its click bounds (thread-local) so `read-modal-input!` can turn a click into Escape. Every dialog inherits it via `draw-dialog-chrome!`. Lights up to the red pill (`close-button-hover-fg` + bold) when the thread-local close-hover flag is set - the same affordance the header and help/tasks overlay close buttons use - so modal X buttons are no longer static.
(draw-field-row! g left row inner-w focused? content)TYPED row — the painter for a form row text is entered into: a line, a
password, an OTP's boxes. An input is drawn as an INPUT: input-field-bg,
padded a space each side, the very control components/find-bar! paints its
query box with — so an empty field is still visibly a field and every place
the TUI takes typing is the same object. It starts at the dialog's own inner
edge, directly under its label.
Focus is the other half, and it is said three ways at once: the focused field
wears the accent ring ▎ down its left edge, keeps the full field surface, and
takes the ink (box-fg, bold). A field the keyboard is NOT in loses the ring,
recedes to theme/field-resting-bg and dims to dialog-hint. That contrast IS
the cursor in a form — there is no • gutter, because a marker in front of
every row says the same thing about all of them.
A TOGGLE is not typed into and does not wear this paper: see
draw-toggle-row!.
content is the field's already-rendered text (ada@example.com,
[1] [2] [ ]). Returns the column its first cell landed on, so a caller that
owns the terminal cursor can place it.
TYPED row — the painter for a form row text is entered into: a line, a password, an OTP's boxes. An input is drawn as an INPUT: `input-field-bg`, padded a space each side, the very control `components/find-bar!` paints its query box with — so an empty field is still visibly a field and every place the TUI takes typing is the same object. It starts at the dialog's own inner edge, directly under its label. Focus is the other half, and it is said three ways at once: the focused field wears the accent ring `▎` down its left edge, keeps the full field surface, and takes the ink (`box-fg`, bold). A field the keyboard is NOT in loses the ring, recedes to `theme/field-resting-bg` and dims to `dialog-hint`. That contrast IS the cursor in a form — there is no `•` gutter, because a marker in front of every row says the same thing about all of them. A TOGGLE is not typed into and does not wear this paper: see [[draw-toggle-row!]]. `content` is the field's already-rendered text (`ada@example.com`, `[1] [2] [ ]`). Returns the column its first cell landed on, so a caller that owns the terminal cursor can place it.
(draw-flat-dialog-chrome! g cols rows title)Flat variant of draw-dialog-chrome!: no drop shadow, no accent title
stripe, no separators - one thin-bordered rect on the dialog background
with the title inline on the top border. Same default footprint and the
same bounds map as the boxed chrome, so dialog-layout works unchanged.
Flat variant of `draw-dialog-chrome!`: no drop shadow, no accent title stripe, no separators - one thin-bordered rect on the dialog background with the title inline on the top border. Same default footprint and the same bounds map as the boxed chrome, so `dialog-layout` works unchanged.
(draw-hint-bar! g left row inner-w hint)Draw hint bar. hint can be:
Hints are CENTERED (not full-width justified) so short hint sets read as one tidy line instead of being stretched ragged across the dialog. Examples: "simple hint" ["move" "select" "cancel"] [["Up/Dn" "move"] ["Enter" "select"] ["Esc" "cancel"]]
Draw hint bar. `hint` can be: - a string: rendered as-is, left-aligned - a vec of strings: centered, dim italic, joined with ' · ' - a vec of [key action] pairs: key bold, action dim italic, the whole run centered with thin ' · ' separators between pairs Hints are CENTERED (not full-width justified) so short hint sets read as one tidy line instead of being stretched ragged across the dialog. Examples: "simple hint" ["move" "select" "cancel"] [["Up/Dn" "move"] ["Enter" "select"] ["Esc" "cancel"]]
(draw-input-item! g left row inner-w focused? text cursor placeholder)A form's TYPED row: draw-field-row! plus what typing needs — the horizontal
scroll that keeps the cursor inside the field and the dim placeholder an
empty field shows. Returns the TerminalPosition the caller parks the
terminal cursor at.
A form's TYPED row: `draw-field-row!` plus what typing needs — the horizontal scroll that keeps the cursor inside the field and the dim `placeholder` an empty field shows. Returns the `TerminalPosition` the caller parks the terminal cursor at.
(draw-selectable-row! g left row inner-w selected? text)The ONE focusable-row painter for LIST dialogs: p/selection-prefix's cursor
glyph, the text, bold while the cursor is on it.
draw-checkbox-item! is this row with a status glyph in front of the label,
and any new focusable LIST row joins them here instead of inventing a second
way to look selected. A form is the other family: its rows are drawn as
INPUTS on their own surface (draw-field-row!) and wear no cursor glyph at
all, because a • in front of every row says the same thing about all of
them.
The ONE focusable-row painter for LIST dialogs: `p/selection-prefix`'s cursor glyph, the text, bold while the cursor is on it. `draw-checkbox-item!` is this row with a status glyph in front of the label, and any new focusable LIST row joins them here instead of inventing a second way to look selected. A form is the other family: its rows are drawn as INPUTS on their own surface (`draw-field-row!`) and wear no cursor glyph at all, because a `•` in front of every row says the same thing about all of them.
(draw-text-input-field! g left row inner-w text cursor)(draw-text-input-field! g left row inner-w text cursor placeholder)Borderless › text input row with an optional dim placeholder. Returns the
TerminalPosition the caller should park the terminal cursor at.
Borderless `› text` input row with an optional dim `placeholder`. Returns the `TerminalPosition` the caller should park the terminal cursor at.
(draw-toggle-row! g left row inner-w focused? content)TOGGLED row — an option of a :select, a checkbox, a slider's track. Exactly
the geometry of draw-field-row!, so a form's rows line up whatever they
are, but painted on the dialog's OWN paper: nothing is typed here, so there is
no input surface to fill. Paper that says "type here" under a row that cannot
take a character is a lie about what the keyboard will do.
Focus is then the accent ring ▎ and the bold ink alone, and the status glyph
(choice-mark) says what the toggle currently IS.
TOGGLED row — an option of a `:select`, a checkbox, a slider's track. Exactly the geometry of [[draw-field-row!]], so a form's rows line up whatever they are, but painted on the dialog's OWN paper: nothing is typed here, so there is no input surface to fill. Paper that says "type here" under a row that cannot take a character is a lie about what the keyboard will do. Focus is then the accent ring `▎` and the bold ink alone, and the status glyph ([[choice-mark]]) says what the toggle currently IS.
(ellipsize s max-w)Right-truncate s to max-w columns with a trailing ….
Thin delegate over the canonical p/ellipsize (lanterna-backed).
Right-truncate `s` to `max-w` columns with a trailing `…`. Thin delegate over the canonical `p/ellipsize` (lanterna-backed).
(embed-transient! screen g region spec)Run ONE transient (tr/run!) INSIDE a frame someone else owns — same box,
same hint row, no second window.
region is already in tr/run! geometry (:left, :inner-w, :hint-row,
:text-w, plus the optional :min-row floor and the :restore! snapshot a
host that pages bands of different heights hands in). Returns tr/run!'s
{:action :switches :options}, or nil on Esc.
This is THE seam between a Lanterna surface and the host-agnostic transient
component. Nothing else calls tr/run! with a transient-host.
Run ONE transient (`tr/run!`) INSIDE a frame someone else owns — same box,
same hint row, no second window.
`region` is already in `tr/run!` geometry (`:left`, `:inner-w`, `:hint-row`,
`:text-w`, plus the optional `:min-row` floor and the `:restore!` snapshot a
host that pages bands of different heights hands in). Returns `tr/run!`'s
`{:action :switches :options}`, or nil on Esc.
This is THE seam between a Lanterna surface and the host-agnostic transient
component. Nothing else calls `tr/run!` with a `transient-host`.(field-content-w inner-w)Columns a form field's TEXT gets on an inner-w-wide dialog row: the focus
ring and the field's own padding come off the top. Public because paint and
cursor placement have to measure the very same field.
Columns a form field's TEXT gets on an `inner-w`-wide dialog row: the focus ring and the field's own padding come off the top. Public because paint and cursor placement have to measure the very same field.
(filter-select-items items query)Apply the shared picker filter: case-insensitive substring matching, preserving
source order. Items may provide :search-text to include metadata beyond the
visible label; otherwise :label is the haystack. A blank query shows all.
Apply the shared picker filter: case-insensitive substring matching, preserving source order. Items may provide `:search-text` to include metadata beyond the visible label; otherwise `:label` is the haystack. A blank query shows all.
(fit-hint-pairs hint text-w)Longest prefix of [key action] hint pairs whose rendered width (with
' · ' separators) fits in text-w columns. put-str! clips to the
SCREEN, not the dialog box, so a footer wider than the content area must
drop whole trailing chords instead of painting across the border.
Longest prefix of `[key action]` hint pairs whose rendered width (with ' · ' separators) fits in `text-w` columns. `put-str!` clips to the SCREEN, not the dialog box, so a footer wider than the content area must drop whole trailing chords instead of painting across the border.
(footer-content-width cols hint)(footer-content-width cols hint min-content)Content width for an action-footer dialog: sized so the box is EXACTLY the
footer's natural width plus two columns of padding on each side, never
narrower than min-content (the widest content line) nor wider than the
terminal. The +2 supplies the extra pad beyond the single-column gutter
draw-dialog-chrome! already reserves inside the border, so a footer of
width W yields 2 blank columns between the frame and the hints on each side.
Content width for an action-footer dialog: sized so the box is EXACTLY the footer's natural width plus two columns of padding on each side, never narrower than `min-content` (the widest content line) nor wider than the terminal. The `+2` supplies the extra pad beyond the single-column gutter `draw-dialog-chrome!` already reserves inside the border, so a footer of width W yields 2 blank columns between the frame and the hints on each side.
(fork-turn-items turns)Rows for the fork-at-turn palette (searchable-select!), one per turn of the
current session (from db-list-session-turns), top-to-bottom. Each row's
:label is the turn's user message (whitespace-collapsed, truncated) and
:hint its ordinal tN; :turn-id carries the session_turn_soul id the
fork copies THROUGH — selecting a row forks the session keeping every turn up
to and INCLUDING it. Type to filter by message text.
Rows for the fork-at-turn palette (`searchable-select!`), one per turn of the current session (from `db-list-session-turns`), top-to-bottom. Each row's `:label` is the turn's user message (whitespace-collapsed, truncated) and `:hint` its ordinal `tN`; `:turn-id` carries the `session_turn_soul` id the fork copies THROUGH — selecting a row forks the session keeping every turn up to and INCLUDING it. Type to filter by message text.
(frame-restorer screen)Snapshot the screen's back buffer NOW and return (fn [] …) / (fn [from to])
that puts those rows back exactly as they look at this moment.
A magit band paints over the host's rows, and when a SHORTER band replaces a taller one the rows between them belong to the HOST again. Blanking them punched a hole in the settings list behind the popup — the whole point of a band is that the buffer it is about stays readable. The host is not repainted while a band flow runs, so restoring the snapshot is the only honest answer.
Returns nil when there is no screen: unit tests redefine every dialog away.
Snapshot the screen's back buffer NOW and return `(fn [] …)` / `(fn [from to])` that puts those rows back exactly as they look at this moment. A magit band paints over the host's rows, and when a SHORTER band replaces a taller one the rows between them belong to the HOST again. Blanking them punched a hole in the settings list behind the popup — the whole point of a band is that the buffer it is about stays readable. The host is not repainted while a band flow runs, so restoring the snapshot is the only honest answer. Returns nil when there is no screen: unit tests redefine every dialog away.
(hint-bar-width hint)Natural rendered width (chars) of a draw-hint-bar! hint — a plain string,
a vec of strings, or a vec of [key action] pairs — using the SAME segment
and separator math the hint bar paints with. Lets a dialog size its box to
the footer instead of a fixed terminal ratio.
Natural rendered width (chars) of a `draw-hint-bar!` hint — a plain string, a vec of strings, or a vec of `[key action]` pairs — using the SAME segment and separator math the hint bar paints with. Lets a dialog size its box to the footer instead of a fixed terminal ratio.
(list-dialog! screen title items opts)Reusable scrollable, selectable list dialog — the SINGLE implementation
behind select-dialog! (plain) and searchable-select! (type-to-filter).
Now a THIN driver: select-modal-component supplies the pure geometry /
scroll / key logic and run-modal! owns the loop. Returns the chosen item
map (the full map, so callers recover :id/slash keys), or nil on Esc.
items is a vec of maps with at least :label. opts:
:filter? type-to-filter on :label, case-insensitive (default false)
:placeholder query placeholder shown while the filter is empty
:enter-label hint-bar verb for Enter (default "select")
:height :content sizes the box to the item count (+ the query
field), capped; nil uses the shared (tall) footprint.
Reusable scrollable, selectable list dialog — the SINGLE implementation
behind `select-dialog!` (plain) and `searchable-select!` (type-to-filter).
Now a THIN driver: `select-modal-component` supplies the pure geometry /
scroll / key logic and `run-modal!` owns the loop. Returns the chosen item
map (the full map, so callers recover `:id`/slash keys), or nil on Esc.
`items` is a vec of maps with at least `:label`. opts:
:filter? type-to-filter on `:label`, case-insensitive (default false)
:placeholder query placeholder shown while the filter is empty
:enter-label hint-bar verb for Enter (default "select")
:height `:content` sizes the box to the item count (+ the query
field), capped; nil uses the shared (tall) footprint.(load-mcp-inventory!)Refresh the cached MCP inventory from the gateway. Never throws: a gateway that is down, or a rejected verb, becomes an inline row instead of yet another modal.
Refresh the cached MCP inventory from the gateway. Never throws: a gateway that is down, or a rejected verb, becomes an inline row instead of yet another modal.
(load-provider-inventory!)Refresh the cached provider fleet from config + gateway. Never throws: a gateway that is down becomes an inline row instead of yet another modal. The auth probes fan out, because one serial round trip per provider would stall the dialog for as long as the slowest provider takes.
Router selection is part of the fleet, not a separate lookup: each entry
carries whether it is the default or the fallback AND the model that choice
picked, so Settings can SHOW what d/f just did.
Refresh the cached provider fleet from config + gateway. Never throws: a gateway that is down becomes an inline row instead of yet another modal. The auth probes fan out, because one serial round trip per provider would stall the dialog for as long as the slowest provider takes. Router selection is part of the fleet, not a separate lookup: each entry carries whether it is the default or the fallback AND the model that choice picked, so Settings can SHOW what `d`/`f` just did.
(log-view-dialog! screen
title
lines
&
{:keys [refresh-fn tail? grammar] :or {grammar "bash"}})FULLSCREEN, syntax-highlighted log viewer — the whole terminal, edge to edge.
Unlike text-view-dialog! (a centered modal box) this owns the entire screen:
a title strip on the top row, the log body filling every row beneath it, and a
hint strip on the bottom row. Each line is colorized by parsing the WHOLE
buffer with tree-sitter (highlight/highlight, default the bash grammar) and
painting the resulting ANSI runs through render/paint-ansi-line! — the same
path that carries syntax color on the transcript's code fences. Fails open to
plain text when the native grammar pack isn't loadable.
Keys: ↑/↓ line, PgUp/PgDn page, Home/End jump, mouse-wheel scroll, r refresh
(when :refresh-fn), Enter/Esc close. Options:
r refresh so a live
buffer (e.g. background-shell logs) can be re-pulled in place.FULLSCREEN, syntax-highlighted log viewer — the whole terminal, edge to edge.
Unlike `text-view-dialog!` (a centered modal box) this owns the entire screen:
a title strip on the top row, the log body filling every row beneath it, and a
hint strip on the bottom row. Each line is colorized by parsing the WHOLE
buffer with tree-sitter (`highlight/highlight`, default the `bash` grammar) and
painting the resulting ANSI runs through `render/paint-ansi-line!` — the same
path that carries syntax color on the transcript's code fences. Fails open to
plain text when the native grammar pack isn't loadable.
Keys: ↑/↓ line, PgUp/PgDn page, Home/End jump, mouse-wheel scroll, `r` refresh
(when `:refresh-fn`), Enter/Esc close. Options:
- :refresh-fn thunk returning fresh lines — enables `r` refresh so a live
buffer (e.g. background-shell logs) can be re-pulled in place.
- :tail? start pinned to the newest line and re-follow the bottom on
refresh (log-tail behaviour); scrolling up releases the pin.
- :grammar tree-sitter grammar for coloring (default "bash"); nil = plain.
Returns nil after close.(magit-dialog! screen root-or-repos)Magit-style status buffer over the git CLI — the C-x g / footer-git modal.
root-or-repos is either ONE root (string/File — the classic single-repo
buffer) or a vector of repo entries {:root :label :draft?} from
magit/workspace-roots — the session's primary workspace root.
gets a Repository <label> — <path> header and its own full section stack;
for a DRAFT session the entries already point at the CLONES the session
edits, so the buffer shows the draft's git state, not the trunk's.
Sections: head/upstream facts, untracked, unmerged, unstaged, staged,
stashes and recent commits. TAB folds a file's diff peek open under
its row; RET visits it FULLSCREEN (a file's syntax-highlighted body, a
commit's or stash's full patch). Verbs mirror magit and route to the repo
UNDER THE CURSOR:
s/u stage/unstage the file or the whole section, S/U all, k
discard with a confirm, c commit/amend (message prompt), P push
(plain / -u / --force-with-lease), F pull, f fetch, b branch
(checkout / create / delete), z stash (push / pop / apply / drop), g
refresh, C-w copy the sha/path/ref under point, q/Esc close. Every verb
shells to the real git binary via
internal.git, and the buffer re-reads every repo after each action, so
what you see is always git status truth. Returns nil.
Magit-style status buffer over the git CLI — the C-x g / footer-git modal.
`root-or-repos` is either ONE root (string/File — the classic single-repo
buffer) or a vector of repo entries `{:root :label :draft?}` from
`magit/workspace-roots` — the session's primary workspace root.
gets a `Repository <label> — <path>` header and its own full section stack;
for a DRAFT session the entries already point at the CLONES the session
edits, so the buffer shows the draft's git state, not the trunk's.
Sections: head/upstream facts, untracked, unmerged, unstaged, staged,
stashes and recent commits. TAB folds a file's diff peek open under
its row; RET visits it FULLSCREEN (a file's syntax-highlighted body, a
commit's or stash's full patch). Verbs mirror magit and route to the repo
UNDER THE CURSOR:
`s`/`u` stage/unstage the file or the whole section, `S`/`U` all, `k`
discard with a confirm, `c` commit/amend (message prompt), `P` push
(plain / -u / --force-with-lease), `F` pull, `f` fetch, `b` branch
(checkout / create / delete), `z` stash (push / pop / apply / drop), `g`
refresh, `C-w` copy the sha/path/ref under point, `q`/Esc close. Every verb
shells to the real `git` binary via
`internal.git`, and the buffer re-reads every repo after each action, so
what you see is always `git status` truth. Returns nil.(magit-mini-read! screen
g
left
inner-w
hint-row
text-w
label
{:keys [initial mask]})Inline editable minibuffer painted over the magit hint-bar row:
<label> <text> with a live cursor. Enter submits the trimmed string (may
be empty), Esc returns nil. Opts: :initial (seed text), :mask (echo char).
Inline editable minibuffer painted over the magit hint-bar row: `<label> <text>` with a live cursor. Enter submits the trimmed string (may be empty), Esc returns nil. Opts: :initial (seed text), :mask (echo char).
(markdown-viewer-dialog! screen title md)Scrollable read-only MARKDOWN viewer: md is lifted to canonical IR
(vis/markdown->ast) and painted with styled headings, bold, and code
accents, tables — through the SAME IR walker the chat uses
(layout/ast->lines). The rich twin of text-viewer-dialog!.
Returns nil on Esc. Supports keyboard scrolling.
Scrollable read-only MARKDOWN viewer: `md` is lifted to canonical IR (`vis/markdown->ast`) and painted with styled headings, bold, and code accents, tables — through the SAME IR walker the chat uses (`layout/ast->lines`). The rich twin of `text-viewer-dialog!`. Returns nil on Esc. Supports keyboard scrolling.
(modal-close-click? key)True when key is a mouse click on the dialog close (✕) button.
True when `key` is a mouse click on the dialog close (✕) button.
(modal-input-pending? screen)True when another keystroke is ALREADY queued for this modal loop. The peeked
event is stashed in the same thread-local slot read-modal-input! drains, so
nothing is lost.
This is the TUI's DEBOUNCE primitive: an expensive per-keystroke effect (the
gateway transcript search) can skip itself while the user is still typing and
run once on the keystroke that lands in a pause — no threads, no timers, and
no repaint problem from an async result arriving while the loop blocks in
readInput.
True when another keystroke is ALREADY queued for this modal loop. The peeked event is stashed in the same thread-local slot `read-modal-input!` drains, so nothing is lost. This is the TUI's DEBOUNCE primitive: an expensive per-keystroke effect (the gateway transcript search) can skip itself while the user is still typing and run once on the keystroke that lands in a pause — no threads, no timers, and no repaint problem from an async result arriving while the loop blocks in `readInput`.
(modal-wheel-delta key)Return list-selection delta for a wheel mouse event, else nil. Negative moves up; positive moves down.
Return list-selection delta for a wheel mouse event, else nil. Negative moves up; positive moves down.
(modal-wheel-step key)Return wheel delta multiplied by any coalesced event count.
Return wheel delta multiplied by any coalesced event count.
(model-picker! screen current)Searchable per-session model picker — TUI parity with the web footer
chooser. Lists every configured model as a row (<provider> / <model>,
the active one marked ● current) plus a top ★ router default row
that CLEARS the per-session override. current is the session's stored
model preference ({:provider <str|kw> :model <str>}) or nil; it marks
the active row exactly like the web picker. Returns the chosen item map
— {:reset? true} for the router-default row, else {:provider <str> :model <str>} — or nil on Esc.
Searchable per-session model picker — TUI parity with the web footer
chooser. Lists every configured model as a row (`<provider> / <model>`,
the active one marked `● current`) plus a top `★ router default` row
that CLEARS the per-session override. `current` is the session's stored
model preference (`{:provider <str|kw> :model <str>}`) or nil; it marks
the active row exactly like the web picker. Returns the chosen item map
— `{:reset? true}` for the router-default row, else `{:provider <str>
:model <str>}` — or nil on Esc.(multi-select-dialog! screen title items)Checkbox multi-select over items (vec of strings). Space toggles the
cursor row, a toggles all, Enter confirms, Esc cancels. Returns the vec
of selected strings (possibly empty) on confirm, nil on Esc. Mirrors the
web modal's alias chips — same proposed options, multi-pick semantics.
Checkbox multi-select over `items` (vec of strings). Space toggles the cursor row, `a` toggles all, Enter confirms, Esc cancels. Returns the vec of selected strings (possibly empty) on confirm, nil on Esc. Mirrors the web modal's alias chips — same proposed options, multi-pick semantics.
(navigator-dialog! screen opts)Global C-g session picker. Full-width project/session hierarchy; transcript lookup is debounced and asynchronous so typing never waits on the gateway.
Global C-g session picker. Full-width project/session hierarchy; transcript lookup is debounced and asynchronous so typing never waits on the gateway.
(normalize-modal-key key)Normalize raw terminal CR/LF/ESC character keystrokes to Lanterna
Enter/Escape key types, and C-g to Escape. Some terminals surface modal
Enter/Escape as KeyType/Character; modal code should not need to
special-case that.
C-g (Emacs keyboard-quit) is rewritten here too, so EVERY dialog closes on
it through the KeyType/Escape branch it already has - one rewrite instead of
an abort clause per key loop.
Normalize raw terminal CR/LF/ESC character keystrokes to Lanterna Enter/Escape key types, and C-g to Escape. Some terminals surface modal Enter/Escape as `KeyType/Character`; modal code should not need to special-case that. C-g (Emacs `keyboard-quit`) is rewritten here too, so EVERY dialog closes on it through the `KeyType/Escape` branch it already has - one rewrite instead of an abort clause per key loop.
(open-nested! screen f)Run f with the screen cleared before AND after it, for a dialog opened
from inside another dialog.
Modals float over the CHAT on purpose, but a modal opened over another modal paints a SMALLER box on top of the parent's, so the parent's border, ✕ and hint bar keep framing it and the user sees two stacked popups. Erasing on the way in gives the nested flow a clean surface; erasing on the way out lets the caller repaint its own frame from scratch.
Run `f` with the screen cleared before AND after it, for a dialog opened from inside another dialog. Modals float over the CHAT on purpose, but a modal opened over another modal paints a SMALLER box on top of the parent's, so the parent's border, ✕ and hint bar keep framing it and the user sees two stacked popups. Erasing on the way in gives the nested flow a clean surface; erasing on the way out lets the caller repaint its own frame from scratch.
Command palette entries. Each is {:id keyword :label str}. The :id is the
action the screen's run-command! executes. Quit is intentionally NOT here
— use Ctrl+C to quit.
The palette is THE discoverable entry point for every app verb: opened with C-x p (reliable on every terminal, unlike Alt/Option chords on macOS) and filtered by typing.
Command palette entries. Each is {:id keyword :label str}. The `:id` is the
action the screen's `run-command!` executes. Quit is intentionally NOT here
— use Ctrl+C to quit.
The palette is THE discoverable entry point for every app verb: opened with
C-x p (reliable on every terminal, unlike Alt/Option chords on macOS) and
filtered by typing.(palette-commands-for {:keys [has-turns?]})palette-commands filtered to the entries that can ACT in ctx. Mirrors the
which-key strip's :show-when gating: an entry tagged :has-turns (both
Fork Session verbs) is DROPPED in a session with no turns — forking a
turnless session is prohibited, so it must not even be discoverable.
ctx is {:has-turns? bool}; a missing/nil ctx is the conservative
turnless case. Untagged entries always survive.
`palette-commands` filtered to the entries that can ACT in `ctx`. Mirrors the
which-key strip's `:show-when` gating: an entry tagged `:has-turns` (both
Fork Session verbs) is DROPPED in a session with no turns — forking a
turnless session is prohibited, so it must not even be discoverable.
`ctx` is `{:has-turns? bool}`; a missing/nil ctx is the conservative
turnless case. Untagged entries always survive.(read-modal-input! screen)Read one modal input event. Consecutive pending wheel events are drained
and returned as one :scroll-delta, so a wheel flood costs one redraw.
The first non-wheel event encountered while draining is held for the next
modal read on this thread. MOVE/DRAG events also refresh the close (X)
hover flag so the button can light up under the cursor.
Read one modal input event. Consecutive pending wheel events are drained and returned as one `:scroll-delta`, so a wheel flood costs one redraw. The first non-wheel event encountered while draining is held for the next modal read on this thread. MOVE/DRAG events also refresh the close (X) hover flag so the button can light up under the cursor.
(read-modal-key! screen)Like Screen/readInput, but drains wheel floods into one synthetic wheel
event. Existing modal loops can use it without bespoke scroll-delta code.
Like `Screen/readInput`, but drains wheel floods into one synthetic wheel event. Existing modal loops can use it without bespoke scroll-delta code.
(region-option-reader screen g {:keys [left inner-w hint-row text-w]})A :read-option for a transient EMBEDDED in someone else's frame.
transient-dialog! builds this for its own modal; a band painted into a host
region (Settings, the provider manager) needs the same minibuffer, on the
host's hint row, so an OPTION is typed without opening a second window.
A `:read-option` for a transient EMBEDDED in someone else's frame. `transient-dialog!` builds this for its own modal; a band painted into a host region (Settings, the provider manager) needs the same minibuffer, on the host's hint row, so an OPTION is typed without opening a second window.
(run-modal! screen {:keys [init measure reconcile paint on-key]})Shared modal driver — the ONE event loop every ported dialog reuses instead
of hand-rolling its own loop/recur. component is a map of PURE fns (they
never touch the screen) plus one impure paint fn:
:init immutable start state (a map), or a 0-arg fn returning it
:measure (fn [state cols rows] -> geom) — geometry, screen-free, TESTABLE
:reconcile (fn [state geom] -> state) — optional clamp (e.g. scroll window)
:paint (fn [g state geom] -> cursor|nil) — the only impure piece; draws to g
:on-key (fn [state key geom] -> state | {::done result}) — screen-free, TESTABLE
run-modal! owns everything the old dialogs copy-pasted: terminal sizing, the
TextGraphics, wheel/close/Esc normalization (via read-modal-key!), the
cursor + DELTA refresh, and the recur loop. A key handler returns the next
state to continue, or {::done v} to close the modal with value v (nil on
Esc/close). Because :measure/:reconcile/:on-key are pure functions of
data, a dialog's geometry and key logic can be unit-tested with no live
terminal at all — the React-like win.
Shared modal driver — the ONE event loop every ported dialog reuses instead
of hand-rolling its own `loop/recur`. `component` is a map of PURE fns (they
never touch the screen) plus one impure paint fn:
:init immutable start state (a map), or a 0-arg fn returning it
:measure (fn [state cols rows] -> geom) — geometry, screen-free, TESTABLE
:reconcile (fn [state geom] -> state) — optional clamp (e.g. scroll window)
:paint (fn [g state geom] -> cursor|nil) — the only impure piece; draws to `g`
:on-key (fn [state key geom] -> state | {::done result}) — screen-free, TESTABLE
run-modal! owns everything the old dialogs copy-pasted: terminal sizing, the
`TextGraphics`, wheel/close/Esc normalization (via `read-modal-key!`), the
cursor + DELTA refresh, and the recur loop. A key handler returns the next
state to continue, or `{::done v}` to close the modal with value `v` (nil on
Esc/close). Because `:measure`/`:reconcile`/`:on-key` are pure functions of
data, a dialog's geometry and key logic can be unit-tested with no live
terminal at all — the React-like win.(searchable-select! screen title items)(searchable-select! screen title items {:keys [placeholder enter-label]})Type-to-filter selection list — the searchable spine of the command palette.
Thin wrapper over list-dialog! (filter on, content-sized, palette
placeholder). Returns the FULL chosen item map (so callers recover
:id / slash keys), or nil on Esc.
The optional opts map overrides the filter field's :placeholder and the
:enter-label — so callers other than the command palette (e.g. the project
switcher) show a fitting prompt instead of "Type a command…".
Type-to-filter selection list — the searchable spine of the command palette. Thin wrapper over `list-dialog!` (filter on, content-sized, palette placeholder). Returns the FULL chosen item map (so callers recover `:id` / slash keys), or nil on Esc. The optional `opts` map overrides the filter field's `:placeholder` and the `:enter-label` — so callers other than the command palette (e.g. the project switcher) show a fitting prompt instead of "Type a command…".
(select-dialog! screen title items)Show a selection list dialog. Returns the selected item map or nil on Esc.
items is a vec of {:label str, …} maps. Thin wrapper over list-dialog!.
Show a selection list dialog. Returns the selected item map or nil on Esc.
`items` is a vec of `{:label str, …}` maps. Thin wrapper over `list-dialog!`.(select-modal-component title
items
{:keys [filter? placeholder enter-label height]})Build the run-modal! component behind list-dialog! — a scrollable,
selectable, optionally type-to-filter list. This is the pure-fn heart of the
dialog: its :measure (geometry), :reconcile (scroll window) and :on-key
(navigation / filtering / select) are plain functions of immutable state, so
they can be exercised in tests WITHOUT a terminal. Only :paint touches the
screen. items/opts match list-dialog!.
Build the `run-modal!` component behind `list-dialog!` — a scrollable, selectable, optionally type-to-filter list. This is the pure-fn heart of the dialog: its `:measure` (geometry), `:reconcile` (scroll window) and `:on-key` (navigation / filtering / select) are plain functions of immutable state, so they can be exercised in tests WITHOUT a terminal. Only `:paint` touches the screen. `items`/opts match `list-dialog!`.
(session-band! screen content-top spec f)(session-band! screen content-top spec f pressed)Run ONE transient as a magit BAND inside the LIVE SESSION frame — the same in-frame band the human-input form and the magit status buffer use, painted over the bottom of the transcript instead of in a window of its own.
content-top is the first row the band may touch (the screen's
[:layout :messages-top]), so the header and the session it is about stay
readable above it. The frame is snapshotted before the band paints and put
back on the way out — the transcript underneath is never repainted from
scratch and never blanked.
f is called with {:screen :g :region :result} ONLY when the transient
produced an action, on the band's own rows: that is where an inline
minibuffer (magit-mini-read!, magit-mini-choose!, magit-mini-confirm!)
asks its follow-up question, on the hint row, instead of opening a modal.
Returns f's value, or nil on Esc.
pressed is that action ALREADY chosen — a slash that names one command of
this band (/draft new) is exactly that key, pre-pressed, so the band paints
itself and goes straight to the question instead of waiting for a keystroke
the human already typed.
Run ONE transient as a magit BAND inside the LIVE SESSION frame — the same
in-frame band the human-input form and the magit status buffer use, painted
over the bottom of the transcript instead of in a window of its own.
`content-top` is the first row the band may touch (the screen's
`[:layout :messages-top]`), so the header and the session it is about stay
readable above it. The frame is snapshotted before the band paints and put
back on the way out — the transcript underneath is never repainted from
scratch and never blanked.
`f` is called with `{:screen :g :region :result}` ONLY when the transient
produced an action, on the band's own rows: that is where an inline
minibuffer (`magit-mini-read!`, `magit-mini-choose!`, `magit-mini-confirm!`)
asks its follow-up question, on the hint row, instead of opening a modal.
Returns `f`'s value, or nil on Esc.
`pressed` is that action ALREADY chosen — a slash that names one command of
this band (`/draft new`) is exactly that key, pre-pressed, so the band paints
itself and goes straight to the question instead of waiting for a keystroke
the human already typed.(session-dialog-items sessions active-id)(session-dialog-items sessions active-id body-w)Build table rows for existing sessions only. New/fork stay dialog options via the N/F shortcuts and command palette; they are not fake table data rows. Rows are sorted by Modified at desc, then Created at desc.
Build table rows for existing sessions only. New/fork stay dialog options via the N/F shortcuts and command palette; they are not fake table data rows. Rows are sorted by Modified at desc, then Created at desc.
(session-dialog-label session active-id body-w)Format one fixed-width session table row. Columns are intentionally stable so the picker reads as a table inside the shared dialog chrome.
Format one fixed-width session table row. Columns are intentionally stable so the picker reads as a table inside the shared dialog chrome.
(session-picker-dialog! screen sessions active-id)Show recent TUI sessions in a fixed-size table. Returns
{:action :new}, {:action :fork}, {:action :switch :id <session-id>},
or nil on Esc.
Show recent TUI sessions in a fixed-size table. Returns
`{:action :new}`, `{:action :fork}`, `{:action :switch :id <session-id>}`,
or nil on Esc.(settings-dialog! screen settings)(settings-dialog! screen settings callbacks)Show the settings dialog.
ONE flat, grouped, scrollable list (mirrors the web settings modal), laid out VS Code-style: a left Table-of-Contents sidebar rail lists the sections with per-section counts and highlights the one owning the selection, while the right pane shows the settings themselves. Toggle rows render a leading status glyph; choice rows cycle their value with Enter; action rows invoke a callback. The rail is a passive locator — arrow keys still move through the right pane and the rail tracks where you are.
settings is the persisted TUI settings map (see
state/default-settings). callbacks also carries :focus-section (a
section label to park the cursor on, e.g. MCP Servers or Providers),
:mcp-add / :provider-add (the add row of each section), :mcp-action
(the verb a server's transient fired) and :provider-transient (one
provider's transient, handed the graphics and the region it paints into).
Esc clears an active search first, then closes and returns the
current settings map.
Show the settings dialog. ONE flat, grouped, scrollable list (mirrors the web settings modal), laid out VS Code-style: a left Table-of-Contents sidebar rail lists the sections with per-section counts and highlights the one owning the selection, while the right pane shows the settings themselves. Toggle rows render a leading status glyph; choice rows cycle their value with Enter; action rows invoke a callback. The rail is a passive locator — arrow keys still move through the right pane and the rail tracks where you are. `settings` is the persisted TUI settings map (see `state/default-settings`). `callbacks` also carries `:focus-section` (a section label to park the cursor on, e.g. `MCP Servers` or `Providers`), `:mcp-add` / `:provider-add` (the add row of each section), `:mcp-action` (the verb a server's transient fired) and `:provider-transient` (one provider's transient, handed the graphics and the region it paints into). Esc clears an active search first, then closes and returns the current settings map.
(start-in-transient! screen content-top)Ask WHERE a new session starts, as a band in the current session's frame:
t the project itself, c a draft forked from the committed HEAD, d a
draft carrying the uncommitted working tree. A draft is named inline on the
hint row.
Returns {:start-in :trunk}, {:start-in :draft :clean? bool :draft {:label :clean?}},
or nil.
Ask WHERE a new session starts, as a band in the current session's frame:
`t` the project itself, `c` a draft forked from the committed HEAD, `d` a
draft carrying the uncommitted working tree. A draft is named inline on the
hint row.
Returns `{:start-in :trunk}`, `{:start-in :draft :clean? bool :draft {:label :clean?}}`,
or nil.(table-modal-component title grid)Pure run-modal! component behind table-view-dialog! — the spreadsheet view
of a vis-table artifact. grid is table/parse-csv output (first row is the
header). Paging, sorting, geometry and the key map are plain functions of
immutable state, so the whole viewer is testable with no terminal; only
:paint touches the screen.
The sheet is PAGED, not scrolled: the window always starts on a page boundary
(table/page-start), so a row never straddles two screens and the title says
which page of how many you are on.
Keys: ↑/↓ pick a row, PgUp/PgDn turn a whole page, ←/→ pick a column, Enter sorts by that column (toggling ascending/descending), Tab yields the row, Esc closes.
Pure `run-modal!` component behind `table-view-dialog!` — the spreadsheet view of a `vis-table` artifact. `grid` is `table/parse-csv` output (first row is the header). Paging, sorting, geometry and the key map are plain functions of immutable state, so the whole viewer is testable with no terminal; only `:paint` touches the screen. The sheet is PAGED, not scrolled: the window always starts on a page boundary (`table/page-start`), so a row never straddles two screens and the title says which page of how many you are on. Keys: ↑/↓ pick a row, PgUp/PgDn turn a whole page, ←/→ pick a column, Enter sorts by that column (toggling ascending/descending), Tab yields the row, Esc closes.
(table-view-dialog! screen tbl)Open a vis-table artifact — the CSV/TSV fence vis_attach writes — as a live
spreadsheet: PgUp/PgDn to turn a page, ↑/↓ and ←/→ to move the row /
column cursor, Enter to sort by the current column. tbl is the click region's
:table payload ({:name :csv :cols :rows :title}). Returns nil, or the
selected row on Tab.
Open a `vis-table` artifact — the CSV/TSV fence `vis_attach` writes — as a live
spreadsheet: PgUp/PgDn to turn a page, ↑/↓ and ←/→ to move the row /
column cursor, Enter to sort by the current column. `tbl` is the click region's
`:table` payload (`{:name :csv :cols :rows :title}`). Returns nil, or the
selected row on Tab.(text-input-dialog! screen
title
label
&
{:keys [mask initial body flat?] :or {initial ""}})Show a text input dialog. Returns string or nil on Esc. Options: :mask char (e.g. * for passwords), :initial string, :body string-or-lines rendered above the input label, :flat? true selects the minimal inline-border chrome.
Show a text input dialog. Returns string or nil on Esc. Options: :mask char (e.g. \* for passwords), :initial string, :body string-or-lines rendered above the input label, :flat? true selects the minimal inline-border chrome.
(text-view-dialog! screen title lines & {:keys [refresh-fn tail?]})Show read-only lines in a scrollable modal. Returns nil after close.
Keys: ↑/↓ line, PgUp/PgDn page, Home/End jump, mouse-wheel scroll, Enter/Esc close. Options:
Show read-only lines in a scrollable modal. Returns nil after close.
Keys: ↑/↓ line, PgUp/PgDn page, Home/End jump, mouse-wheel scroll,
Enter/Esc close. Options:
- :refresh-fn thunk returning fresh lines — enables [r] refresh so a live
buffer (e.g. background logs) can be re-pulled in place.
- :tail? start pinned to the newest line and re-follow the bottom on
refresh (log-tail behaviour); scrolling up releases the pin.(text-viewer-dialog! screen title text)Show a scrollable read-only text viewer dialog.
title is the dialog header. text is a string (may contain newlines)
that is rendered VERBATIM - same content the LLM receives, only soft-
wrapped to fit the dialog width. No markdown, no truncation, no
reformatting.
Returns nil on Esc. Supports keyboard scrolling.
Show a scrollable read-only text viewer dialog. `title` is the dialog header. `text` is a string (may contain newlines) that is rendered VERBATIM - same content the LLM receives, only soft- wrapped to fit the dialog width. No markdown, no truncation, no reformatting. Returns nil on Esc. Supports keyboard scrolling.
(transient-dialog! screen title body spec)Host ONE magit transient in its OWN modal — the popup for flows that have no
status buffer to sit in (provider authentication). body (a string or lines)
is the caller's guidance, painted once at the top of the content area; the
transient owns every row under it and its hint bar lands on the dialog's own
hint row. The box is sized to what it actually holds, so a two-line prompt
opens a small dialog instead of a half-screen one.
OPTION items are read INLINE on that hint row (magit-mini-read!), honouring
the item's :prompt (default <label>:) and :mask (\* for a credential);
mark such an item :secret? true and its value renders as dots, never as
text. spec may carry a :title for the popup itself when the frame's title
would read redundantly.
Returns tr/run!'s {:action :switches :options}, or nil on Esc.
Host ONE magit transient in its OWN modal — the popup for flows that have no
status buffer to sit in (provider authentication). `body` (a string or lines)
is the caller's guidance, painted once at the top of the content area; the
transient owns every row under it and its hint bar lands on the dialog's own
hint row. The box is sized to what it actually holds, so a two-line prompt
opens a small dialog instead of a half-screen one.
OPTION items are read INLINE on that hint row (`magit-mini-read!`), honouring
the item's `:prompt` (default `<label>:`) and `:mask` (`\*` for a credential);
mark such an item `:secret? true` and its value renders as dots, never as
text. `spec` may carry a `:title` for the popup itself when the frame's title
would read redundantly.
Returns `tr/run!`'s `{:action :switches :options}`, or nil on Esc.(transient-host screen g)The standard modal HOST for tr/run! — the one adapter between a Lanterna
screen and the host-agnostic transient component. It paints through g,
flushes with the modal cursor hidden, borrows this namespace's hint bar, and
normalizes one modal keystroke into what the component understands: :esc,
a Character, or nil for "nothing actionable, just repaint".
Any surface holding a screen and a TextGraphics embeds a transient with
this — the magit status buffer, the provider dialog, transient-dialog!.
The standard modal HOST for `tr/run!` — the one adapter between a Lanterna `screen` and the host-agnostic transient component. It paints through `g`, flushes with the modal cursor hidden, borrows this namespace's hint bar, and normalizes one modal keystroke into what the component understands: `:esc`, a Character, or nil for "nothing actionable, just repaint". Any surface holding a screen and a `TextGraphics` embeds a transient with this — the magit status buffer, the provider dialog, `transient-dialog!`.
(update-modal-close-hover! key)On a MOVE/DRAG event, set the thread-local close-hover flag when the cursor is within the recorded close-button bounds, clear it otherwise. Lets the modal close (X) button light up on hover like the header/overlay buttons.
On a MOVE/DRAG event, set the thread-local close-hover flag when the cursor is within the recorded close-button bounds, clear it otherwise. Lets the modal close (X) button light up on hover like the header/overlay buttons.
(with-frame-restored! screen f)Run f — typically a nested dialog or a full-screen prompt — and put THIS
frame back exactly as it was.
open-nested! erases the screen on the way OUT, which is right when the
caller repaints itself afterwards and catastrophic in the middle of a magit
band flow: the next band lands on blank paper and everything above it is
gone. Snapshotting the back buffer and writing it back costs one delta
refresh and keeps the host frame on screen across the detour.
Run `f` — typically a nested dialog or a full-screen prompt — and put THIS frame back exactly as it was. `open-nested!` erases the screen on the way OUT, which is right when the caller repaints itself afterwards and catastrophic in the middle of a magit band flow: the next band lands on blank paper and everything above it is gone. Snapshotting the back buffer and writing it back costs one delta refresh and keeps the host frame on screen across the detour.
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 |