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