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}