ExAthena.Chat.Tui.State (ExAthena v0.15.1)

Copy Markdown View Source

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

autocomplete()

@type autocomplete() :: nil | %{items: [String.t()], idx: non_neg_integer()}

event_kind()

@type event_kind() ::
  :user
  | :assistant
  | :tool_call
  | :tool_result
  | :tool_result_error
  | :tool_block
  | :error
  | :warning
  | :info
  | :status
  | :thinking
  | :detail_header

event_row()

@type event_row() :: {event_kind(), term()}

popup()

@type popup() ::
  nil
  | {:model, [String.t()], non_neg_integer()}
  | {:mode, [atom()], non_neg_integer()}
  | {:provider, [{String.t(), String.t()}], non_neg_integer()}

t()

@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

append_event(state, row)

@spec append_event(t(), event_row()) :: t()

append_loop_event(state, arg2)

@spec append_loop_event(t(), term()) :: t()

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.

apply_result(state, result)

@spec apply_result(t(), ExAthena.Result.t()) :: t()

clear_session(state)

@spec clear_session(t()) :: t()

close_autocomplete(state)

@spec close_autocomplete(t()) :: t()

Clear the autocomplete popup state.

close_popup(state)

@spec close_popup(t()) :: t()

current_popup_selection(state)

@spec current_popup_selection(t()) :: any() | nil

cycle_details_tab(state)

@spec cycle_details_tab(t()) :: t()

Switch to the next tab in order, wrapping to the first.

cycle_diff_mode(state)

@spec cycle_diff_mode(t()) :: t()

Toggle the Changes tab's diff layout.

details_tabs()

@spec details_tabs() :: [atom()]

Returns the ordered list of tab atoms used in the details pane.

flush_stream(state)

@spec flush_stream(t()) :: t()

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.

history(arg1)

@spec history(t()) :: [String.t()]

Return the list of prior user-message contents in this session, newest-first. Used by the input's Up/Down history navigation.

history_next(state)

@spec history_next(t()) :: {t(), String.t() | nil}

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.

history_prev(state, draft)

@spec history_prev(t(), String.t()) :: {t(), String.t() | nil}

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_autocomplete(state, delta)

@spec move_autocomplete(t(), integer()) :: t()

Move the autocomplete selection by delta (wraps top/bottom).

move_popup_selection(state, delta)

@spec move_popup_selection(t(), integer()) :: t()

new(session)

@spec new(ExAthena.Chat.Session.t()) :: t()

open_popup(state, arg)

@spec open_popup(
  t(),
  {:model, [String.t()]}
  | {:mode, [atom()]}
  | {:provider, [{String.t(), String.t()}]}
) :: t()

reset_history_nav(state)

@spec reset_history_nav(t()) :: t()

Cancel any in-progress history navigation (e.g. on submit or typing).

reset_pane_scroll(state)

@spec reset_pane_scroll(t()) :: t()

Reset both panes back to auto-bottom (e.g. after Enter or End).

reset_stream_state(state)

@spec reset_stream_state(t()) :: t()

Reset the streaming parser state at the start of a new turn. Called by Tui.dispatch_message/2 before issuing the next request.

scroll_details(state, delta)

@spec scroll_details(t(), integer()) :: t()

Like scroll_messages/2 but for the details pane.

scroll_details_top(state)

@spec scroll_details_top(t()) :: t()

Jump the details pane to the top.

scroll_messages(state, delta)

@spec scroll_messages(t(), integer()) :: t()

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.

scroll_messages_top(state)

@spec scroll_messages_top(t()) :: t()

Jump the messages pane to the top (a very large offset; View clamps).

selected_autocomplete(state)

@spec selected_autocomplete(t()) :: String.t() | nil

Return the currently-selected autocomplete entry (or nil).

set_api_key(state, key)

@spec set_api_key(t(), String.t()) :: t()

set_details_tab(state, tab)

@spec set_details_tab(t(), atom()) :: t()

set_diff_mode(state, mode)

@spec set_diff_mode(t(), :inline | :side_by_side) :: t()

Set the Changes tab's diff layout (:inline or :side_by_side).

set_git_diff(state, lines)

@spec set_git_diff(t(), [String.t()]) :: t()

Replace the cached git diff output with a fresh list of lines.

set_loading(state, flag)

@spec set_loading(t(), boolean()) :: t()

set_mode(state, mode)

@spec set_mode(t(), atom()) :: t()

set_model(state, model)

@spec set_model(t(), String.t()) :: t()

set_mouse_enabled(state, enabled?)

@spec set_mouse_enabled(t(), boolean()) :: t()

Record whether crossterm mouse capture is currently on.

set_provider(state, provider)

@spec set_provider(t(), atom()) :: t()

update_autocomplete(state, value)

@spec update_autocomplete(t(), String.t()) :: t()

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.