Skip to main content

vtcode_core/ui/
tui.rs

1//! TUI protocol types and session interface.
2//!
3//! When the `tui` feature is enabled, this module re-exports the full app-layer
4//! protocol from `vtcode-tui`.  When the feature is disabled (headless build),
5//! it re-exports the shared data types from `vtcode-commons` and provides
6//! lightweight no-op stubs for `InlineHandle`, `InlineSession`, and
7//! `InlineEvent`.
8
9// ── Shared data types (always available from vtcode-commons) ────────────────
10
11pub use vtcode_commons::ui_protocol::{
12    EditingMode, InlineHeaderContext, InlineHeaderHighlight, InlineHeaderStatusBadge,
13    InlineHeaderStatusTone, InlineLinkRange, InlineLinkTarget, InlineListItem,
14    InlineListSearchConfig, InlineListSelection, InlineMessageKind, InlineSegment, InlineTextStyle,
15    InlineTheme, LayoutModeOverride, PlanContent, PlanPhase, PlanStep, ReasoningDisplayMode,
16    RewindAction, SecurePromptConfig, SessionSurface, SlashCommandItem, UiMode, WizardModalMode,
17    WizardStep, convert_style, theme_from_color_fields,
18};
19
20pub use vtcode_commons::ui_protocol::KeyboardProtocolSettings;
21
22// ── Full TUI re-exports (feature = "tui") ───────────────────────────────────
23
24#[cfg(feature = "tui")]
25pub use vtcode_tui::app::*;
26
27// ── Headless stubs (feature = "tui" disabled) ───────────────────────────────
28
29#[cfg(not(feature = "tui"))]
30mod headless {
31    use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
32
33    use super::{
34        InlineListItem, InlineListSearchConfig, InlineListSelection, InlineMessageKind,
35        InlineSegment, SecurePromptConfig,
36    };
37
38    use crate::ui::theme::ThemeStyles;
39
40    /// Headless `InlineEvent` — all variants present so match arms compile.
41    #[derive(Clone, Debug, PartialEq, Eq)]
42    pub enum InlineEvent {
43        Submit(String),
44        QueueSubmit(String),
45        Steer(String),
46        ProcessLatestQueued,
47        EditQueue,
48        Cancel,
49        Exit,
50        Interrupt,
51        Pause,
52        Resume,
53        BackgroundOperation,
54        ScrollLineUp,
55        ScrollLineDown,
56        ScrollPageUp,
57        ScrollPageDown,
58        OpenFileInEditor(String),
59        OpenUrl(String),
60        LaunchEditor,
61        ForceCancelPtySession,
62        RequestInlinePromptSuggestion(String),
63        ToggleMode,
64        HistoryPrevious,
65        HistoryNext,
66    }
67
68    /// Minimal command surface used by tests and headless sinks.
69    #[derive(Clone, Debug)]
70    pub enum InlineCommand {
71        AppendLine {
72            kind: InlineMessageKind,
73            segments: Vec<InlineSegment>,
74        },
75        AppendPastedMessage {
76            kind: InlineMessageKind,
77            text: String,
78            line_count: usize,
79        },
80        Inline {
81            kind: InlineMessageKind,
82            segment: InlineSegment,
83        },
84        ReplaceLast {
85            count: usize,
86            kind: InlineMessageKind,
87            lines: Vec<Vec<InlineSegment>>,
88        },
89        ForceRedraw,
90        Shutdown,
91        ClearScreen,
92        CloseModal,
93        SetReasoningStage(Option<String>),
94    }
95
96    /// No-op handle for headless builds. All methods silently discard.
97    #[derive(Clone, Debug)]
98    pub struct InlineHandle {
99        sender: Option<UnboundedSender<InlineCommand>>,
100    }
101
102    impl InlineHandle {
103        pub fn new_for_tests(sender: UnboundedSender<InlineCommand>) -> Self {
104            Self {
105                sender: Some(sender),
106            }
107        }
108
109        fn send_command(&self, command: InlineCommand) {
110            if let Some(sender) = &self.sender {
111                let _ = sender.send(command);
112            }
113        }
114
115        pub fn append_line(&self, kind: InlineMessageKind, segments: Vec<InlineSegment>) {
116            self.send_command(InlineCommand::AppendLine { kind, segments });
117        }
118        pub fn append_pasted_message(
119            &self,
120            kind: InlineMessageKind,
121            text: String,
122            line_count: usize,
123        ) {
124            self.send_command(InlineCommand::AppendPastedMessage {
125                kind,
126                text,
127                line_count,
128            });
129        }
130        pub fn inline(&self, kind: InlineMessageKind, segment: InlineSegment) {
131            self.send_command(InlineCommand::Inline { kind, segment });
132        }
133        pub fn replace_last(
134            &self,
135            count: usize,
136            kind: InlineMessageKind,
137            lines: Vec<Vec<InlineSegment>>,
138        ) {
139            self.send_command(InlineCommand::ReplaceLast { count, kind, lines });
140        }
141        pub fn force_redraw(&self) {
142            self.send_command(InlineCommand::ForceRedraw);
143        }
144        pub fn shutdown(&self) {
145            self.send_command(InlineCommand::Shutdown);
146        }
147        pub fn clear_screen(&self) {
148            self.send_command(InlineCommand::ClearScreen);
149        }
150        pub fn show_modal(
151            &self,
152            _title: String,
153            _lines: Vec<String>,
154            _secure_prompt: Option<SecurePromptConfig>,
155        ) {
156        }
157        pub fn show_list_modal(
158            &self,
159            _title: String,
160            _lines: Vec<String>,
161            _items: Vec<InlineListItem>,
162            _selected: Option<InlineListSelection>,
163            _search: Option<InlineListSearchConfig>,
164        ) {
165        }
166        pub fn close_modal(&self) {
167            self.send_command(InlineCommand::CloseModal);
168        }
169        pub fn set_reasoning_stage(&self, stage: Option<String>) {
170            self.send_command(InlineCommand::SetReasoningStage(stage));
171        }
172    }
173
174    /// Headless session — events never arrive.
175    pub struct InlineSession {
176        pub handle: InlineHandle,
177        pub events: UnboundedReceiver<InlineEvent>,
178    }
179
180    impl InlineSession {
181        pub async fn next_event(&mut self) -> Option<InlineEvent> {
182            self.events.recv().await
183        }
184
185        pub fn clone_inline_handle(&self) -> InlineHandle {
186            self.handle.clone()
187        }
188    }
189
190    /// Headless appearance config with sensible defaults.
191    #[derive(Debug, Clone, Default)]
192    pub struct SessionAppearanceConfig {
193        pub theme: String,
194        pub ui_mode: super::UiMode,
195        pub show_sidebar: bool,
196        pub min_content_width: u16,
197        pub min_navigation_width: u16,
198        pub navigation_width_percent: u8,
199        pub transcript_bottom_padding: u16,
200        pub dim_completed_todos: bool,
201        pub message_block_spacing: u8,
202        pub layout_mode: super::LayoutModeOverride,
203        pub reasoning_display_mode: super::ReasoningDisplayMode,
204        pub reasoning_visible_default: bool,
205        pub vim_mode: bool,
206        pub screen_reader_mode: bool,
207        pub reduce_motion_mode: bool,
208        pub reduce_motion_keep_progress_animation: bool,
209        pub customization: (),
210    }
211
212    /// Build an [`InlineTheme`](super::InlineTheme) from core theme styles.
213    pub fn theme_from_styles(styles: &ThemeStyles) -> super::InlineTheme {
214        super::theme_from_color_fields(
215            styles.foreground,
216            styles.background,
217            styles.primary,
218            styles.secondary,
219            styles.tool,
220            styles.tool_detail,
221            styles.pty_output,
222        )
223    }
224}
225
226#[cfg(not(feature = "tui"))]
227pub use headless::*;