Skip to main content

agent_air_tui/widgets/
mod.rs

1//! TUI Widgets
2//!
3//! Reusable widget components for LLM-powered terminal applications.
4//!
5//! # Core Widgets (always present)
6//! - [`ChatView`] - Chat message display with streaming support
7//! - [`TextInput`] - Text input buffer with cursor management
8//!
9//! # Registerable Widgets
10//! - [`PermissionPanel`] - Permission request panel for tool interactions
11//! - [`QuestionPanel`] - Question panel for user input collection
12//! - [`SessionPickerState`] - Session picker for viewing/switching sessions
13//! - [`SlashPopupState`] - Slash command popup
14//!
15//! # Widget System
16//!
17//! Widgets can be registered with the App via the Widget trait. This allows
18//! agents to customize which widgets are available.
19
20use crossterm::event::KeyEvent;
21use ratatui::{Frame, layout::Rect};
22use std::any::Any;
23
24use crate::controller::{AskUserQuestionsResponse, PermissionPanelResponse};
25use crate::keys::NavigationHelper;
26use crate::permissions::BatchPermissionResponse;
27use crate::themes::Theme;
28
29pub mod batch_permission_panel;
30pub mod chat;
31pub mod chat_helpers;
32pub mod conversation;
33pub mod input;
34pub mod permission_panel;
35pub mod question_panel;
36pub mod session_picker;
37pub mod slash_popup;
38pub mod status_bar;
39
40pub use batch_permission_panel::{
41    BatchKeyAction, BatchPermissionOption, BatchPermissionPanel, BatchPermissionPanelConfig,
42};
43pub use chat::{ChatView, ChatViewConfig, MessageRole, ToolMessageData, ToolStatus};
44pub use chat_helpers::RenderFn;
45pub use conversation::{ConversationView, ConversationViewFactory};
46pub use input::TextInput;
47pub use permission_panel::{
48    KeyAction as PermissionKeyAction, PermissionOption, PermissionPanel, PermissionPanelConfig,
49};
50pub use question_panel::{
51    AnswerState, EnterAction, FocusItem, KeyAction as QuestionKeyAction, QuestionPanel,
52    QuestionPanelConfig,
53};
54pub use session_picker::{
55    SessionInfo, SessionPickerConfig, SessionPickerState, render_session_picker,
56};
57pub use slash_popup::{
58    SimpleCommand, SlashCommandDisplay, SlashPopupConfig, SlashPopupState, render_slash_popup,
59};
60pub use status_bar::{StatusBar, StatusBarConfig, StatusBarData};
61
62/// Standard widget IDs for built-in widgets
63pub mod widget_ids {
64    // Core widgets (always present)
65    pub const CHAT_VIEW: &str = "chat_view";
66    pub const TEXT_INPUT: &str = "text_input";
67
68    // Registerable widgets
69    pub const BATCH_PERMISSION_PANEL: &str = "batch_permission_panel";
70    pub const PERMISSION_PANEL: &str = "permission_panel";
71    pub const QUESTION_PANEL: &str = "question_panel";
72    pub const SESSION_PICKER: &str = "session_picker";
73    pub const SLASH_POPUP: &str = "slash_popup";
74    pub const THEME_PICKER: &str = "theme_picker";
75    pub const STATUS_BAR: &str = "status_bar";
76}
77
78/// Context provided to widgets when handling key events.
79///
80/// This contains references to the theme and a navigation helper that allows
81/// widgets to respect configured key bindings instead of hardcoding key codes.
82pub struct WidgetKeyContext<'a> {
83    /// Theme for styling.
84    pub theme: &'a Theme,
85    /// Navigation helper for checking key bindings.
86    pub nav: NavigationHelper<'a>,
87}
88
89/// Result of a widget handling a key event
90#[derive(Debug, Clone)]
91pub enum WidgetKeyResult {
92    /// Key was not handled by this widget
93    NotHandled,
94    /// Key was handled, no further action needed
95    Handled,
96    /// Key was handled, and the widget requests an action from App
97    Action(WidgetAction),
98}
99
100/// Actions that widgets can request from the App
101#[derive(Debug, Clone)]
102pub enum WidgetAction {
103    /// Submit a question panel response
104    SubmitQuestion {
105        tool_use_id: String,
106        response: AskUserQuestionsResponse,
107    },
108    /// Cancel a question panel
109    CancelQuestion { tool_use_id: String },
110    /// Submit a permission panel response
111    SubmitPermission {
112        tool_use_id: String,
113        response: PermissionPanelResponse,
114    },
115    /// Cancel a permission panel
116    CancelPermission { tool_use_id: String },
117    /// Submit a batch permission response
118    SubmitBatchPermission {
119        batch_id: String,
120        response: BatchPermissionResponse,
121    },
122    /// Cancel a batch permission panel
123    CancelBatchPermission { batch_id: String },
124    /// Switch to a different session
125    SwitchSession { session_id: i64 },
126    /// Execute a slash command
127    ExecuteCommand { command: String },
128    /// Close the widget (theme picker confirm/cancel)
129    Close,
130}
131
132/// Trait for registerable TUI widgets
133///
134/// Widgets that implement this trait can be registered with the App and will
135/// receive key events and rendering opportunities based on their state.
136pub trait Widget: Send + 'static {
137    /// Unique identifier for this widget type
138    fn id(&self) -> &'static str;
139
140    /// Priority for key event handling (higher = checked first)
141    ///
142    /// Modal widgets should have high priority to intercept input.
143    /// Default is 100.
144    fn priority(&self) -> u8 {
145        100
146    }
147
148    /// Whether the widget is currently active/visible
149    fn is_active(&self) -> bool;
150
151    /// Handle key event, return result indicating what action to take.
152    ///
153    /// The `ctx` parameter provides access to the theme and a navigation helper
154    /// that respects configured key bindings.
155    fn handle_key(&mut self, key: KeyEvent, ctx: &WidgetKeyContext) -> WidgetKeyResult;
156
157    /// Render the widget
158    fn render(&mut self, frame: &mut Frame, area: Rect, theme: &Theme);
159
160    /// Calculate required height for this widget
161    ///
162    /// Returns 0 if the widget doesn't need dedicated space.
163    fn required_height(&self, available: u16) -> u16 {
164        let _ = available;
165        0
166    }
167
168    /// Whether this widget blocks input to the text input when active
169    fn blocks_input(&self) -> bool {
170        false
171    }
172
173    /// Whether this widget is a full-screen overlay
174    ///
175    /// Overlay widgets are rendered on top of everything else.
176    fn is_overlay(&self) -> bool {
177        false
178    }
179
180    /// Cast to Any for downcasting
181    fn as_any(&self) -> &dyn Any;
182
183    /// Cast to Any for mutable downcasting
184    fn as_any_mut(&mut self) -> &mut dyn Any;
185
186    /// Convert to `Box<dyn Any>` for owned downcasting
187    fn into_any(self: Box<Self>) -> Box<dyn Any>;
188}