Pure state container for the mix athena.chat TUI.
Wraps a %ExAthena.Chat.Session{} with UI-only fields (input, popup,
scrollback, streaming buffer, etc.) and provides the transitions the
ExAthena.Chat.Tui App calls from mount/1, handle_event/2, and
handle_info/2.
No ex_ratatui imports here — keeps tests fast and avoids dragging the NIF into pure-data unit tests.
Summary
Functions
Apply an ExAthena.Loop.Events.t() to the UI state.
Clear the autocomplete popup state.
Switch to the next tab in order, wrapping to the first.
Toggle the Changes tab's diff layout.
Returns the ordered list of tab atoms used in the details pane.
Materialize stream_buffer into the events list, and the
details_stream_buffer / details_thinking_buffer into the details
list. Clears all three buffers.
Return the list of prior user-message contents in this session, newest-first. Used by the input's Up/Down history navigation.
Step forward in input history (newer). Past the newest entry restores the saved draft and exits navigation mode.
Step backward in input history (older). On the first call from a
fresh state, snapshots the current draft (whatever the user had
typed) so a later history_next/2 can restore it.
Move the autocomplete selection by delta (wraps top/bottom).
Cancel any in-progress history navigation (e.g. on submit or typing).
Reset both panes back to auto-bottom (e.g. after Enter or End).
Reset the streaming parser state at the start of a new turn.
Called by Tui.dispatch_message/2 before issuing the next request.
Like scroll_messages/2 but for the details pane.
Jump the details pane to the top.
Move the messages pane. delta < 0 scrolls up (older content),
delta > 0 scrolls down. Returns to the bottom when the offset would
go below 0. Idempotent at the top.
Jump the messages pane to the top (a very large offset; View clamps).
Return the currently-selected autocomplete entry (or nil).
Set the Changes tab's diff layout (:inline or :side_by_side).
Replace the cached git diff output with a fresh list of lines.
Record whether crossterm mouse capture is currently on.
Recompute the autocomplete suggestions from the current textarea value.
Opens the popup when the value starts with / and has no whitespace
yet (the user is typing the verb). Closes it otherwise. Preserves the
selected index when possible so re-rendering doesn't jump it.
Types
@type autocomplete() :: nil | %{items: [String.t()], idx: non_neg_integer()}
@type event_kind() ::
:user
| :assistant
| :tool_call
| :tool_result
| :tool_result_error
| :tool_block
| :error
| :warning
| :info
| :status
| :thinking
| :detail_header
@type event_row() :: {event_kind(), term()}
@type popup() :: nil | {:model, [String.t()], non_neg_integer()} | {:mode, [atom()], non_neg_integer()} | {:provider, [{String.t(), String.t()}], non_neg_integer()}
@type t() :: %ExAthena.Chat.Tui.State{ api_key: String.t() | nil, api_key_pending: boolean(), autocomplete: autocomplete(), details: [event_row()], details_scroll: non_neg_integer() | nil, details_scroll_offset: non_neg_integer(), details_stream_buffer: String.t(), details_tab: :timeline | :changes, details_thinking_buffer: String.t(), diff_mode: :inline | :side_by_side, events: [event_row()], footer: String.t(), git_diff_lines: [String.t()], git_diff_scroll_offset: non_neg_integer(), history_draft: String.t(), history_idx: non_neg_integer() | nil, input_ref: reference() | nil, loading?: boolean(), messages_scroll: non_neg_integer() | nil, mouse_enabled: boolean(), pending_message: String.t() | nil, popup: popup(), prior_log_level: atom(), run_task: pid() | nil, scroll_offset: non_neg_integer(), session: ExAthena.Chat.Session.t(), show_details: boolean(), stream_buffer: String.t(), stream_parser: %{ mode: atom(), pending: String.t(), close_tag: String.t() | nil }, streamed_this_turn?: boolean(), thinking_open?: boolean(), tool_blocks: %{required(String.t()) => map()} }
Functions
Apply an ExAthena.Loop.Events.t() to the UI state.
:content deltas accumulate into stream_buffer (left pane) and
details_stream_buffer (right pane); both are materialized by
flush_stream/1. Most other events produce a one-line row in the
left pane and a full-detail block in the right pane.
@spec apply_result(t(), ExAthena.Result.t()) :: t()
Clear the autocomplete popup state.
Switch to the next tab in order, wrapping to the first.
Toggle the Changes tab's diff layout.
@spec details_tabs() :: [atom()]
Returns the ordered list of tab atoms used in the details pane.
Materialize stream_buffer into the events list, and the
details_stream_buffer / details_thinking_buffer into the details
list. Clears all three buffers.
For each pane, if the most recent row is already the same kind
(:assistant / :thinking), the buffered text is appended in place.
Otherwise a fresh row is started.
Return the list of prior user-message contents in this session, newest-first. Used by the input's Up/Down history navigation.
Step forward in input history (newer). Past the newest entry restores the saved draft and exits navigation mode.
Returns {state, replacement_text} — same contract as
history_prev/2.
Step backward in input history (older). On the first call from a
fresh state, snapshots the current draft (whatever the user had
typed) so a later history_next/2 can restore it.
Returns {state, replacement_text} — the caller writes
replacement_text into the textarea. Returns {state, nil} when
there's no history to navigate.
Move the autocomplete selection by delta (wraps top/bottom).
@spec new(ExAthena.Chat.Session.t()) :: t()
Reset both panes back to auto-bottom (e.g. after Enter or End).
Reset the streaming parser state at the start of a new turn.
Called by Tui.dispatch_message/2 before issuing the next request.
Like scroll_messages/2 but for the details pane.
Jump the details pane to the top.
Move the messages pane. delta < 0 scrolls up (older content),
delta > 0 scrolls down. Returns to the bottom when the offset would
go below 0. Idempotent at the top.
Jump the messages pane to the top (a very large offset; View clamps).
Return the currently-selected autocomplete entry (or nil).
Set the Changes tab's diff layout (:inline or :side_by_side).
Replace the cached git diff output with a fresh list of lines.
Record whether crossterm mouse capture is currently on.
Recompute the autocomplete suggestions from the current textarea value.
Opens the popup when the value starts with / and has no whitespace
yet (the user is typing the verb). Closes it otherwise. Preserves the
selected index when possible so re-rendering doesn't jump it.