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