diff options
| author | Michelle Tilley <michelle@michelletilley.net> | 2026-04-21 13:07:27 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-21 13:07:27 -0700 |
| commit | 2f702ad446fcd6a261a3bea0ab2807d70eca43e2 (patch) | |
| tree | 4cfa6276257cefbe73f7fa46a74026170aaf8435 /crates/atuin-ai/src/tui/view/mod.rs | |
| parent | docs: document show_numeric_shortcuts (#3433) (diff) | |
| download | atuin-2f702ad446fcd6a261a3bea0ab2807d70eca43e2.zip | |
refactor: Replace ad-hoc dispatch with FSM + driver architecture (#3434)
Replaces the tangled dispatch handler system (`tui/dispatch.rs`,
`tui/state.rs`) with a pure finite state machine + driver architecture.
The FSM handles all state transitions as explicit `(State, Event) →
(NewState, Effects)` mappings. The driver executes IO effects and
bridges the TUI to the FSM.
Diffstat (limited to 'crates/atuin-ai/src/tui/view/mod.rs')
| -rw-r--r-- | crates/atuin-ai/src/tui/view/mod.rs | 62 |
1 files changed, 42 insertions, 20 deletions
diff --git a/crates/atuin-ai/src/tui/view/mod.rs b/crates/atuin-ai/src/tui/view/mod.rs index 6e13e406..d40a44d4 100644 --- a/crates/atuin-ai/src/tui/view/mod.rs +++ b/crates/atuin-ai/src/tui/view/mod.rs @@ -5,7 +5,9 @@ use eye_declare::{ }; use ratatui_core::style::{Color, Modifier, Style}; -use crate::tools::{ClientToolCall, HistorySearchFilterMode, ToolPreview, TrackedTool}; +use crate::driver::ViewState; +use crate::fsm::{AgentState, StreamPhase}; +use crate::tools::{ClientToolCall, HistorySearchFilterMode, ToolPreview}; use crate::tui::components::select::SelectOption; use crate::tui::components::session_continue::SessionContinue; use crate::tui::events::{AiTuiEvent, PermissionResult}; @@ -14,10 +16,23 @@ use super::components::atuin_ai::AtuinAi; use super::components::input_box::InputBox; use super::components::markdown::Markdown; use super::components::select::Select; -use super::state::{AppMode, Session}; +use super::state::AppMode; mod turn; +impl From<&AgentState> for AppMode { + fn from(state: &AgentState) -> Self { + match state { + AgentState::Idle { .. } => AppMode::Input, + AgentState::Turn { + stream: StreamPhase::Connecting, + } => AppMode::Generating, + AgentState::Turn { .. } => AppMode::Streaming, + AgentState::Error(_) => AppMode::Error, + } + } +} + /// Build the element tree from current state. /// /// Layout (top to bottom): @@ -26,28 +41,27 @@ mod turn; /// - Error display (if in error state) /// - Spacer /// - Input box (bordered, with contextual keybindings) -pub(crate) fn ai_view(state: &Session) -> Elements { - let mut turn_builder = turn::TurnBuilder::new(&state.tool_tracker); +pub(crate) fn ai_view(state: &ViewState) -> Elements { + let mut turn_builder = turn::TurnBuilder::new(&state.tools); - for event in &state.archived_view_events { + for event in &state.archived_events { turn_builder.add_event(event); } - for event in &state.conversation.events[state.view_start_index..] { + for event in &state.visible_events { turn_builder.add_event(event); } let turns = turn_builder.build(); - let busy = state.interaction.mode == AppMode::Streaming - || state.interaction.mode == AppMode::Generating; + let busy = state.is_busy(); let last_index = turns.len().saturating_sub(1); element! { AtuinAi( - mode: state.interaction.mode, - has_command: state.conversation.has_any_command(), - is_input_blank: state.interaction.is_input_blank, - pending_confirmation: state.interaction.confirmation_pending, - has_executing_preview: state.tool_tracker.has_executing_preview(), + mode: AppMode::from(&state.agent_state), + has_command: state.has_command(), + is_input_blank: state.is_input_blank, + pending_confirmation: state.has_confirmation(), + has_executing_preview: state.tools.has_executing_preview(), ) { #(if state.is_resumed && (!state.is_exiting() || !turns.is_empty()) { SessionContinue(key: "continuation-notice", continued_at: state.last_event_time) @@ -77,6 +91,15 @@ pub(crate) fn ai_view(state: &Session) -> Elements { } }) + #(if let AgentState::Error(ref msg) = state.agent_state { + View(key: "error-display", padding_left: Cells::from(2), padding_top: Cells::from(1)) { + Text { + Span(text: "Error: ", style: Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)) + Span(text: msg, style: Style::default().fg(Color::Red)) + } + } + }) + #(if !state.is_exiting() { #(input_view(state)) }) @@ -84,11 +107,10 @@ pub(crate) fn ai_view(state: &Session) -> Elements { } } -fn input_view(state: &Session) -> Elements { - let asking_tool = state.tool_tracker.asking_for_permission(); +fn input_view(state: &ViewState) -> Elements { + let asking_tool = state.tools.awaiting_permission(); let in_git_project = state.in_git_project; let slash_results = state - .interaction .slash_command_search_results .iter() .take(4) @@ -107,12 +129,12 @@ fn input_view(state: &Session) -> Elements { title: "Generate a command or ask a question", title_right: "Atuin AI", footer: state.footer_text(), - active: state.interaction.mode == AppMode::Input && !state.interaction.confirmation_pending, + active: state.is_input_active(), slash_suggestion: first_slash_result.cloned() ) - #(if state.interaction.is_input_blank && state.conversation.has_any_command() && state.interaction.mode == AppMode::Input { - #(if state.interaction.confirmation_pending { + #(if state.is_input_blank && state.has_command() && state.is_input_active() { + #(if state.has_confirmation() { Text { Span(text: "[Enter] Confirm dangerous command [Esc] Cancel", style: Style::default().fg(Color::Gray)) } } else { Text { Span(text: "[Enter] Execute suggested command [Tab] Insert Command", style: Style::default().fg(Color::Gray)) } @@ -140,7 +162,7 @@ fn input_view(state: &Session) -> Elements { } } -fn tool_call_view(tool_call: &TrackedTool, in_git_project: bool) -> Elements { +fn tool_call_view(tool_call: &crate::fsm::tools::TrackedTool, in_git_project: bool) -> Elements { let verb = tool_call.tool.descriptor().display_verb; let tool_desc = match &tool_call.tool { ClientToolCall::Read(tool) => tool.path.display().to_string(), |
