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