1#![deny(clippy::print_stdout, clippy::print_stderr)]
5#![deny(clippy::disallowed_methods)]
6use agcodex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
7use agcodex_core::config::Config;
8use agcodex_core::config::ConfigOverrides;
9use agcodex_core::config::ConfigToml;
10use agcodex_core::config::find_agcodex_home;
11use agcodex_core::config::load_config_as_toml_with_cli_overrides;
12use agcodex_core::modes::ModeManager;
13use agcodex_core::modes::OperatingMode;
14use agcodex_core::protocol::AskForApproval;
15use agcodex_core::protocol::SandboxPolicy;
16use agcodex_login::AuthMode;
17use agcodex_login::CodexAuth;
18use agcodex_ollama::DEFAULT_OSS_MODEL;
19use agcodex_protocol::config_types::SandboxMode;
20use app::App;
21use std::fs::OpenOptions;
22use std::path::PathBuf;
23use tracing::error;
24use tracing_appender::non_blocking;
25use tracing_subscriber::EnvFilter;
26use tracing_subscriber::prelude::*;
27
28mod app;
29mod app_event;
30mod app_event_sender;
31mod bottom_pane;
32mod chatwidget;
33mod citation_regex;
34mod cli;
35mod common;
36pub mod custom_terminal;
37mod dialogs;
38mod diff_render;
39mod exec_command;
40mod features;
41mod file_search;
42mod get_git_diff;
43mod history_cell;
44pub mod insert_history;
45pub mod live_wrap;
46mod markdown;
47mod markdown_stream;
48mod notification;
49pub mod onboarding;
50mod render;
51mod session_log;
52mod shimmer;
53mod slash_command;
54mod status_indicator_widget;
55mod streaming;
56mod text_formatting;
57mod tui;
58mod user_approval_widget;
59mod widgets;
60
61#[cfg(test)]
64mod chatwidget_stream_tests;
65
66#[cfg(not(debug_assertions))]
67mod updates;
68#[cfg(not(debug_assertions))]
69use color_eyre::owo_colors::OwoColorize;
70
71pub use cli::Cli;
72pub use dialogs::LoadSessionBrowser;
73pub use dialogs::LoadSessionState;
74pub use dialogs::SaveSessionDialog;
75pub use dialogs::SaveSessionState;
76pub use features::HistoryBrowser;
77pub use features::MessageJump;
78pub use features::RoleFilter;
79pub use widgets::SessionBrowser;
80pub use widgets::SessionSwitcher;
81pub use widgets::SessionSwitcherState;
82
83pub async fn run_main(
86 cli: Cli,
87 codex_linux_sandbox_exe: Option<PathBuf>,
88) -> std::io::Result<agcodex_core::protocol::TokenUsage> {
89 let (sandbox_mode, approval_policy) = if cli.full_auto {
90 (
91 Some(SandboxMode::WorkspaceWrite),
92 Some(AskForApproval::OnFailure),
93 )
94 } else if cli.dangerously_bypass_approvals_and_sandbox {
95 (
96 Some(SandboxMode::DangerFullAccess),
97 Some(AskForApproval::Never),
98 )
99 } else {
100 (
101 cli.sandbox_mode.map(Into::<SandboxMode>::into),
102 cli.approval_policy.map(Into::into),
103 )
104 };
105
106 let model = if let Some(model) = &cli.model {
110 Some(model.clone())
111 } else if cli.oss {
112 Some(DEFAULT_OSS_MODEL.to_owned())
113 } else {
114 None };
116
117 let model_provider_override = if cli.oss {
118 Some(BUILT_IN_OSS_MODEL_PROVIDER_ID.to_owned())
119 } else {
120 None
121 };
122
123 let operating_mode = match cli.mode.as_deref() {
125 Some(s) => match s.to_lowercase().as_str() {
126 "plan" => OperatingMode::Plan,
127 "review" => OperatingMode::Review,
128 _ => OperatingMode::Build,
129 },
130 None => OperatingMode::Build,
131 };
132 let mode_manager = ModeManager::new(operating_mode);
133
134 let cwd = cli.cwd.clone().map(|p| p.canonicalize().unwrap_or(p));
136
137 let overrides = ConfigOverrides {
138 model,
139 approval_policy,
140 sandbox_mode,
141 cwd,
142 model_provider: model_provider_override,
143 config_profile: cli.config_profile.clone(),
144 codex_linux_sandbox_exe,
145 base_instructions: Some(mode_manager.prompt_suffix().to_string()),
146 include_plan_tool: Some(true),
147 include_apply_patch_tool: None,
148 disable_response_storage: cli.oss.then_some(true),
149 show_raw_agent_reasoning: cli.oss.then_some(true),
150 };
151
152 let cli_kv_overrides = match cli.config_overrides.parse_overrides() {
154 Ok(v) => v,
155 #[allow(clippy::print_stderr)]
156 Err(e) => {
157 eprintln!("Error parsing -c overrides: {e}");
158 std::process::exit(1);
159 }
160 };
161
162 let mut config = {
163 #[allow(clippy::print_stderr)]
166 match Config::load_with_cli_overrides(cli_kv_overrides.clone(), overrides) {
167 Ok(config) => config,
168 Err(err) => {
169 eprintln!("Error loading configuration: {err}");
170 std::process::exit(1);
171 }
172 }
173 };
174
175 #[allow(clippy::print_stderr)]
177 let config_toml = {
178 let codex_home = match find_agcodex_home() {
179 Ok(codex_home) => codex_home,
180 Err(err) => {
181 eprintln!("Error finding codex home: {err}");
182 std::process::exit(1);
183 }
184 };
185
186 match load_config_as_toml_with_cli_overrides(&codex_home, cli_kv_overrides) {
187 Ok(config_toml) => config_toml,
188 Err(err) => {
189 eprintln!("Error loading config.toml: {err}");
190 std::process::exit(1);
191 }
192 }
193 };
194
195 let should_show_trust_screen = determine_repo_trust_state(
196 &mut config,
197 &config_toml,
198 approval_policy,
199 sandbox_mode,
200 cli.config_profile.clone(),
201 )?;
202
203 let log_dir = agcodex_core::config::log_dir(&config)?;
204 std::fs::create_dir_all(&log_dir)?;
205 let mut log_file_opts = OpenOptions::new();
207 log_file_opts.create(true).append(true);
208
209 #[cfg(unix)]
214 {
215 use std::os::unix::fs::OpenOptionsExt;
216 log_file_opts.mode(0o600);
217 }
218
219 let log_file = log_file_opts.open(log_dir.join("agcodex-tui.log"))?;
220
221 let (non_blocking, _guard) = non_blocking(log_file);
223
224 let env_filter = || {
226 EnvFilter::try_from_default_env()
227 .unwrap_or_else(|_| EnvFilter::new("agcodex_core=info,codex_tui=info"))
228 };
229
230 let file_layer = tracing_subscriber::fmt::layer()
232 .with_writer(non_blocking)
233 .with_target(false)
234 .with_filter(env_filter());
235
236 if cli.oss {
237 agcodex_ollama::ensure_oss_ready(&config)
238 .await
239 .map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?;
240 }
241
242 let _ = tracing_subscriber::registry().with(file_layer).try_init();
243
244 #[allow(clippy::print_stderr)]
245 #[cfg(not(debug_assertions))]
246 if let Some(latest_version) = updates::get_upgrade_version(&config) {
247 let current_version = env!("CARGO_PKG_VERSION");
248 let exe = std::env::current_exe()?;
249 let managed_by_npm = std::env::var_os("CODEX_MANAGED_BY_NPM").is_some();
250
251 eprintln!(
252 "{} {current_version} -> {latest_version}.",
253 "✨⬆️ Update available!".bold().cyan()
254 );
255
256 if managed_by_npm {
257 let npm_cmd = "npm install -g @openai/codex@latest";
258 eprintln!("Run {} to update.", npm_cmd.cyan().on_black());
259 } else if cfg!(target_os = "macos")
260 && (exe.starts_with("/opt/homebrew") || exe.starts_with("/usr/local"))
261 {
262 let brew_cmd = "brew upgrade codex";
263 eprintln!("Run {} to update.", brew_cmd.cyan().on_black());
264 } else {
265 eprintln!(
266 "See {} for the latest releases and installation options.",
267 "https://github.com/openai/codex/releases/latest"
268 .cyan()
269 .on_black()
270 );
271 }
272
273 eprintln!("");
274 }
275
276 run_ratatui_app(cli, config, should_show_trust_screen)
277 .map_err(|err| std::io::Error::other(err.to_string()))
278}
279
280fn run_ratatui_app(
281 cli: Cli,
282 config: Config,
283 should_show_trust_screen: bool,
284) -> color_eyre::Result<agcodex_core::protocol::TokenUsage> {
285 color_eyre::install()?;
286
287 let prev_hook = std::panic::take_hook();
292 std::panic::set_hook(Box::new(move |info| {
293 tracing::error!("panic: {info}");
294 prev_hook(info);
295 }));
296 let mut terminal = tui::init(&config)?;
297 terminal.clear()?;
298
299 session_log::maybe_init(&config);
301
302 let Cli { prompt, images, .. } = cli;
303 let mut app = App::new(config.clone(), prompt, images, should_show_trust_screen);
304
305 let app_result = app.run(&mut terminal);
306 let usage = app.token_usage();
307
308 restore();
309 session_log::log_session_end();
311 app_result.map(|_| usage)
313}
314
315#[expect(
316 clippy::print_stderr,
317 reason = "TUI should no longer be displayed, so we can write to stderr."
318)]
319fn restore() {
320 if let Err(err) = tui::restore() {
321 eprintln!(
322 "failed to restore terminal. Run `reset` or restart your terminal to recover: {err}"
323 );
324 }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328pub enum LoginStatus {
329 AuthMode(AuthMode),
330 NotAuthenticated,
331}
332
333fn get_login_status(config: &Config) -> LoginStatus {
334 if config.model_provider.requires_openai_auth {
335 let codex_home = config.codex_home.clone();
338 match CodexAuth::from_codex_home(&codex_home, config.preferred_auth_method) {
339 Ok(Some(auth)) => LoginStatus::AuthMode(auth.mode),
340 Ok(None) => LoginStatus::NotAuthenticated,
341 Err(err) => {
342 error!("Failed to read auth.json: {err}");
343 LoginStatus::NotAuthenticated
344 }
345 }
346 } else {
347 LoginStatus::NotAuthenticated
348 }
349}
350
351fn determine_repo_trust_state(
355 config: &mut Config,
356 config_toml: &ConfigToml,
357 approval_policy_overide: Option<AskForApproval>,
358 sandbox_mode_override: Option<SandboxMode>,
359 config_profile_override: Option<String>,
360) -> std::io::Result<bool> {
361 let config_profile = config_toml.get_config_profile(config_profile_override)?;
362
363 if approval_policy_overide.is_some() || sandbox_mode_override.is_some() {
364 Ok(false)
367 } else if config_profile.approval_policy.is_some() {
368 Ok(false)
371 } else if config_toml.approval_policy.is_some() || config_toml.sandbox_mode.is_some() {
372 Ok(false)
375 } else if config_toml.is_cwd_trusted(&config.cwd) {
376 config.approval_policy = AskForApproval::OnRequest;
379 config.sandbox_policy = SandboxPolicy::new_workspace_write_policy();
380 Ok(false)
381 } else {
382 Ok(true)
384 }
385}