pub struct TuiState {Show 18 fields
pub input: String,
pub cursor: usize,
pub transcript: Vec<TranscriptEntry>,
pub streaming: Option<String>,
pub modal: Option<Modal>,
pub theme: Theme,
pub keymap: Keymap,
pub vim_enabled: bool,
pub vim_mode: VimMode,
pub history: PromptHistory,
pub history_search: Option<HistorySearchState>,
pub input_focus: InputFocus,
pub status: StatusLine,
pub scroll: usize,
pub should_quit: bool,
pub external_editor_requested: bool,
pub pending_images: Vec<String>,
pub last_submission: Option<String>,
/* private fields */
}Expand description
The whole TUI view-model — see the module doc comment. Constructed
fresh per TUI session by the CLI render layer; every mutation goes
through Self::apply.
Fields§
§input: StringThe composer’s current text.
cursor: usizeByte offset into input — always on a char boundary.
transcript: Vec<TranscriptEntry>The scrollback transcript, oldest first.
streaming: Option<String>In-progress assistant text (streaming) — None when no turn is
mid-flight.
modal: Option<Modal>The currently-showing modal, if any.
theme: ThemeThe active display theme.
keymap: KeymapThe resolved (default + overrides) keybinding table.
vim_enabled: boolD8 “vim” — whether modal editing is active at all
(crate::Config::tui_vim_mode). false (the default): every key
is a plain insert/navigate, VimMode is never consulted.
vim_mode: VimModeThe current vim sub-mode (only meaningful when vim_enabled).
history: PromptHistoryThe cross-session prompt history.
history_search: Option<HistorySearchState>Live Ctrl+R search state, if Self::input_focus is
InputFocus::HistorySearch.
input_focus: InputFocusWhat the composer area is currently showing.
status: StatusLineThe status line’s contents.
scroll: usizeCurrent transcript scroll offset (pages back from the bottom).
should_quit: boolSet once the user has asked to quit — the render loop’s exit signal.
external_editor_requested: boolSet while the CLI layer’s $EDITOR invocation is in flight.
pending_images: Vec<String>Image references pasted into the composer, in submission order —
drained by the CLI layer once it reads Action::Submit.
last_submission: Option<String>Set by apply(Action::Submit(text)) to Some(text) — the CLI event
loop’s ONE polling point for “a turn needs to be sent”: call
Self::take_submission after every Self::on_key (or manual
apply) to both read and clear it in one step, so a submission is
never double-sent.
Implementations§
Source§impl TuiState
impl TuiState
Sourcepub fn new(
theme: Theme,
keymap: Keymap,
vim_enabled: bool,
history: PromptHistory,
) -> TuiState
pub fn new( theme: Theme, keymap: Keymap, vim_enabled: bool, history: PromptHistory, ) -> TuiState
A fresh, empty state — theme/vim_enabled/keymap typically come
from the resolved crate::Config (tui_theme/tui_vim_mode/
tui_keymap), history from PromptHistory::load_from_file.
Sourcepub fn new_default() -> TuiState
pub fn new_default() -> TuiState
Convenience for a caller that doesn’t need Default::default()-style
construction control — plain-mode, dark theme, default keymap, empty
history. Handy for tests and the render layer’s smoke-test harness.
Sourcepub fn handle_key(&self, key: KeyEvent) -> Vec<Action>
pub fn handle_key(&self, key: KeyEvent) -> Vec<Action>
Translate one keypress into the Actions it produces — READS
state (to be context-sensitive: a modal open, history-search
active, vim normal-mode all change what a key means) but never
mutates it. Call Self::apply on each returned action (in order)
to actually realize the transition — the render layer’s on_key
convenience does exactly that.
Sourcepub fn on_key(&mut self, key: KeyEvent)
pub fn on_key(&mut self, key: KeyEvent)
Run Self::handle_key, then Self::apply every resulting
action in order — the render layer’s one-call-per-keypress
convenience. Every externally-relevant outcome (a turn to send, an
editor to launch, …) lands in a dedicated TuiState field
(Self::last_submission/Self::external_editor_requested/
Self::should_quit) the caller polls afterward — Action itself
is intentionally NOT Clone (it carries one-shot reply channels),
so this doesn’t hand actions back; a caller that needs to react to
the RAW action stream (e.g. a test) calls handle_key+apply
directly instead, as most of this module’s own tests do.
Sourcepub fn take_submission(&mut self) -> Option<String>
pub fn take_submission(&mut self) -> Option<String>
Take (and clear) the most recent submission, if any — see
Self::last_submission’s doc comment.
Sourcepub fn take_pending_images(&mut self) -> Vec<String>
pub fn take_pending_images(&mut self) -> Vec<String>
F5 (Fable-5 adversarial review): the CLI layer’s paired polling
point alongside Self::take_submission — call both together,
same tick, right after take_submission returns Some: this
drains (and clears) every image path staged via
Action::PasteImage for THAT submission, for the caller to
route into the turn’s multimodal content (e.g.
Agent::send_with_images). Previously Action::Submit cleared
pending_images eagerly, before the CLI layer could ever read it
— this method is what makes draining it the CLI’s job instead, so
a pasted image path actually reaches the model.
Sourcepub fn fail_close_pending_modals(&mut self)
pub fn fail_close_pending_modals(&mut self)
D-1 (Fable-5 delta review — MEDIUM, “error-path indefinite hang”):
drop the active modal AND everything still queued behind it,
without sending a reply. Each Modal variant that carries a
reply channel (Approval/ChildApproval’s std::sync::mpsc::Sender,
Elicitation’s tokio::sync::oneshot::Sender) has its sender
dropped as part of this — the corresponding blocked caller
(TuiApprovalHandler::ask/elicitation) already treats a closed
channel as its documented fail-closed default
(ApprovalOutcome::Deny / a declined ElicitationResponse; see
crate::tui::handlers), so this never silently allows anything.
OAuthDeviceCode carries no reply channel — dropping it is a plain
dismissal.
The CLI’s render loop (run_turn_blocking_with_input) calls this
when its own terminal I/O has failed while a modal is still
unanswered: nothing is left alive to answer it (crossterm is
broken), and the in-flight turn’s worker thread is parked in a
blocking recv()/.await on that modal’s reply channel that
std::thread::scope will join before the loop can return ANY
value, including its own I/O error — so leaving the modal pending
would hang the whole session forever instead of surfacing that
error.