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