Skip to main content

vtcode_tui/core_tui/
mod.rs

1use anyhow::Result;
2use std::sync::Arc;
3use tokio::sync::mpsc;
4
5use crate::config::types::UiSurfacePreference;
6
7pub mod alternate_screen;
8pub mod app;
9pub(crate) mod language_badge;
10pub mod log;
11pub mod panic_hook;
12pub mod runner;
13pub mod session;
14pub mod style;
15pub mod theme_parser;
16pub mod types;
17pub mod widgets;
18
19pub use style::{convert_style, theme_from_styles};
20pub use theme_parser::ThemeConfigParser;
21pub use types::{
22    ContentPart, EditingMode, FocusChangeCallback, InlineCommand, InlineEvent, InlineEventCallback,
23    InlineHandle, InlineHeaderContext, InlineHeaderHighlight, InlineHeaderStatusBadge,
24    InlineHeaderStatusTone, InlineLinkRange, InlineLinkTarget, InlineListItem,
25    InlineListSearchConfig, InlineListSelection, InlineMessageKind, InlineSegment, InlineSession,
26    InlineTextStyle, InlineTheme, ListOverlayRequest, ModalOverlayRequest, OverlayEvent,
27    OverlayHotkey, OverlayHotkeyAction, OverlayHotkeyKey, OverlayRequest, OverlaySelectionChange,
28    OverlaySubmission, RewindAction, SecurePromptConfig, WizardModalMode, WizardOverlayRequest,
29    WizardStep,
30};
31
32use runner::{TuiOptions, run_tui};
33
34pub fn spawn_session(
35    theme: InlineTheme,
36    placeholder: Option<String>,
37    surface_preference: UiSurfacePreference,
38    inline_rows: u16,
39    event_callback: Option<InlineEventCallback>,
40    active_pty_sessions: Option<Arc<std::sync::atomic::AtomicUsize>>,
41    workspace_root: Option<std::path::PathBuf>,
42) -> Result<InlineSession> {
43    spawn_session_with_prompts(
44        theme,
45        placeholder,
46        surface_preference,
47        inline_rows,
48        event_callback,
49        active_pty_sessions,
50        None,
51        crate::config::KeyboardProtocolConfig::default(),
52        workspace_root,
53    )
54}
55
56/// Spawn session with optional custom prompts pre-loaded
57#[allow(clippy::too_many_arguments)]
58pub fn spawn_session_with_prompts(
59    theme: InlineTheme,
60    placeholder: Option<String>,
61    surface_preference: UiSurfacePreference,
62    inline_rows: u16,
63    event_callback: Option<InlineEventCallback>,
64    active_pty_sessions: Option<Arc<std::sync::atomic::AtomicUsize>>,
65    input_activity_counter: Option<Arc<std::sync::atomic::AtomicU64>>,
66    keyboard_protocol: crate::config::KeyboardProtocolConfig,
67    workspace_root: Option<std::path::PathBuf>,
68) -> Result<InlineSession> {
69    spawn_session_with_prompts_and_options(
70        theme,
71        placeholder,
72        surface_preference,
73        inline_rows,
74        event_callback,
75        None,
76        active_pty_sessions,
77        input_activity_counter,
78        keyboard_protocol,
79        workspace_root,
80        None,
81        "Agent TUI".to_string(),
82        None,
83    )
84}
85
86/// Spawn session with host-injected UI options.
87#[allow(clippy::too_many_arguments)]
88pub fn spawn_session_with_prompts_and_options(
89    theme: InlineTheme,
90    placeholder: Option<String>,
91    surface_preference: UiSurfacePreference,
92    inline_rows: u16,
93    event_callback: Option<InlineEventCallback>,
94    focus_callback: Option<FocusChangeCallback>,
95    active_pty_sessions: Option<Arc<std::sync::atomic::AtomicUsize>>,
96    input_activity_counter: Option<Arc<std::sync::atomic::AtomicU64>>,
97    keyboard_protocol: crate::config::KeyboardProtocolConfig,
98    workspace_root: Option<std::path::PathBuf>,
99    appearance: Option<session::config::AppearanceConfig>,
100    app_name: String,
101    non_interactive_hint: Option<String>,
102) -> Result<InlineSession> {
103    use crossterm::tty::IsTty;
104
105    // Check stdin is a terminal BEFORE spawning the task
106    if !std::io::stdin().is_tty() {
107        return Err(anyhow::anyhow!(
108            "cannot run interactive TUI: stdin is not a terminal (must be run in an interactive terminal)"
109        ));
110    }
111
112    let (command_tx, command_rx) = mpsc::unbounded_channel();
113    let (event_tx, event_rx) = mpsc::unbounded_channel();
114    let show_logs = log::is_tui_log_capture_enabled();
115
116    tokio::spawn(async move {
117        if let Err(error) = run_tui(
118            command_rx,
119            event_tx,
120            TuiOptions {
121                surface_preference,
122                inline_rows,
123                show_logs,
124                log_theme: None,
125                event_callback,
126                focus_callback,
127                active_pty_sessions,
128                input_activity_counter,
129                keyboard_protocol,
130                workspace_root,
131            },
132            move |rows| {
133                session::Session::new_with_logs(
134                    theme,
135                    placeholder,
136                    rows,
137                    show_logs,
138                    appearance,
139                    app_name,
140                )
141            },
142        )
143        .await
144        {
145            let error_msg = error.to_string();
146            if error_msg.contains("stdin is not a terminal") {
147                eprintln!("Error: Interactive TUI requires a proper terminal.");
148                if let Some(hint) = non_interactive_hint.as_deref() {
149                    eprintln!("{}", hint);
150                } else {
151                    eprintln!("Use a non-interactive mode in your host app for piped input.");
152                }
153            } else {
154                eprintln!("Error: TUI startup failed: {:#}", error);
155            }
156            tracing::error!(%error, "inline session terminated unexpectedly");
157        }
158    });
159
160    Ok(InlineSession {
161        handle: InlineHandle { sender: command_tx },
162        events: event_rx,
163    })
164}