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