vtcode_tui/
session_options.rs1use std::path::PathBuf;
2use std::sync::Arc;
3
4use crate::UiSurfacePreference;
5use crate::config::KeyboardProtocolConfig;
6use crate::core_tui::app::session::AppSession;
7use crate::core_tui::app::types::{
8 FocusChangeCallback, InlineEventCallback, InlineSession, InlineTheme, SlashCommandItem,
9};
10use crate::core_tui::log;
11use crate::core_tui::runner::{TuiOptions, run_tui};
12use crate::core_tui::session::config::AppearanceConfig;
13use crate::options::{KeyboardProtocolSettings, SessionSurface};
14
15#[derive(Clone)]
17pub struct SessionOptions {
18 pub placeholder: Option<String>,
19 pub surface_preference: SessionSurface,
20 pub inline_rows: u16,
21 pub event_callback: Option<InlineEventCallback>,
22 pub focus_callback: Option<FocusChangeCallback>,
23 pub active_pty_sessions: Option<Arc<std::sync::atomic::AtomicUsize>>,
24 pub input_activity_counter: Option<Arc<std::sync::atomic::AtomicU64>>,
25 pub keyboard_protocol: KeyboardProtocolSettings,
26 pub workspace_root: Option<PathBuf>,
27 pub slash_commands: Vec<SlashCommandItem>,
28 pub appearance: Option<AppearanceConfig>,
29 pub app_name: String,
30 pub non_interactive_hint: Option<String>,
31}
32
33impl Default for SessionOptions {
34 fn default() -> Self {
35 Self {
36 placeholder: None,
37 surface_preference: SessionSurface::Auto,
38 inline_rows: crate::config::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS,
39 event_callback: None,
40 focus_callback: None,
41 active_pty_sessions: None,
42 input_activity_counter: None,
43 keyboard_protocol: KeyboardProtocolSettings::default(),
44 workspace_root: None,
45 slash_commands: Vec::new(),
46 appearance: None,
47 app_name: "Agent TUI".to_string(),
48 non_interactive_hint: None,
49 }
50 }
51}
52
53impl SessionOptions {
54 pub fn from_host(host: &impl crate::host::HostAdapter) -> Self {
56 let defaults = host.session_defaults();
57 Self {
58 surface_preference: defaults.surface_preference,
59 inline_rows: defaults.inline_rows,
60 keyboard_protocol: defaults.keyboard_protocol,
61 workspace_root: host.workspace_root(),
62 slash_commands: host.slash_commands(),
63 app_name: host.app_name(),
64 non_interactive_hint: host.non_interactive_hint(),
65 ..Self::default()
66 }
67 }
68}
69
70pub fn spawn_session_with_options(
72 theme: InlineTheme,
73 options: SessionOptions,
74) -> anyhow::Result<InlineSession> {
75 use crossterm::tty::IsTty;
76
77 if !std::io::stdin().is_tty() {
79 return Err(anyhow::anyhow!(
80 "cannot run interactive TUI: stdin is not a terminal (must be run in an interactive terminal)"
81 ));
82 }
83
84 let (command_tx, command_rx) = tokio::sync::mpsc::unbounded_channel();
85 let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel();
86 let show_logs = log::is_tui_log_capture_enabled();
87
88 tokio::spawn(async move {
89 if let Err(error) = run_tui(
90 command_rx,
91 event_tx,
92 TuiOptions {
93 surface_preference: UiSurfacePreference::from(options.surface_preference),
94 inline_rows: options.inline_rows,
95 show_logs,
96 log_theme: None,
97 event_callback: options.event_callback,
98 focus_callback: options.focus_callback,
99 active_pty_sessions: options.active_pty_sessions,
100 input_activity_counter: options.input_activity_counter,
101 keyboard_protocol: KeyboardProtocolConfig::from(options.keyboard_protocol),
102 workspace_root: options.workspace_root,
103 },
104 move |rows| {
105 AppSession::new_with_logs(
106 theme,
107 options.placeholder,
108 rows,
109 show_logs,
110 options.appearance,
111 options.slash_commands,
112 options.app_name,
113 )
114 },
115 )
116 .await
117 {
118 let error_msg = error.to_string();
119 if error_msg.contains("stdin is not a terminal") {
120 eprintln!("Error: Interactive TUI requires a proper terminal.");
121 if let Some(hint) = options.non_interactive_hint.as_deref() {
122 eprintln!("{}", hint);
123 } else {
124 eprintln!("Use a non-interactive mode in your host app for piped input.");
125 }
126 } else {
127 eprintln!("Error: TUI startup failed: {:#}", error);
128 }
129 tracing::error!(%error, "inline session terminated unexpectedly");
130 }
131 });
132
133 Ok(InlineSession {
134 handle: crate::core_tui::app::types::InlineHandle { sender: command_tx },
135 events: event_rx,
136 })
137}
138
139pub fn spawn_session_with_host(
141 theme: InlineTheme,
142 host: &impl crate::host::HostAdapter,
143) -> anyhow::Result<InlineSession> {
144 spawn_session_with_options(theme, SessionOptions::from_host(host))
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 struct DemoHost;
152
153 impl crate::host::WorkspaceInfoProvider for DemoHost {
154 fn workspace_name(&self) -> String {
155 "demo".to_string()
156 }
157
158 fn workspace_root(&self) -> Option<PathBuf> {
159 Some(PathBuf::from("/workspace/demo"))
160 }
161 }
162
163 impl crate::host::NotificationProvider for DemoHost {
164 fn set_terminal_focused(&self, _focused: bool) {}
165 }
166
167 impl crate::host::ThemeProvider for DemoHost {
168 fn available_themes(&self) -> Vec<String> {
169 vec!["default".to_string()]
170 }
171
172 fn active_theme_name(&self) -> Option<String> {
173 Some("default".to_string())
174 }
175 }
176
177 impl crate::host::HostAdapter for DemoHost {
178 fn session_defaults(&self) -> crate::host::HostSessionDefaults {
179 crate::host::HostSessionDefaults {
180 surface_preference: SessionSurface::Inline,
181 inline_rows: 24,
182 keyboard_protocol: KeyboardProtocolSettings::default(),
183 }
184 }
185 }
186
187 #[test]
190 fn session_options_from_host_uses_defaults() {
191 let options = SessionOptions::from_host(&DemoHost);
192
193 assert_eq!(options.surface_preference, SessionSurface::Inline);
194 assert_eq!(options.inline_rows, 24);
195 assert_eq!(
196 options.workspace_root,
197 Some(PathBuf::from("/workspace/demo"))
198 );
199 }
200}