// Forbid accidental stdout/stderr writes in the *library* portion of the TUI.
// The standalone `codex-tui` binary prints a short help message before the
// alternate‑screen mode starts; that file opts‑out locally via `allow`.
#![deny(clippy::print_stdout, clippy::print_stderr)]
#![deny(clippy::disallowed_methods)]
use crate::legacy_core::check_execpolicy_for_warnings;
use crate::legacy_core::config::Config;
use crate::legacy_core::config::ConfigBuilder;
use crate::legacy_core::config::ConfigOverrides;
use crate::legacy_core::config::ConfigTomlLoadResult;
use crate::legacy_core::config::load_config_toml_with_layer_stack;
use crate::legacy_core::config::resolve_bootstrap_auth_keyring_backend_kind;
use crate::legacy_core::config::resolve_oss_provider;
use crate::legacy_core::config::resolve_profile_v2_config_path;
use crate::legacy_core::format_exec_policy_error_with_source;
use crate::session_resume::ResolveCwdOutcome;
use crate::session_resume::resolve_cwd_for_resume_or_fork;
pub use crate::startup_error::LocalStateDbStartupError;
use additional_dirs::add_dir_warning_message;
use app::App;
pub use app::AppExitInfo;
pub use app::ExitReason;
use app_server_session::AppServerSession;
use app_server_session::ThreadParamsMode;
use codex_app_server_client::AppServerClient;
use codex_app_server_client::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
use codex_app_server_client::InProcessAppServerClient;
use codex_app_server_client::InProcessClientStartArgs;
use codex_app_server_client::RemoteAppServerClient;
use codex_app_server_client::RemoteAppServerConnectArgs;
pub use codex_app_server_client::RemoteAppServerEndpoint;
use codex_app_server_protocol::Account as AppServerAccount;
use codex_app_server_protocol::AskForApproval;
use codex_app_server_protocol::AuthMode as AppServerAuthMode;
use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::Thread as AppServerThread;
use codex_app_server_protocol::ThreadListCwdFilter;
use codex_app_server_protocol::ThreadListParams;
use codex_app_server_protocol::ThreadSortKey as AppServerThreadSortKey;
use codex_app_server_protocol::ThreadSourceKind;
use codex_cloud_config::cloud_config_bundle_loader_for_storage;
use codex_config::CloudConfigBundleLoader;
use codex_config::ConfigLoadError;
use codex_config::LoaderOverrides;
use codex_config::format_config_error_with_source;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecServerRuntimePaths;
use codex_login::AuthConfig;
use codex_login::default_client::originator;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_login::enforce_login_restrictions;
use codex_protocol::ThreadId;
use codex_protocol::config_types::AltScreenMode;
use codex_protocol::config_types::SandboxMode;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_rollout::StateDbHandle;
use codex_rollout::state_db;
use codex_state::log_db;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::canonicalize_existing_preserving_symlinks;
use codex_utils_home_dir::find_codex_home;
use codex_utils_oss::ensure_oss_provider_ready;
use codex_utils_oss::get_default_model_for_oss_provider;
use color_eyre::eyre::WrapErr;
use cwd_prompt::CwdPromptAction;
pub use session_archive_commands::DeleteConfirmation;
pub use session_archive_commands::SessionArchiveAction;
pub use session_archive_commands::SessionArchiveCommandOptions;
pub use session_archive_commands::run_session_archive_command;
use std::fs::OpenOptions;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
pub use token_usage::TokenUsage;
use tracing::Level;
use tracing::error;
use tracing::warn;
use tracing_appender::non_blocking;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::filter::Targets;
use tracing_subscriber::prelude::*;
use url::Url;
use uuid::Uuid;

pub(crate) use codex_app_server_client::legacy_core;

mod additional_dirs;
mod app;
mod app_backtrack;
mod app_command;
mod app_event;
mod app_event_sender;
mod app_server_approval_conversions;
mod app_server_session;
mod approval_events;
mod ascii_animation;
mod bottom_pane;
mod branch_summary;
mod chatwidget;
mod cli;
mod clipboard_copy;
mod clipboard_paste;
mod collaboration_modes;
mod color;
mod config_update;
pub(crate) mod custom_terminal;
mod pets;
pub use custom_terminal::Terminal;
mod auto_review_denials;
mod cwd_prompt;
mod debug_config;
mod diff_model;
mod diff_render;
mod exec_cell;
mod exec_command;
mod external_agent_config_migration;
mod external_agent_config_migration_flow;
mod external_agent_config_migration_model;
mod external_editor;
mod file_search;
mod frames;
mod get_git_diff;
mod git_action_directives;
mod goal_display;
mod goal_files;
mod history_cell;
mod hooks_rpc;
mod ide_context;
pub(crate) mod insert_history;
pub use insert_history::insert_history_lines;
mod key_hint;
mod keymap;
mod keymap_setup;
mod line_truncation;
pub(crate) mod live_wrap;
pub use live_wrap::RowBuilder;
mod local_chatgpt_auth;
mod markdown;
mod markdown_render;
mod markdown_stream;
mod markdown_text_merge;
mod mention_codec;
mod model_catalog;
mod model_migration;
mod motion;
mod multi_agents;
mod notifications;
#[cfg(any(not(debug_assertions), test))]
mod npm_registry;
pub(crate) mod onboarding;
mod oss_selection;
mod pager_overlay;
mod permission_compat;
pub(crate) mod public_widgets;
mod render;
mod resize_reflow_cap;
mod resume_picker;
mod selection_list;
mod service_tier_resolution;
mod session_archive_commands;
mod session_log;
mod session_resume;
mod session_state;
mod shimmer;
mod skills_helpers;
mod slash_command;
mod startup_error;
mod startup_hooks_review;
mod status;
mod status_indicator_widget;
mod streaming;
mod style;
mod terminal_hyperlinks;
mod terminal_palette;
mod terminal_probe;
mod terminal_title;
mod terminal_visualization_instructions;
mod text_formatting;
mod theme_picker;
mod thread_transcript;
mod token_usage;
mod tooltips;
mod transcript_reflow;
mod tui;
mod ui_consts;
pub(crate) mod update_action;
pub use update_action::UpdateAction;
#[cfg(not(debug_assertions))]
pub use update_action::get_update_action;
mod update_prompt;
#[cfg(any(not(debug_assertions), test))]
mod update_versions;
mod updates;
#[cfg(any(not(debug_assertions), test))]
mod updates_cache;
mod version;
mod width;
#[cfg(any(target_os = "windows", test))]
mod windows_sandbox;
mod workspace_command;

mod wrapping;

mod table_detect;
#[cfg(test)]
pub(crate) mod test_backend;
#[cfg(test)]
pub(crate) mod test_support;

use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
use crate::onboarding::onboarding_screen::run_onboarding_app;
use crate::startup_hooks_review::StartupHooksReviewOutcome;
use crate::startup_hooks_review::load_startup_hooks_review_entry;
use crate::startup_hooks_review::maybe_run_startup_hooks_review;
use crate::tui::Tui;
pub use cli::Cli;
use codex_arg0::Arg0DispatchPaths;
pub use markdown_render::render_markdown_text;
pub use public_widgets::composer_input::ComposerAction;
pub use public_widgets::composer_input::ComposerInput;
// (tests access modules directly within the crate)

const TUI_LOG_FILE_NAME: &str = "codex-tui.log";

#[cfg(unix)]
const AUTO_CONNECT_DAEMON_CONNECT_TIMEOUT: std::time::Duration =
    std::time::Duration::from_millis(50);

#[allow(clippy::too_many_arguments)]
async fn start_embedded_app_server(
    arg0_paths: Arg0DispatchPaths,
    config: Config,
    cli_kv_overrides: Vec<(String, toml::Value)>,
    loader_overrides: LoaderOverrides,
    strict_config: bool,
    cloud_config_bundle: CloudConfigBundleLoader,
    feedback: codex_feedback::CodexFeedback,
    log_db: Option<log_db::LogDbLayer>,
    state_db: Option<StateDbHandle>,
    environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<InProcessAppServerClient> {
    start_embedded_app_server_with(
        arg0_paths,
        config,
        cli_kv_overrides,
        loader_overrides,
        strict_config,
        cloud_config_bundle,
        feedback,
        log_db,
        state_db,
        environment_manager,
        InProcessAppServerClient::start,
    )
    .await
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum AppServerTarget {
    Embedded,
    LocalDaemon { endpoint: RemoteAppServerEndpoint },
    Remote { endpoint: RemoteAppServerEndpoint },
}

impl AppServerTarget {
    pub(crate) fn uses_remote_workspace(&self) -> bool {
        matches!(self, Self::Remote { .. })
    }

    fn thread_params_mode(&self) -> ThreadParamsMode {
        if self.uses_remote_workspace() {
            ThreadParamsMode::Remote
        } else {
            ThreadParamsMode::Embedded
        }
    }
}

async fn init_state_db_for_app_server_target(
    config: &Config,
    app_server_target: &AppServerTarget,
) -> std::io::Result<Option<StateDbHandle>> {
    match app_server_target {
        AppServerTarget::Embedded => state_db::try_init(config).await.map(Some).map_err(|err| {
            let database_path = codex_state::runtime_db_path_for_corruption_error(&err)
                .unwrap_or_else(|| codex_state::state_db_path(config.sqlite_home.as_path()));
            std::io::Error::other(LocalStateDbStartupError::new(
                database_path,
                format!("{err:#}"),
            ))
        }),
        AppServerTarget::LocalDaemon { .. } | AppServerTarget::Remote { .. } => {
            Ok(state_db::get_state_db(config).await)
        }
    }
}

// TODO(jif) delete after 22/11/2026.
fn remove_legacy_tui_log_file(codex_home: &Path) {
    // Shared append-only TUI logs could grow without bound. Existing processes
    // may still hold the file open, so startup cleanup is best effort.
    let _ = std::fs::remove_file(codex_home.join("log").join(TUI_LOG_FILE_NAME));
}

fn remote_addr_has_explicit_port(addr: &str, parsed: &Url) -> bool {
    let Some(host) = parsed.host_str() else {
        return false;
    };
    if parsed.port().is_some() {
        return true;
    }

    let Some((_, rest)) = addr.split_once("://") else {
        return false;
    };
    let authority_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
    let authority = &rest[..authority_end];
    let host_and_port = authority
        .rsplit_once('@')
        .map_or(authority, |(_, host_and_port)| host_and_port);
    let explicit_default_port = match parsed.scheme() {
        "ws" => 80,
        "wss" => 443,
        _ => return false,
    };
    let expected_host = if host.contains(':') {
        format!("[{host}]")
    } else {
        host.to_string()
    };
    host_and_port == format!("{expected_host}:{explicit_default_port}")
}

fn websocket_url_supports_auth_token(parsed: &Url) -> bool {
    match (parsed.scheme(), parsed.host()) {
        ("wss", Some(_)) => true,
        ("ws", Some(url::Host::Domain(domain))) => domain.eq_ignore_ascii_case("localhost"),
        ("ws", Some(url::Host::Ipv4(addr))) => addr.is_loopback(),
        ("ws", Some(url::Host::Ipv6(addr))) => addr.is_loopback(),
        _ => false,
    }
}

pub fn resolve_remote_addr(addr: &str) -> color_eyre::Result<RemoteAppServerEndpoint> {
    if let Some(socket_path) = addr.strip_prefix("unix://") {
        let socket_path = if socket_path.is_empty() {
            let codex_home = find_codex_home().wrap_err("failed to resolve CODEX_HOME")?;
            codex_app_server_client::app_server_control_socket_path(&codex_home)
                .map_err(color_eyre::Report::new)?
        } else {
            AbsolutePathBuf::relative_to_current_dir(socket_path)
                .map_err(color_eyre::Report::new)?
        };
        return Ok(RemoteAppServerEndpoint::UnixSocket { socket_path });
    }

    let parsed = match Url::parse(addr) {
        Ok(parsed) => parsed,
        Err(_) => {
            color_eyre::eyre::bail!(
                "invalid remote address `{addr}`; expected `ws://host:port`, `wss://host:port`, `unix://`, or `unix://PATH`"
            );
        }
    };
    if matches!(parsed.scheme(), "ws" | "wss")
        && parsed.host_str().is_some()
        && remote_addr_has_explicit_port(addr, &parsed)
        && parsed.path() == "/"
        && parsed.query().is_none()
        && parsed.fragment().is_none()
    {
        return Ok(RemoteAppServerEndpoint::WebSocket {
            websocket_url: parsed.to_string(),
            auth_token: None,
        });
    }

    color_eyre::eyre::bail!(
        "invalid remote address `{addr}`; expected `ws://host:port`, `wss://host:port`, `unix://`, or `unix://PATH`"
    );
}

pub fn remote_addr_supports_auth_token(endpoint: &RemoteAppServerEndpoint) -> bool {
    match endpoint {
        RemoteAppServerEndpoint::WebSocket { websocket_url, .. } => {
            Url::parse(websocket_url).is_ok_and(|parsed| websocket_url_supports_auth_token(&parsed))
        }
        RemoteAppServerEndpoint::UnixSocket { .. } => false,
    }
}

async fn connect_remote_app_server(
    endpoint: RemoteAppServerEndpoint,
) -> color_eyre::Result<AppServerClient> {
    let app_server = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
        endpoint,
        client_name: "codex-tui".to_string(),
        client_version: env!("CARGO_PKG_VERSION").to_string(),
        experimental_api: true,
        opt_out_notification_methods: Vec::new(),
        channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
    })
    .await
    .wrap_err("failed to connect to remote app server")?;
    Ok(AppServerClient::Remote(app_server))
}

#[cfg(unix)]
async fn maybe_probe_default_daemon_socket(codex_home: &Path) -> Option<AbsolutePathBuf> {
    let socket_path = codex_app_server_client::app_server_control_socket_path(codex_home).ok()?;
    if !socket_path.as_path().try_exists().unwrap_or(false) {
        return None;
    }

    match tokio::time::timeout(
        AUTO_CONNECT_DAEMON_CONNECT_TIMEOUT,
        tokio::net::UnixStream::connect(socket_path.as_path()),
    )
    .await
    {
        Ok(Ok(_stream)) => Some(socket_path),
        Ok(Err(err)) => {
            tracing::debug!(%err, socket_path = %socket_path.display(), "skipping default app-server daemon socket");
            None
        }
        Err(_) => {
            tracing::debug!(
                socket_path = %socket_path.display(),
                timeout_ms = AUTO_CONNECT_DAEMON_CONNECT_TIMEOUT.as_millis(),
                "timed out probing default app-server daemon socket"
            );
            None
        }
    }
}

#[cfg(not(unix))]
async fn maybe_probe_default_daemon_socket(_codex_home: &Path) -> Option<AbsolutePathBuf> {
    None
}

#[allow(clippy::too_many_arguments)]
async fn start_app_server(
    target: &AppServerTarget,
    arg0_paths: Arg0DispatchPaths,
    config: Config,
    cli_kv_overrides: Vec<(String, toml::Value)>,
    loader_overrides: LoaderOverrides,
    strict_config: bool,
    cloud_config_bundle: CloudConfigBundleLoader,
    feedback: codex_feedback::CodexFeedback,
    log_db: Option<log_db::LogDbLayer>,
    state_db: Option<StateDbHandle>,
    environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<AppServerClient> {
    match target {
        AppServerTarget::Embedded => start_embedded_app_server(
            arg0_paths,
            config,
            cli_kv_overrides,
            loader_overrides,
            strict_config,
            cloud_config_bundle,
            feedback,
            log_db,
            state_db,
            environment_manager,
        )
        .await
        .map(AppServerClient::InProcess),
        AppServerTarget::LocalDaemon { endpoint } | AppServerTarget::Remote { endpoint } => {
            connect_remote_app_server(endpoint.clone()).await
        }
    }
}

pub(crate) async fn start_app_server_for_picker(
    config: &Config,
    target: &AppServerTarget,
    state_db: Option<StateDbHandle>,
    environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<AppServerSession> {
    let app_server = start_app_server(
        target,
        Arg0DispatchPaths::default(),
        config.clone(),
        Vec::new(),
        LoaderOverrides::default(),
        /*strict_config*/ false,
        CloudConfigBundleLoader::default(),
        codex_feedback::CodexFeedback::new(),
        /*log_db*/ None,
        state_db,
        environment_manager,
    )
    .await?;
    Ok(AppServerSession::new(
        app_server,
        target.thread_params_mode(),
    ))
}

#[cfg(test)]
pub(crate) async fn start_embedded_app_server_for_picker(
    config: &Config,
) -> color_eyre::Result<AppServerSession> {
    let state_db = init_state_db_for_app_server_target(config, &AppServerTarget::Embedded).await?;
    start_app_server_for_picker(
        config,
        &AppServerTarget::Embedded,
        state_db,
        Arc::new(EnvironmentManager::default_for_tests()),
    )
    .await
}

#[allow(clippy::too_many_arguments)]
async fn start_embedded_app_server_with<F, Fut>(
    arg0_paths: Arg0DispatchPaths,
    config: Config,
    cli_kv_overrides: Vec<(String, toml::Value)>,
    loader_overrides: LoaderOverrides,
    strict_config: bool,
    cloud_config_bundle: CloudConfigBundleLoader,
    feedback: codex_feedback::CodexFeedback,
    log_db: Option<log_db::LogDbLayer>,
    state_db: Option<StateDbHandle>,
    environment_manager: Arc<EnvironmentManager>,
    start_client: F,
) -> color_eyre::Result<InProcessAppServerClient>
where
    F: FnOnce(InProcessClientStartArgs) -> Fut,
    Fut: Future<Output = std::io::Result<InProcessAppServerClient>>,
{
    let config_warnings = config
        .startup_warnings
        .iter()
        .map(|warning| ConfigWarningNotification {
            summary: warning.clone(),
            details: None,
            path: None,
            range: None,
        })
        .collect();
    let client = start_client(InProcessClientStartArgs {
        arg0_paths,
        config: Arc::new(config),
        cli_overrides: cli_kv_overrides,
        loader_overrides,
        strict_config,
        cloud_config_bundle,
        feedback,
        log_db,
        state_db,
        environment_manager,
        config_warnings,
        session_source: serde_json::from_value(serde_json::json!("cli"))
            .unwrap_or_else(|err| panic!("cli session source should deserialize: {err}")),
        enable_codex_api_key_env: false,
        client_name: "codex-tui".to_string(),
        client_version: env!("CARGO_PKG_VERSION").to_string(),
        experimental_api: true,
        opt_out_notification_methods: Vec::new(),
        channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
    })
    .await
    .wrap_err("failed to start embedded app server")?;
    Ok(client)
}

async fn shutdown_app_server_if_present(app_server: Option<AppServerSession>) {
    if let Some(app_server) = app_server
        && let Err(err) = app_server.shutdown().await
    {
        warn!(%err, "Failed to shut down temporary embedded app server");
    }
}

fn session_target_from_app_server_thread(
    thread: AppServerThread,
) -> Option<resume_picker::SessionTarget> {
    match ThreadId::from_string(&thread.id) {
        Ok(thread_id) => Some(resume_picker::SessionTarget {
            path: thread.path,
            thread_id,
        }),
        Err(err) => {
            warn!(
                thread_id = thread.id,
                %err,
                "Ignoring app-server thread with invalid thread id during TUI session lookup"
            );
            None
        }
    }
}

pub(crate) fn resume_source_kinds(include_non_interactive: bool) -> Vec<ThreadSourceKind> {
    let mut source_kinds = vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode];
    if include_non_interactive {
        // `thread/list` treats omitted and empty `sourceKinds` as interactive-only,
        // so include-non-interactive has to name the user-resumable non-interactive
        // sources explicitly until the API grows an unfiltered request.
        source_kinds.extend([ThreadSourceKind::Exec, ThreadSourceKind::AppServer]);
    }
    source_kinds
}

async fn lookup_session_target_by_name_with_app_server(
    app_server: &mut AppServerSession,
    name: &str,
) -> color_eyre::Result<Option<resume_picker::SessionTarget>> {
    let mut cursor = None;
    loop {
        let response = app_server
            .thread_list(ThreadListParams {
                cursor: cursor.clone(),
                limit: Some(100),
                sort_key: Some(AppServerThreadSortKey::UpdatedAt),
                sort_direction: None,
                model_providers: None,
                source_kinds: Some(vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode]),
                archived: Some(false),
                cwd: None,
                use_state_db_only: false,
                search_term: Some(name.to_string()),
            })
            .await?;
        if let Some(thread) = response
            .data
            .into_iter()
            .find(|thread| thread.name.as_deref() == Some(name))
        {
            return Ok(session_target_from_app_server_thread(thread));
        }
        if response.next_cursor.is_none() {
            return Ok(None);
        }
        cursor = response.next_cursor;
    }
}

async fn lookup_session_target_with_app_server(
    app_server: &mut AppServerSession,
    id_or_name: &str,
) -> color_eyre::Result<Option<resume_picker::SessionTarget>> {
    if Uuid::parse_str(id_or_name).is_ok() {
        let thread_id = match ThreadId::from_string(id_or_name) {
            Ok(thread_id) => thread_id,
            Err(err) => {
                warn!(
                    session = id_or_name,
                    %err,
                    "Failed to parse session id during TUI lookup"
                );
                return Ok(None);
            }
        };
        return match app_server
            .thread_read(thread_id, /*include_turns*/ false)
            .await
        {
            Ok(thread) => Ok(session_target_from_app_server_thread(thread)),
            Err(err) => {
                warn!(
                    session = id_or_name,
                    %err,
                    "thread/read failed during TUI session lookup"
                );
                Ok(None)
            }
        };
    }

    lookup_session_target_by_name_with_app_server(app_server, id_or_name).await
}

async fn lookup_latest_session_target_with_app_server(
    app_server: &mut AppServerSession,
    config: &Config,
    cwd_filter: Option<&Path>,
    include_non_interactive: bool,
) -> color_eyre::Result<Option<resume_picker::SessionTarget>> {
    let uses_remote_workspace = app_server.uses_remote_workspace();
    for lookup_mode in [
        LatestSessionLookupMode::StateDbOnly,
        LatestSessionLookupMode::ScanAndRepair,
    ] {
        let response = app_server
            .thread_list(latest_session_lookup_params(
                uses_remote_workspace,
                config,
                cwd_filter,
                include_non_interactive,
                lookup_mode,
            ))
            .await?;
        let target = response
            .data
            .into_iter()
            .find_map(session_target_from_app_server_thread);
        if target.as_ref().is_some_and(|target| {
            uses_remote_workspace || target.path.as_deref().is_some_and(std::path::Path::exists)
        }) {
            return Ok(target);
        }
    }
    Ok(None)
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LatestSessionLookupMode {
    StateDbOnly,
    ScanAndRepair,
}

fn latest_session_lookup_params(
    uses_remote_workspace: bool,
    config: &Config,
    cwd_filter: Option<&Path>,
    include_non_interactive: bool,
    lookup_mode: LatestSessionLookupMode,
) -> ThreadListParams {
    ThreadListParams {
        cursor: None,
        limit: Some(1),
        sort_key: Some(AppServerThreadSortKey::UpdatedAt),
        sort_direction: None,
        model_providers: if uses_remote_workspace {
            None
        } else {
            Some(vec![config.model_provider_id.clone()])
        },
        source_kinds: Some(resume_source_kinds(include_non_interactive)),
        archived: Some(false),
        cwd: cwd_filter.map(|cwd| ThreadListCwdFilter::One(cwd.to_string_lossy().to_string())),
        use_state_db_only: match lookup_mode {
            LatestSessionLookupMode::StateDbOnly => true,
            LatestSessionLookupMode::ScanAndRepair => false,
        },
        search_term: None,
    }
}

fn config_cwd_for_app_server_target(
    cwd: Option<&Path>,
    app_server_target: &AppServerTarget,
    environment_manager: &EnvironmentManager,
) -> std::io::Result<Option<AbsolutePathBuf>> {
    if app_server_target.uses_remote_workspace()
        || environment_manager
            .default_environment()
            .is_some_and(|environment| environment.is_remote())
    {
        return Ok(None);
    }

    let cwd = match cwd {
        Some(path) => {
            AbsolutePathBuf::from_absolute_path(canonicalize_existing_preserving_symlinks(path)?)
        }
        None => AbsolutePathBuf::current_dir(),
    }?;
    Ok(Some(cwd))
}

fn should_load_configured_environments(
    loader_overrides: &LoaderOverrides,
    app_server_target: &AppServerTarget,
) -> bool {
    !loader_overrides.ignore_user_config && !app_server_target.uses_remote_workspace()
}

fn latest_session_cwd_filter<'a>(
    uses_remote_workspace: bool,
    remote_cwd_override: Option<&'a Path>,
    config: &'a Config,
    show_all: bool,
) -> Option<&'a Path> {
    if show_all {
        return None;
    }

    if uses_remote_workspace {
        remote_cwd_override
    } else {
        Some(config.cwd.as_path())
    }
}

fn app_server_target_for_launch(
    explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,
    default_daemon_socket: Option<AbsolutePathBuf>,
    can_reuse_implicit_local_daemon: bool,
) -> AppServerTarget {
    match explicit_remote_endpoint {
        Some(endpoint) => AppServerTarget::Remote { endpoint },
        None if can_reuse_implicit_local_daemon => {
            default_daemon_socket.map_or(AppServerTarget::Embedded, |socket_path| {
                AppServerTarget::LocalDaemon {
                    endpoint: RemoteAppServerEndpoint::UnixSocket { socket_path },
                }
            })
        }
        None => AppServerTarget::Embedded,
    }
}

fn loader_overrides_are_default(loader_overrides: &LoaderOverrides) -> bool {
    let loader_overrides_are_default = loader_overrides.user_config_path.is_none()
        && loader_overrides.user_config_profile.is_none()
        && loader_overrides.managed_config_path.is_none()
        && loader_overrides.system_config_path.is_none()
        && loader_overrides.system_requirements_path.is_none()
        && !loader_overrides.ignore_managed_requirements
        && !loader_overrides.ignore_user_config
        && !loader_overrides.ignore_user_and_project_exec_policy_rules
        && loader_overrides
            .macos_managed_config_requirements_base64
            .is_none();
    #[cfg(target_os = "macos")]
    let loader_overrides_are_default =
        loader_overrides_are_default && loader_overrides.managed_preferences_base64.is_none();
    loader_overrides_are_default
}

fn can_reuse_implicit_local_daemon(
    cli_kv_overrides: &[(String, toml::Value)],
    loader_overrides: &LoaderOverrides,
    strict_config: bool,
    has_non_replayable_launch_overrides: bool,
) -> bool {
    // A reused daemon cannot adopt this invocation's full launch config state.
    cli_kv_overrides.is_empty()
        && loader_overrides_are_default(loader_overrides)
        && !strict_config
        && !has_non_replayable_launch_overrides
}

pub async fn run_main(
    mut cli: Cli,
    arg0_paths: Arg0DispatchPaths,
    loader_overrides: LoaderOverrides,
    explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,
) -> std::io::Result<AppExitInfo> {
    let strict_config = cli.strict_config;
    let (sandbox_mode, approval_policy) = if cli.dangerously_bypass_approvals_and_sandbox {
        (
            Some(SandboxMode::DangerFullAccess),
            Some(AskForApproval::Never.to_core()),
        )
    } else {
        (
            cli.sandbox_mode.map(Into::<SandboxMode>::into),
            cli.approval_policy.map(Into::into),
        )
    };

    // Map the legacy --search flag to the canonical web_search mode.
    if cli.web_search {
        cli.config_overrides
            .raw_overrides
            .push("web_search=\"live\"".to_string());
    }

    // When using `--oss`, let the bootstrapper pick the model (defaulting to
    // gpt-oss:20b) and ensure it is present locally. Also, force the built‑in
    let raw_overrides = cli.config_overrides.raw_overrides.clone();
    // `oss` model provider.
    let overrides_cli = codex_utils_cli::CliConfigOverrides { raw_overrides };
    let cli_kv_overrides = match overrides_cli.parse_overrides() {
        // Parse `-c` overrides from the CLI.
        Ok(v) => v,
        #[allow(clippy::print_stderr)]
        Err(e) => {
            eprintln!("Error parsing -c overrides: {e}");
            std::process::exit(1);
        }
    };

    // we load config.toml here to determine project state.
    #[allow(clippy::print_stderr)]
    let codex_home = match find_codex_home() {
        Ok(codex_home) => codex_home.to_path_buf(),
        Err(err) => {
            eprintln!("Error finding codex home: {err}");
            std::process::exit(1);
        }
    };

    let mut launch_loader_overrides = loader_overrides.clone();
    if let Some(profile_v2) = cli.config_profile_v2.as_ref() {
        let user_config_path = resolve_profile_v2_config_path(&codex_home, profile_v2);
        launch_loader_overrides.user_config_path = Some(user_config_path);
        launch_loader_overrides.user_config_profile = Some(profile_v2.clone());
    }
    let reuse_implicit_local_daemon = can_reuse_implicit_local_daemon(
        &cli_kv_overrides,
        &launch_loader_overrides,
        strict_config,
        cli.bypass_hook_trust,
    );
    let default_daemon = if explicit_remote_endpoint.is_none() && reuse_implicit_local_daemon {
        maybe_probe_default_daemon_socket(&codex_home).await
    } else {
        None
    };
    let app_server_target = app_server_target_for_launch(
        explicit_remote_endpoint,
        default_daemon,
        reuse_implicit_local_daemon,
    );
    let remote_cwd_override = cli
        .cwd
        .clone()
        .filter(|_| app_server_target.uses_remote_workspace());

    let local_runtime_paths = ExecServerRuntimePaths::from_optional_paths(
        arg0_paths.codex_self_exe.clone(),
        arg0_paths.codex_linux_sandbox_exe.clone(),
    )?;
    let environment_manager =
        if should_load_configured_environments(&loader_overrides, &app_server_target) {
            EnvironmentManager::from_codex_home(codex_home.clone(), Some(local_runtime_paths)).await
        } else {
            EnvironmentManager::from_env(Some(local_runtime_paths)).await
        }
        .map(Arc::new)
        .map_err(std::io::Error::other)?;
    let cwd = cli.cwd.clone();
    let config_cwd =
        config_cwd_for_app_server_target(cwd.as_deref(), &app_server_target, &environment_manager)?;
    let mut loader_overrides = loader_overrides;
    if let Some(profile_v2) = cli.config_profile_v2.as_ref() {
        let user_config_path = resolve_profile_v2_config_path(&codex_home, profile_v2);
        loader_overrides.user_config_path = Some(user_config_path);
        loader_overrides.user_config_profile = Some(profile_v2.clone());
    }

    let bootstrap_config = load_bootstrap_config_or_exit(
        &codex_home,
        config_cwd.as_ref(),
        cli_kv_overrides.clone(),
        loader_overrides.clone(),
        strict_config,
        CloudConfigBundleLoader::default(),
    )
    .await;
    let bootstrap_config_toml = &bootstrap_config.config_toml;

    let chatgpt_base_url = bootstrap_config_toml
        .chatgpt_base_url
        .clone()
        .unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string());
    let cloud_config_bundle = cloud_config_bundle_loader_for_storage(
        codex_home.to_path_buf(),
        /*enable_codex_api_key_env*/ false,
        bootstrap_config_toml
            .cli_auth_credentials_store
            .unwrap_or_default(),
        resolve_bootstrap_auth_keyring_backend_kind(&bootstrap_config)?,
        chatgpt_base_url,
    )
    .await;

    let cwd_override = if app_server_target.uses_remote_workspace() {
        None
    } else {
        cwd.clone()
    };

    let mut manually_selected_oss_provider = None;
    let model_provider_override = if cli.oss {
        let bootstrap_config_with_cloud_config;
        let config_toml_for_oss = if cli.oss_provider.is_none() {
            // The first load intentionally skips cloud config so we can read
            // auth/base-url settings needed to fetch the bundle. If OSS mode
            // needs a default provider from config, reload with the bundle.
            bootstrap_config_with_cloud_config = load_bootstrap_config_or_exit(
                &codex_home,
                config_cwd.as_ref(),
                cli_kv_overrides.clone(),
                loader_overrides.clone(),
                strict_config,
                cloud_config_bundle.clone(),
            )
            .await;
            &bootstrap_config_with_cloud_config.config_toml
        } else {
            bootstrap_config_toml
        };

        let resolved = resolve_oss_provider(cli.oss_provider.as_deref(), config_toml_for_oss);

        if let Some(provider) = resolved {
            Some(provider)
        } else {
            // No provider configured, prompt the user
            let selection = oss_selection::select_oss_provider().await?;
            let provider = selection.provider;
            if provider == "__CANCELLED__" {
                return Err(std::io::Error::other(
                    "OSS provider selection was cancelled by user",
                ));
            }
            if selection.manually_selected {
                manually_selected_oss_provider = Some(provider.clone());
            }
            Some(provider)
        }
    } else {
        None
    };

    // When using `--oss`, let the bootstrapper pick the model based on selected provider
    let model = if let Some(model) = &cli.model {
        Some(model.clone())
    } else if cli.oss {
        // Use the provider from model_provider_override
        model_provider_override
            .as_ref()
            .and_then(|provider_id| get_default_model_for_oss_provider(provider_id))
            .map(std::borrow::ToOwned::to_owned)
    } else {
        None // No model specified, will use the default.
    };

    let additional_dirs = cli.add_dir.clone();

    let overrides = ConfigOverrides {
        model,
        approval_policy,
        sandbox_mode,
        cwd: cwd_override,
        model_provider: model_provider_override.clone(),
        codex_self_exe: arg0_paths.codex_self_exe.clone(),
        codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
        main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
        show_raw_agent_reasoning: cli.oss.then_some(true),
        bypass_hook_trust: cli.bypass_hook_trust.then_some(true),
        additional_writable_roots: additional_dirs,
        ..Default::default()
    };

    let mut config = load_config_or_exit(
        cli_kv_overrides.clone(),
        overrides.clone(),
        loader_overrides.clone(),
        cloud_config_bundle.clone(),
        strict_config,
    )
    .await;

    remove_legacy_tui_log_file(config.codex_home.as_path());

    let otel_originator = originator().value;
    let otel = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        codex_app_server_client::build_otel_provider(
            &config,
            env!("CARGO_PKG_VERSION"),
            /*service_name_override*/ None,
            /*default_analytics_enabled*/ true,
        )
    })) {
        Ok(Ok(otel)) => otel,
        Ok(Err(e)) => {
            #[allow(clippy::print_stderr)]
            {
                eprintln!("Could not create otel exporter: {e}");
            }
            None
        }
        Err(_) => {
            #[allow(clippy::print_stderr)]
            {
                eprintln!("Could not create otel exporter: panicked during initialization");
            }
            None
        }
    };
    if let Some(metrics) = otel.as_ref().and_then(codex_otel::OtelProvider::metrics) {
        let _ = codex_otel::record_process_start_once(metrics, otel_originator.as_str());
        let telemetry =
            codex_rollout::sqlite_telemetry_recorder(metrics.clone(), otel_originator.as_str());
        let _ = codex_state::install_process_db_telemetry(telemetry);
    }
    let state_db = init_state_db_for_app_server_target(&config, &app_server_target).await?;

    let effective_toml = config.config_layer_stack.effective_config();
    match effective_toml.try_into() {
        Ok(config_toml) => {
            match codex_app_server_client::migrate_personality_if_needed(
                &config.codex_home,
                &config_toml,
                state_db.clone(),
            )
            .await
            {
                Ok(true) => {
                    config = load_config_or_exit(
                        cli_kv_overrides.clone(),
                        overrides.clone(),
                        loader_overrides.clone(),
                        cloud_config_bundle.clone(),
                        strict_config,
                    )
                    .await;
                }
                Ok(false) => {}
                Err(err) => {
                    tracing::warn!(error = %err, "failed to run personality migration");
                }
            }
        }
        Err(err) => {
            tracing::warn!(error = %err, "failed to deserialize config for personality migration");
        }
    }
    let config_toml_log_dir_configured = config
        .config_layer_stack
        .effective_config()
        .as_table()
        .is_some_and(|table| table.contains_key("log_dir"));

    #[allow(clippy::print_stderr)]
    match check_execpolicy_for_warnings(&config.config_layer_stack).await {
        Ok(None) => {}
        Ok(Some(err)) | Err(err) => {
            eprintln!(
                "Error loading rules:\n{}",
                format_exec_policy_error_with_source(&err)
            );
            std::process::exit(1);
        }
    }

    set_default_client_residency_requirement(config.enforce_residency.value());

    if let Some(warning) = add_dir_warning_message(
        &cli.add_dir,
        &config.permissions.effective_permission_profile(),
        config.cwd.as_path(),
    ) {
        #[allow(clippy::print_stderr)]
        {
            eprintln!("Error adding directories: {warning}");
            std::process::exit(1);
        }
    }

    if !app_server_target.uses_remote_workspace() {
        #[allow(clippy::print_stderr)]
        if let Err(err) = enforce_login_restrictions(&AuthConfig {
            codex_home: config.codex_home.to_path_buf(),
            auth_credentials_store_mode: config.cli_auth_credentials_store_mode,
            keyring_backend_kind: config.auth_keyring_backend_kind(),
            forced_login_method: config.forced_login_method,
            forced_chatgpt_workspace_id: config.forced_chatgpt_workspace_id.clone(),
            chatgpt_base_url: Some(config.chatgpt_base_url.clone()),
        })
        .await
        {
            eprintln!("{err}");
            std::process::exit(1);
        }
    }

    let (tui_file_layer, _tui_file_log_guard) = if config_toml_log_dir_configured {
        let log_dir = config.log_dir.clone();
        std::fs::create_dir_all(&log_dir)?;
        let mut log_file_opts = OpenOptions::new();
        log_file_opts.create(true).append(true);

        // Ensure the file is only readable and writable by the current user.
        // Doing the equivalent to `chmod 600` on Windows is quite a bit more
        // code and requires the Windows API crates.
        #[cfg(unix)]
        {
            use std::os::unix::fs::OpenOptionsExt;
            log_file_opts.mode(0o600);
        }

        let log_file = log_file_opts.open(log_dir.join(TUI_LOG_FILE_NAME))?;
        let (non_blocking, guard) = non_blocking(log_file);
        let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
            EnvFilter::new("codex_core=info,codex_tui=info,codex_rmcp_client=info")
        });
        let file_layer = tracing_subscriber::fmt::layer()
            .with_writer(non_blocking)
            .with_target(true)
            .with_ansi(false)
            .with_span_events(
                tracing_subscriber::fmt::format::FmtSpan::NEW
                    | tracing_subscriber::fmt::format::FmtSpan::CLOSE,
            )
            .with_filter(env_filter);
        (Some(file_layer), Some(guard))
    } else {
        (None, None)
    };

    let feedback = codex_feedback::CodexFeedback::new();
    let feedback_layer = feedback.logger_layer();
    let feedback_metadata_layer = feedback.metadata_layer();

    if cli.oss && model_provider_override.is_some() {
        // We're in the oss section, so provider_id should be Some
        // Let's handle None case gracefully though just in case
        let provider_id = match model_provider_override.as_ref() {
            Some(id) => id,
            None => {
                error!("OSS provider unexpectedly not set when oss flag is used");
                return Err(std::io::Error::other(
                    "OSS provider not set but oss flag was used",
                ));
            }
        };
        ensure_oss_provider_ready(provider_id, &config).await?;
    }

    let otel_logger_layer = otel.as_ref().and_then(|o| o.logger_layer());

    let otel_tracing_layer = otel.as_ref().and_then(|o| o.tracing_layer());

    let log_db = state_db.clone().map(log_db::start);
    let log_db_layer = log_db
        .clone()
        .map(|layer| layer.with_filter(Targets::new().with_default(Level::TRACE)));

    let _ = tracing_subscriber::registry()
        .with(tui_file_layer)
        .with(feedback_layer)
        .with(feedback_metadata_layer)
        .with(log_db_layer)
        .with(otel_logger_layer)
        .with(otel_tracing_layer)
        .try_init();

    run_ratatui_app(
        cli,
        arg0_paths,
        loader_overrides,
        strict_config,
        app_server_target,
        remote_cwd_override,
        config,
        manually_selected_oss_provider,
        overrides,
        cli_kv_overrides,
        cloud_config_bundle,
        feedback,
        log_db,
        state_db,
        environment_manager,
    )
    .await
    .map_err(|err| std::io::Error::other(err.to_string()))
}

#[allow(clippy::too_many_arguments)]
async fn run_ratatui_app(
    cli: Cli,
    arg0_paths: Arg0DispatchPaths,
    loader_overrides: LoaderOverrides,
    strict_config: bool,
    app_server_target: AppServerTarget,
    remote_cwd_override: Option<PathBuf>,
    initial_config: Config,
    manually_selected_oss_provider: Option<String>,
    overrides: ConfigOverrides,
    cli_kv_overrides: Vec<(String, toml::Value)>,
    mut cloud_config_bundle: CloudConfigBundleLoader,
    feedback: codex_feedback::CodexFeedback,
    log_db: Option<log_db::LogDbLayer>,
    state_db: Option<StateDbHandle>,
    environment_manager: Arc<EnvironmentManager>,
) -> color_eyre::Result<AppExitInfo> {
    let uses_remote_workspace = app_server_target.uses_remote_workspace();
    color_eyre::install()?;

    tooltips::announcement::prewarm();

    // Forward panic reports through tracing so they appear in the UI status
    // line, but do not swallow the default/color-eyre panic handler.
    // Chain to the previous hook so users still get a rich panic report
    // (including backtraces) after we restore the terminal.
    let prev_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        tracing::error!("panic: {info}");
        prev_hook(info);
    }));
    let mut initialized_terminal = tui::init()?;
    initialized_terminal.terminal.clear()?;

    let mut tui = Tui::new(
        initialized_terminal.terminal,
        initialized_terminal.enhanced_keys_supported,
        initialized_terminal.stderr_guard,
    );
    let mut terminal_restore_guard = TerminalRestoreGuard::new();

    #[cfg(not(debug_assertions))]
    {
        use crate::update_prompt::UpdatePromptOutcome;

        let skip_update_prompt = cli.prompt.as_ref().is_some_and(|prompt| !prompt.is_empty());
        if !skip_update_prompt {
            match update_prompt::run_update_prompt_if_needed(&mut tui, &initial_config).await? {
                UpdatePromptOutcome::Continue => {}
                UpdatePromptOutcome::RunUpdate(action) => {
                    terminal_restore_guard.restore()?;
                    return Ok(AppExitInfo {
                        token_usage: crate::token_usage::TokenUsage::default(),
                        thread_id: None,
                        resume_hint: None,
                        update_action: Some(action),
                        exit_reason: ExitReason::UserRequested,
                    });
                }
            }
        }
    }

    // Initialize high-fidelity session event logging if enabled.
    session_log::maybe_init(&initial_config);

    let app_server_session = match start_app_server(
        &app_server_target,
        arg0_paths.clone(),
        initial_config.clone(),
        cli_kv_overrides.clone(),
        loader_overrides.clone(),
        strict_config,
        cloud_config_bundle.clone(),
        feedback.clone(),
        log_db.clone(),
        state_db.clone(),
        environment_manager.clone(),
    )
    .await
    {
        Ok(app_server) => AppServerSession::new(app_server, app_server_target.thread_params_mode()),
        Err(err) => {
            terminal_restore_guard.restore_silently();
            session_log::log_session_end();
            return Err(err);
        }
    }
    .with_remote_cwd_override(remote_cwd_override.clone());
    if let Some(provider) = manually_selected_oss_provider.as_deref()
        && let Err(err) = config_update::write_config_batch(
            app_server_session.request_handle(),
            vec![config_update::build_oss_provider_edit(provider)],
        )
        .await
    {
        warn!(
            %err,
            provider,
            "Failed to persist selected OSS provider preference"
        );
    }
    let mut app_server = Some(app_server_session);

    let should_show_trust_screen_flag =
        !uses_remote_workspace && should_show_trust_screen(&initial_config);
    #[cfg(target_os = "windows")]
    let mut trust_decision_was_made = false;
    let login_status = if initial_config.model_provider.requires_openai_auth {
        let Some(app_server) = app_server.as_mut() else {
            unreachable!("app server should exist when auth is required");
        };
        get_login_status(app_server, &initial_config).await?
    } else {
        LoginStatus::NotAuthenticated
    };
    let should_show_onboarding =
        should_show_onboarding(login_status, &initial_config, should_show_trust_screen_flag);

    let config = if should_show_onboarding {
        let show_login_screen = should_show_login_screen(login_status, &initial_config);
        let onboarding_result = run_onboarding_app(
            OnboardingScreenArgs {
                show_login_screen,
                show_trust_screen: should_show_trust_screen_flag,
                login_status,
                app_server_request_handle: app_server
                    .as_ref()
                    .map(AppServerSession::request_handle),
                config: initial_config.clone(),
            },
            if show_login_screen {
                app_server.as_mut()
            } else {
                None
            },
            &mut tui,
        )
        .await?;
        if onboarding_result.should_exit {
            shutdown_app_server_if_present(app_server.take()).await;
            terminal_restore_guard.restore_silently();
            session_log::log_session_end();
            let _ = tui.terminal.clear();
            return Ok(AppExitInfo {
                token_usage: crate::token_usage::TokenUsage::default(),
                thread_id: None,
                resume_hint: None,
                update_action: None,
                exit_reason: ExitReason::UserRequested,
            });
        }
        #[cfg(target_os = "windows")]
        {
            trust_decision_was_made = onboarding_result.directory_trust_persisted;
        }
        // If this onboarding run included the login step, always refresh the cloud config bundle
        // and rebuild config. This avoids missing newly available cloud-managed policy due to login
        // status detection edge cases.
        if show_login_screen && !uses_remote_workspace {
            cloud_config_bundle = cloud_config_bundle_loader_for_storage(
                initial_config.codex_home.to_path_buf(),
                /*enable_codex_api_key_env*/ false,
                initial_config.cli_auth_credentials_store_mode,
                initial_config.auth_keyring_backend_kind(),
                initial_config.chatgpt_base_url.clone(),
            )
            .await;
        }

        // If the user made an explicit trust decision, or we showed the login flow, reload config
        // so current process state reflects persisted trust/auth changes.
        if onboarding_result.directory_trust_persisted
            || (show_login_screen && !uses_remote_workspace)
        {
            load_config_or_exit(
                cli_kv_overrides.clone(),
                overrides.clone(),
                loader_overrides.clone(),
                cloud_config_bundle.clone(),
                strict_config,
            )
            .await
        } else {
            initial_config
        }
    } else {
        initial_config
    };

    let mut missing_session_exit = |id_str: &str, action: &str| {
        error!("Error finding conversation path: {id_str}");
        terminal_restore_guard.restore_silently();
        session_log::log_session_end();
        let _ = tui.terminal.clear();
        Ok(AppExitInfo {
            token_usage: crate::token_usage::TokenUsage::default(),
            thread_id: None,
            resume_hint: None,
            update_action: None,
            exit_reason: ExitReason::Fatal(format!(
                "No saved session found with ID {id_str}. Run `codex {action}` without an ID to choose from existing sessions."
            )),
        })
    };

    let use_fork = cli.fork_picker || cli.fork_last || cli.fork_session_id.is_some();
    let session_selection = if use_fork {
        if let Some(id_str) = cli.fork_session_id.as_deref() {
            let Some(startup_app_server) = app_server.as_mut() else {
                unreachable!("app server should be initialized for --fork <id>");
            };
            match lookup_session_target_with_app_server(startup_app_server, id_str).await? {
                Some(target_session) => resume_picker::SessionSelection::Fork(target_session),
                None => {
                    shutdown_app_server_if_present(app_server.take()).await;
                    return missing_session_exit(id_str, "fork");
                }
            }
        } else if cli.fork_last {
            let filter_cwd = latest_session_cwd_filter(
                uses_remote_workspace,
                remote_cwd_override.as_deref(),
                &config,
                cli.fork_show_all,
            );
            let Some(app_server) = app_server.as_mut() else {
                unreachable!("app server should be initialized for --fork --last");
            };
            match lookup_latest_session_target_with_app_server(
                app_server, &config, filter_cwd, /*include_non_interactive*/ false,
            )
            .await?
            {
                Some(target_session) => resume_picker::SessionSelection::Fork(target_session),
                None => resume_picker::SessionSelection::StartFresh,
            }
        } else if cli.fork_picker {
            let Some(app_server) = app_server.take() else {
                unreachable!("app server should be initialized for --fork picker");
            };
            match resume_picker::run_fork_picker_with_app_server(
                &mut tui,
                &config,
                cli.fork_show_all,
                app_server,
            )
            .await?
            {
                resume_picker::SessionSelection::Exit => {
                    terminal_restore_guard.restore_silently();
                    session_log::log_session_end();
                    return Ok(AppExitInfo {
                        token_usage: crate::token_usage::TokenUsage::default(),
                        thread_id: None,
                        resume_hint: None,
                        update_action: None,
                        exit_reason: ExitReason::UserRequested,
                    });
                }
                other => other,
            }
        } else {
            resume_picker::SessionSelection::StartFresh
        }
    } else if let Some(id_str) = cli.resume_session_id.as_deref() {
        let Some(startup_app_server) = app_server.as_mut() else {
            unreachable!("app server should be initialized for --resume <id>");
        };
        match lookup_session_target_with_app_server(startup_app_server, id_str).await? {
            Some(target_session) => resume_picker::SessionSelection::Resume(target_session),
            None => {
                shutdown_app_server_if_present(app_server.take()).await;
                return missing_session_exit(id_str, "resume");
            }
        }
    } else if cli.resume_last {
        let filter_cwd = latest_session_cwd_filter(
            uses_remote_workspace,
            remote_cwd_override.as_deref(),
            &config,
            cli.resume_show_all,
        );
        let Some(app_server) = app_server.as_mut() else {
            unreachable!("app server should be initialized for --resume --last");
        };
        match lookup_latest_session_target_with_app_server(
            app_server,
            &config,
            filter_cwd,
            cli.resume_include_non_interactive,
        )
        .await?
        {
            Some(target_session) => resume_picker::SessionSelection::Resume(target_session),
            None => resume_picker::SessionSelection::StartFresh,
        }
    } else if cli.resume_picker {
        let Some(app_server) = app_server.take() else {
            unreachable!("app server should be initialized for --resume picker");
        };
        match resume_picker::run_resume_picker_with_app_server(
            &mut tui,
            &config,
            cli.resume_show_all,
            cli.resume_include_non_interactive,
            app_server,
        )
        .await?
        {
            resume_picker::SessionSelection::Exit => {
                terminal_restore_guard.restore_silently();
                session_log::log_session_end();
                return Ok(AppExitInfo {
                    token_usage: crate::token_usage::TokenUsage::default(),
                    thread_id: None,
                    resume_hint: None,
                    update_action: None,
                    exit_reason: ExitReason::UserRequested,
                });
            }
            other => other,
        }
    } else {
        resume_picker::SessionSelection::StartFresh
    };

    let current_cwd = config.cwd.clone();
    let allow_prompt = !uses_remote_workspace && cli.cwd.is_none();
    let action_and_target_session_if_resume_or_fork = match &session_selection {
        resume_picker::SessionSelection::Resume(target_session) => {
            Some((CwdPromptAction::Resume, target_session))
        }
        resume_picker::SessionSelection::Fork(target_session) => {
            Some((CwdPromptAction::Fork, target_session))
        }
        _ => None,
    };
    let fallback_cwd = match action_and_target_session_if_resume_or_fork {
        Some((action, target_session)) => {
            if uses_remote_workspace {
                Some(current_cwd.to_path_buf())
            } else {
                match resolve_cwd_for_resume_or_fork(
                    &mut tui,
                    state_db.as_deref(),
                    &current_cwd,
                    target_session.thread_id,
                    target_session.path.as_deref(),
                    action,
                    allow_prompt,
                )
                .await?
                {
                    ResolveCwdOutcome::Continue(cwd) => cwd,
                    ResolveCwdOutcome::Exit => {
                        terminal_restore_guard.restore_silently();
                        session_log::log_session_end();
                        return Ok(AppExitInfo {
                            token_usage: crate::token_usage::TokenUsage::default(),
                            thread_id: None,
                            resume_hint: None,
                            update_action: None,
                            exit_reason: ExitReason::UserRequested,
                        });
                    }
                }
            }
        }
        None => None,
    };

    let picker_cancelled_without_selection = matches!(
        session_selection,
        resume_picker::SessionSelection::StartFresh
    ) && (cli.resume_picker || cli.fork_picker);

    let mut config = match &session_selection {
        resume_picker::SessionSelection::Resume(_) | resume_picker::SessionSelection::Fork(_) => {
            load_config_or_exit_with_fallback_cwd(
                cli_kv_overrides.clone(),
                overrides.clone(),
                loader_overrides.clone(),
                cloud_config_bundle.clone(),
                strict_config,
                fallback_cwd,
            )
            .await
        }
        resume_picker::SessionSelection::StartFresh if picker_cancelled_without_selection => {
            load_config_or_exit(
                cli_kv_overrides.clone(),
                overrides.clone(),
                loader_overrides.clone(),
                cloud_config_bundle.clone(),
                strict_config,
            )
            .await
        }
        _ => config,
    };

    // Configure syntax highlighting theme from the final config — onboarding
    // and resume/fork can both reload config with a different tui_theme, so
    // this must happen after the last possible reload.
    if let Some(w) = crate::render::highlight::set_theme_override(
        config.tui_theme.clone(),
        find_codex_home().ok().map(AbsolutePathBuf::into_path_buf),
    ) {
        config.startup_warnings.push(w);
    }

    set_default_client_residency_requirement(config.enforce_residency.value());
    let should_show_trust_screen = should_show_trust_screen(&config);
    #[cfg(target_os = "windows")]
    let windows_sandbox_level = crate::windows_sandbox::level_from_config(&config);
    #[cfg(target_os = "windows")]
    let required_elevated_sandbox_needs_setup = windows_sandbox_level
        == WindowsSandboxLevel::Elevated
        && config
            .config_layer_stack
            .requirements()
            .windows_sandbox_mode
            .source
            .is_some()
        && !crate::windows_sandbox::sandbox_setup_is_complete(config.codex_home.as_path());
    #[cfg(target_os = "windows")]
    let should_prompt_windows_sandbox_nux_at_startup = (trust_decision_was_made
        && windows_sandbox_level == WindowsSandboxLevel::Disabled)
        || required_elevated_sandbox_needs_setup;
    #[cfg(not(target_os = "windows"))]
    let should_prompt_windows_sandbox_nux_at_startup = false;

    let Cli {
        prompt,
        shared,
        no_alt_screen,
        ..
    } = cli;
    let images = shared.into_inner().images;

    let use_alt_screen = determine_alt_screen_mode(no_alt_screen, config.tui_alternate_screen);
    tui.set_alt_screen_enabled(use_alt_screen);
    let mut app_server = match app_server {
        Some(app_server) => app_server,
        None => match start_app_server(
            &app_server_target,
            arg0_paths,
            config.clone(),
            cli_kv_overrides.clone(),
            loader_overrides.clone(),
            strict_config,
            cloud_config_bundle.clone(),
            feedback.clone(),
            log_db.clone(),
            state_db.clone(),
            environment_manager.clone(),
        )
        .await
        {
            Ok(app_server) => {
                AppServerSession::new(app_server, app_server_target.thread_params_mode())
                    .with_remote_cwd_override(remote_cwd_override.clone())
            }
            Err(err) => {
                terminal_restore_guard.restore_silently();
                session_log::log_session_end();
                return Err(err);
            }
        },
    };

    // Persistent app-server resumes may attach to an already-running thread,
    // where resume config overrides are ignored.
    let is_persistent_resume = !matches!(&app_server_target, AppServerTarget::Embedded)
        && matches!(
            &session_selection,
            resume_picker::SessionSelection::Resume(_)
        );
    let bypass_hook_trust_for_startup_review = config.bypass_hook_trust && !is_persistent_resume;
    let hooks_request_handle = app_server.request_handle();
    let hooks_cwd = config.cwd.to_path_buf();
    let startup_prefetch_started_at = Instant::now();
    let (startup_bootstrap, startup_hooks_entry) = tokio::join!(
        app_server.bootstrap(&config),
        load_startup_hooks_review_entry(hooks_request_handle, hooks_cwd),
    );
    let startup_bootstrap = Some(startup_bootstrap?);
    let startup_elapsed_before_app = startup_prefetch_started_at.elapsed();
    let startup_hooks_browser = match maybe_run_startup_hooks_review(
        &mut app_server,
        &mut tui,
        &config,
        bypass_hook_trust_for_startup_review,
        startup_hooks_entry,
    )
    .await?
    {
        StartupHooksReviewOutcome::Continue => None,
        StartupHooksReviewOutcome::OpenHooksBrowser(data) => Some(data),
    };

    let app_result = App::run(
        &mut tui,
        app_server,
        config,
        cli_kv_overrides.clone(),
        overrides.clone(),
        loader_overrides.clone(),
        cloud_config_bundle,
        prompt,
        images,
        session_selection,
        feedback,
        should_show_trust_screen, // Proxy to: is it a first run in this directory?
        should_prompt_windows_sandbox_nux_at_startup,
        app_server_target,
        state_db,
        environment_manager,
        startup_elapsed_before_app,
        startup_bootstrap,
        startup_hooks_browser,
    )
    .await;

    terminal_restore_guard.restore_silently();
    // Mark the end of the recorded session.
    session_log::log_session_end();
    // ignore error when collecting usage – report underlying error instead
    app_result
}

#[expect(
    clippy::print_stderr,
    reason = "TUI should no longer be displayed, so we can write to stderr."
)]
fn restore() {
    if let Err(err) = tui::restore_after_exit() {
        eprintln!(
            "failed to restore terminal. Run `reset` or restart your terminal to recover: {err}"
        );
    }
}

struct TerminalRestoreGuard {
    active: bool,
}

impl TerminalRestoreGuard {
    fn new() -> Self {
        Self { active: true }
    }

    #[cfg_attr(debug_assertions, allow(dead_code))]
    fn restore(&mut self) -> color_eyre::Result<()> {
        if self.active {
            crate::tui::restore_after_exit()?;
            self.active = false;
        }
        Ok(())
    }

    fn restore_silently(&mut self) {
        if self.active {
            restore();
            self.active = false;
        }
    }
}

impl Drop for TerminalRestoreGuard {
    fn drop(&mut self) {
        self.restore_silently();
    }
}

/// Determine whether to use the terminal's alternate screen buffer.
///
/// - If `--no-alt-screen` is explicitly passed, always disable alternate screen
/// - Otherwise, respect the `tui.alternate_screen` config setting:
///   - `always`: Use alternate screen
///   - `never`: Inline mode only, preserves scrollback
///   - `auto` (default): Use alternate screen
fn determine_alt_screen_mode(no_alt_screen: bool, tui_alternate_screen: AltScreenMode) -> bool {
    if no_alt_screen {
        return false;
    }

    tui_alternate_screen != AltScreenMode::Never
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoginStatus {
    AuthMode(AppServerAuthMode),
    NotAuthenticated,
}

/// Determines the user's authentication mode using a lightweight account read
/// rather than a full `bootstrap`, avoiding the model-list fetch and
/// rate-limit round-trip that `bootstrap` would trigger.
async fn get_login_status(
    app_server: &mut AppServerSession,
    config: &Config,
) -> color_eyre::Result<LoginStatus> {
    if !config.model_provider.requires_openai_auth {
        return Ok(LoginStatus::NotAuthenticated);
    }

    let account = app_server.read_account().await?;
    Ok(match account.account {
        Some(AppServerAccount::ApiKey {}) => LoginStatus::AuthMode(AppServerAuthMode::ApiKey),
        Some(AppServerAccount::Chatgpt { .. }) => LoginStatus::AuthMode(AppServerAuthMode::Chatgpt),
        Some(AppServerAccount::AmazonBedrock {}) => LoginStatus::NotAuthenticated,
        None => LoginStatus::NotAuthenticated,
    })
}

async fn load_config_or_exit(
    cli_kv_overrides: Vec<(String, toml::Value)>,
    overrides: ConfigOverrides,
    loader_overrides: LoaderOverrides,
    cloud_config_bundle: CloudConfigBundleLoader,
    strict_config: bool,
) -> Config {
    load_config_or_exit_with_fallback_cwd(
        cli_kv_overrides,
        overrides,
        loader_overrides,
        cloud_config_bundle,
        strict_config,
        /*fallback_cwd*/ None,
    )
    .await
}

async fn load_config_or_exit_with_fallback_cwd(
    cli_kv_overrides: Vec<(String, toml::Value)>,
    overrides: ConfigOverrides,
    loader_overrides: LoaderOverrides,
    cloud_config_bundle: CloudConfigBundleLoader,
    strict_config: bool,
    fallback_cwd: Option<PathBuf>,
) -> Config {
    #[allow(clippy::print_stderr)]
    match ConfigBuilder::default()
        .cli_overrides(cli_kv_overrides)
        .harness_overrides(overrides)
        .loader_overrides(loader_overrides)
        .strict_config(strict_config)
        .cloud_config_bundle(cloud_config_bundle)
        .fallback_cwd(fallback_cwd)
        .build()
        .await
    {
        Ok(config) => config,
        Err(err) => {
            eprintln!("Error loading configuration: {err}");
            std::process::exit(1);
        }
    }
}

#[allow(clippy::print_stderr)]
async fn load_bootstrap_config_or_exit(
    codex_home: &Path,
    cwd: Option<&AbsolutePathBuf>,
    cli_kv_overrides: Vec<(String, codex_config::TomlValue)>,
    loader_overrides: LoaderOverrides,
    strict_config: bool,
    cloud_config_bundle: CloudConfigBundleLoader,
) -> ConfigTomlLoadResult {
    match load_config_toml_with_layer_stack(
        codex_home,
        cwd,
        cli_kv_overrides,
        codex_config::ConfigLoadOptions {
            loader_overrides,
            strict_config,
            cloud_config_bundle,
        },
    )
    .await
    {
        Ok(config_toml) => config_toml,
        Err(err) => {
            let config_error = err
                .get_ref()
                .and_then(|err| err.downcast_ref::<ConfigLoadError>())
                .map(ConfigLoadError::config_error);
            if let Some(config_error) = config_error {
                eprintln!(
                    "Error loading config.toml:\n{}",
                    format_config_error_with_source(config_error)
                );
            } else {
                eprintln!("Error loading config.toml: {err}");
            }
            std::process::exit(1);
        }
    }
}

/// Determine if the user has decided whether to trust the current directory.
fn should_show_trust_screen(config: &Config) -> bool {
    config.active_project.trust_level.is_none()
}

fn should_show_onboarding(
    login_status: LoginStatus,
    config: &Config,
    show_trust_screen: bool,
) -> bool {
    if show_trust_screen {
        return true;
    }

    should_show_login_screen(login_status, config)
}

fn should_show_login_screen(login_status: LoginStatus, config: &Config) -> bool {
    // Only show the login screen for providers that actually require OpenAI auth
    // (OpenAI or equivalents). For OSS/other providers, skip login entirely.
    if !config.model_provider.requires_openai_auth {
        return false;
    }

    login_status == LoginStatus::NotAuthenticated
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::legacy_core::config::ConfigBuilder;
    use crate::legacy_core::config::ConfigOverrides;
    use codex_app_server_protocol::AskForApproval;
    use codex_app_server_protocol::ClientRequest;
    use codex_app_server_protocol::RequestId;
    use codex_app_server_protocol::ThreadStartParams;
    use codex_app_server_protocol::ThreadStartResponse;
    use codex_config::config_toml::ProjectConfig;
    use pretty_assertions::assert_eq;
    use serial_test::serial;
    use tempfile::TempDir;

    async fn build_config(temp_dir: &TempDir) -> std::io::Result<Config> {
        ConfigBuilder::default()
            .codex_home(temp_dir.path().to_path_buf())
            .build()
            .await
    }

    fn write_session_rollout(
        codex_home: &Path,
        filename_ts: &str,
        meta_rfc3339: &str,
        preview: &str,
        model_provider: &str,
        cwd: &Path,
    ) -> color_eyre::Result<ThreadId> {
        let uuid = Uuid::new_v4();
        let uuid_str = uuid.to_string();
        let thread_id = ThreadId::from_string(&uuid_str)?;
        let year = &filename_ts[0..4];
        let month = &filename_ts[5..7];
        let day = &filename_ts[8..10];
        let rollout_path = codex_home
            .join("sessions")
            .join(year)
            .join(month)
            .join(day)
            .join(format!("rollout-{filename_ts}-{uuid_str}.jsonl"));
        let parent = rollout_path
            .parent()
            .ok_or_else(|| color_eyre::eyre::eyre!("rollout path is missing a parent directory"))?;
        std::fs::create_dir_all(parent)?;

        let session_meta = codex_protocol::protocol::SessionMeta {
            id: thread_id,
            timestamp: meta_rfc3339.to_string(),
            cwd: cwd.to_path_buf(),
            originator: "codex".to_string(),
            cli_version: "0.0.0".to_string(),
            source: codex_protocol::protocol::SessionSource::Cli,
            model_provider: Some(model_provider.to_string()),
            ..Default::default()
        };
        let session_meta = serde_json::to_value(codex_protocol::protocol::SessionMetaLine {
            meta: session_meta,
            git: None,
        })?;
        let lines = [
            serde_json::json!({
                "timestamp": meta_rfc3339,
                "type": "session_meta",
                "payload": session_meta,
            })
            .to_string(),
            serde_json::json!({
                "timestamp": meta_rfc3339,
                "type": "response_item",
                "payload": {
                    "type": "message",
                    "role": "user",
                    "content": [{"type": "input_text", "text": preview}],
                },
            })
            .to_string(),
            serde_json::json!({
                "timestamp": meta_rfc3339,
                "type": "event_msg",
                "payload": {
                    "type": "user_message",
                    "message": preview,
                    "kind": "plain",
                },
            })
            .to_string(),
        ];
        std::fs::write(&rollout_path, lines.join("\n") + "\n")?;
        let updated_at =
            chrono::DateTime::parse_from_rfc3339(meta_rfc3339)?.with_timezone(&chrono::Utc);
        let times = std::fs::FileTimes::new().set_modified(updated_at.into());
        std::fs::OpenOptions::new()
            .append(true)
            .open(rollout_path)?
            .set_times(times)?;

        Ok(thread_id)
    }

    #[test]
    fn startup_removes_legacy_tui_log_file() -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let legacy_log_dir = temp_dir.path().join("log");
        std::fs::create_dir_all(&legacy_log_dir)?;
        let legacy_log = legacy_log_dir.join(TUI_LOG_FILE_NAME);
        std::fs::write(&legacy_log, "legacy log")?;

        remove_legacy_tui_log_file(temp_dir.path());

        assert!(!legacy_log.exists());
        Ok(())
    }

    async fn start_test_embedded_app_server(
        config: Config,
    ) -> color_eyre::Result<InProcessAppServerClient> {
        let state_db =
            init_state_db_for_app_server_target(&config, &AppServerTarget::Embedded).await?;
        start_embedded_app_server(
            Arg0DispatchPaths::default(),
            config,
            Vec::new(),
            LoaderOverrides::default(),
            /*strict_config*/ false,
            CloudConfigBundleLoader::default(),
            codex_feedback::CodexFeedback::new(),
            /*log_db*/ None,
            state_db,
            Arc::new(EnvironmentManager::default_for_tests()),
        )
        .await
    }

    #[test]
    fn alternate_screen_auto_uses_alt_screen() {
        assert!(determine_alt_screen_mode(
            /*no_alt_screen*/ false,
            AltScreenMode::Auto,
        ));
        assert!(determine_alt_screen_mode(
            /*no_alt_screen*/ false,
            AltScreenMode::Always,
        ));
        assert!(!determine_alt_screen_mode(
            /*no_alt_screen*/ false,
            AltScreenMode::Never,
        ));
        assert!(!determine_alt_screen_mode(
            /*no_alt_screen*/ true,
            AltScreenMode::Auto,
        ));
    }

    #[test]
    fn session_target_display_label_falls_back_to_thread_id() {
        let thread_id = ThreadId::new();
        let target = crate::resume_picker::SessionTarget {
            path: None,
            thread_id,
        };

        assert_eq!(target.display_label(), format!("thread {thread_id}"));
    }

    #[test]
    fn resolve_remote_addr_accepts_websocket_url() {
        assert_eq!(
            resolve_remote_addr("ws://127.0.0.1:4500").expect("ws URL should normalize"),
            RemoteAppServerEndpoint::WebSocket {
                websocket_url: "ws://127.0.0.1:4500/".to_string(),
                auth_token: None,
            }
        );
    }

    #[test]
    fn resolve_remote_addr_accepts_secure_websocket_url() {
        assert_eq!(
            resolve_remote_addr("wss://example.com:443").expect("wss URL should normalize"),
            RemoteAppServerEndpoint::WebSocket {
                websocket_url: "wss://example.com/".to_string(),
                auth_token: None,
            }
        );
    }

    #[test]
    fn resolve_remote_addr_accepts_default_socket() -> color_eyre::Result<()> {
        let codex_home = find_codex_home().wrap_err("failed to resolve CODEX_HOME")?;
        assert_eq!(
            resolve_remote_addr("unix://")?,
            RemoteAppServerEndpoint::UnixSocket {
                socket_path: codex_app_server_client::app_server_control_socket_path(&codex_home)?,
            }
        );
        Ok(())
    }

    #[test]
    fn resolve_remote_addr_accepts_relative_socket_path() -> color_eyre::Result<()> {
        assert_eq!(
            resolve_remote_addr("unix://codex.sock")?,
            RemoteAppServerEndpoint::UnixSocket {
                socket_path: AbsolutePathBuf::relative_to_current_dir("codex.sock")?,
            }
        );
        Ok(())
    }

    #[test]
    fn resolve_remote_addr_accepts_absolute_socket_path() -> color_eyre::Result<()> {
        let temp_dir = TempDir::new()?;
        let socket_path = temp_dir.path().join("codex.sock");
        assert_eq!(
            resolve_remote_addr(&format!("unix://{}", socket_path.display()))?,
            RemoteAppServerEndpoint::UnixSocket {
                socket_path: AbsolutePathBuf::from_absolute_path(&socket_path)?,
            }
        );
        Ok(())
    }

    #[test]
    fn resolve_remote_addr_rejects_invalid_remote_addresses() {
        for addr in [
            "ws://127.0.0.1",
            "wss://example.com",
            "127.0.0.1:4500",
            "https://127.0.0.1:4500",
        ] {
            let err = resolve_remote_addr(addr).expect_err("invalid remote addresses should fail");
            assert!(err.to_string().contains(
                "expected `ws://host:port`, `wss://host:port`, `unix://`, or `unix://PATH`"
            ));
        }
    }

    #[tokio::test]
    async fn default_daemon_auto_connect_skips_missing_socket() -> color_eyre::Result<()> {
        let codex_home = TempDir::new()?;
        assert!(
            maybe_probe_default_daemon_socket(codex_home.path())
                .await
                .is_none()
        );
        Ok(())
    }

    #[cfg(unix)]
    #[tokio::test]
    async fn default_daemon_auto_connect_probes_socket_only() -> color_eyre::Result<()> {
        let codex_home = TempDir::new()?;
        let socket_path =
            codex_app_server_client::app_server_control_socket_path(codex_home.path())?;
        std::fs::create_dir_all(socket_path.as_path().parent().expect("socket parent"))?;
        let _listener = tokio::net::UnixListener::bind(socket_path.as_path())?;

        assert_eq!(
            maybe_probe_default_daemon_socket(codex_home.path()).await,
            Some(socket_path)
        );
        Ok(())
    }

    #[test]
    fn app_server_target_for_launch_uses_local_daemon_for_default_socket() -> color_eyre::Result<()>
    {
        let socket_path = AbsolutePathBuf::relative_to_current_dir("codex.sock")?;
        let target = app_server_target_for_launch(
            /*explicit_remote_endpoint*/ None,
            Some(socket_path.clone()),
            /*can_reuse_implicit_local_daemon*/ true,
        );

        assert_eq!(
            target,
            AppServerTarget::LocalDaemon {
                endpoint: RemoteAppServerEndpoint::UnixSocket { socket_path },
            }
        );
        assert!(!target.uses_remote_workspace());
        assert_eq!(target.thread_params_mode(), ThreadParamsMode::Embedded);
        Ok(())
    }

    #[test]
    fn app_server_target_for_launch_prefers_explicit_remote_endpoint() -> color_eyre::Result<()> {
        let explicit_endpoint = RemoteAppServerEndpoint::UnixSocket {
            socket_path: AbsolutePathBuf::relative_to_current_dir("explicit.sock")?,
        };
        let target = app_server_target_for_launch(
            Some(explicit_endpoint.clone()),
            Some(AbsolutePathBuf::relative_to_current_dir("default.sock")?),
            /*can_reuse_implicit_local_daemon*/ false,
        );

        assert_eq!(
            target,
            AppServerTarget::Remote {
                endpoint: explicit_endpoint,
            }
        );
        assert!(target.uses_remote_workspace());
        assert_eq!(target.thread_params_mode(), ThreadParamsMode::Remote);
        Ok(())
    }

    #[test]
    fn app_server_target_for_launch_skips_local_daemon_when_launch_config_is_not_replayable()
    -> color_eyre::Result<()> {
        let socket_path = AbsolutePathBuf::relative_to_current_dir("codex.sock")?;
        let target = app_server_target_for_launch(
            /*explicit_remote_endpoint*/ None,
            Some(socket_path),
            /*can_reuse_implicit_local_daemon*/ false,
        );

        assert_eq!(target, AppServerTarget::Embedded);
        Ok(())
    }

    #[test]
    fn can_reuse_implicit_local_daemon_requires_default_launch_config() -> color_eyre::Result<()> {
        let mut loader_overrides = LoaderOverrides::default();
        let cli_kv_overrides = vec![("web_search".to_string(), toml::Value::String("live".into()))];

        assert!(can_reuse_implicit_local_daemon(
            &[],
            &LoaderOverrides::default(),
            /*strict_config*/ false,
            /*has_non_replayable_launch_overrides*/ false,
        ));
        assert!(!can_reuse_implicit_local_daemon(
            &cli_kv_overrides,
            &LoaderOverrides::default(),
            /*strict_config*/ false,
            /*has_non_replayable_launch_overrides*/ false,
        ));
        loader_overrides.ignore_user_config = true;
        assert!(!can_reuse_implicit_local_daemon(
            &[],
            &loader_overrides,
            /*strict_config*/ false,
            /*has_non_replayable_launch_overrides*/ false,
        ));
        assert!(!can_reuse_implicit_local_daemon(
            &[],
            &LoaderOverrides::default(),
            /*strict_config*/ true,
            /*has_non_replayable_launch_overrides*/ false,
        ));
        assert!(!can_reuse_implicit_local_daemon(
            &[],
            &LoaderOverrides::default(),
            /*strict_config*/ false,
            /*has_non_replayable_launch_overrides*/ true,
        ));
        Ok(())
    }

    #[test]
    fn should_load_configured_environments_for_local_daemon() -> color_eyre::Result<()> {
        let target = AppServerTarget::LocalDaemon {
            endpoint: RemoteAppServerEndpoint::UnixSocket {
                socket_path: AbsolutePathBuf::relative_to_current_dir("codex.sock")?,
            },
        };

        assert!(should_load_configured_environments(
            &LoaderOverrides::default(),
            &target,
        ));
        Ok(())
    }

    #[tokio::test]
    async fn latest_session_lookup_params_keep_local_filters_for_embedded_sessions()
    -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let config = build_config(&temp_dir).await?;
        let cwd = temp_dir.path().join("project");

        let params = latest_session_lookup_params(
            /*uses_remote_workspace*/ false,
            &config,
            Some(cwd.as_path()),
            /*include_non_interactive*/ false,
            LatestSessionLookupMode::StateDbOnly,
        );

        assert_eq!(
            params.model_providers,
            Some(vec![config.model_provider_id.clone()])
        );
        assert_eq!(
            params.cwd,
            Some(ThreadListCwdFilter::One(cwd.to_string_lossy().to_string()))
        );
        assert!(params.use_state_db_only);

        let scan_params = latest_session_lookup_params(
            /*uses_remote_workspace*/ false,
            &config,
            Some(cwd.as_path()),
            /*include_non_interactive*/ false,
            LatestSessionLookupMode::ScanAndRepair,
        );
        assert!(!scan_params.use_state_db_only);
        Ok(())
    }

    #[tokio::test]
    async fn latest_session_lookup_params_keep_local_filters_for_local_daemon_sessions()
    -> color_eyre::Result<()> {
        let temp_dir = TempDir::new()?;
        let config = build_config(&temp_dir).await?;
        let cwd = temp_dir.path().join("project");
        let target = AppServerTarget::LocalDaemon {
            endpoint: RemoteAppServerEndpoint::UnixSocket {
                socket_path: AbsolutePathBuf::relative_to_current_dir("codex.sock")?,
            },
        };

        let params = latest_session_lookup_params(
            target.uses_remote_workspace(),
            &config,
            Some(cwd.as_path()),
            /*include_non_interactive*/ false,
            LatestSessionLookupMode::StateDbOnly,
        );

        assert_eq!(params.model_providers, Some(vec![config.model_provider_id]));
        assert_eq!(
            params.cwd,
            Some(ThreadListCwdFilter::One(cwd.to_string_lossy().to_string()))
        );
        Ok(())
    }

    #[tokio::test]
    async fn latest_session_lookup_params_omit_local_filters_for_remote_sessions()
    -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let config = build_config(&temp_dir).await?;

        let params = latest_session_lookup_params(
            /*uses_remote_workspace*/ true,
            &config,
            /*cwd_filter*/ None,
            /*include_non_interactive*/ false,
            LatestSessionLookupMode::StateDbOnly,
        );

        assert_eq!(params.model_providers, None);
        assert_eq!(params.cwd, None);
        Ok(())
    }

    #[tokio::test]
    async fn latest_session_lookup_params_can_include_non_interactive_sources()
    -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let config = build_config(&temp_dir).await?;

        let params = latest_session_lookup_params(
            /*uses_remote_workspace*/ true,
            &config,
            /*cwd_filter*/ None,
            /*include_non_interactive*/ true,
            LatestSessionLookupMode::StateDbOnly,
        );

        assert_eq!(
            params.source_kinds,
            Some(vec![
                ThreadSourceKind::Cli,
                ThreadSourceKind::VsCode,
                ThreadSourceKind::Exec,
                ThreadSourceKind::AppServer,
            ])
        );
        Ok(())
    }

    #[tokio::test]
    async fn latest_session_lookup_params_keep_explicit_cwd_filter_for_remote_sessions()
    -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let config = build_config(&temp_dir).await?;
        let cwd = Path::new("repo/on/server");

        let params = latest_session_lookup_params(
            /*uses_remote_workspace*/ true,
            &config,
            Some(cwd),
            /*include_non_interactive*/ false,
            LatestSessionLookupMode::StateDbOnly,
        );

        assert_eq!(params.model_providers, None);
        assert_eq!(
            params.cwd,
            Some(ThreadListCwdFilter::One(String::from("repo/on/server")))
        );
        Ok(())
    }

    #[tokio::test]
    async fn latest_session_cwd_filter_respects_scope_options() -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let config = build_config(&temp_dir).await?;
        let remote_cwd = Path::new("repo/on/server");

        let local_filter = latest_session_cwd_filter(
            /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, &config,
            /*show_all*/ false,
        );
        let show_all_filter = latest_session_cwd_filter(
            /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, &config,
            /*show_all*/ true,
        );
        let remote_filter = latest_session_cwd_filter(
            /*uses_remote_workspace*/ true,
            Some(remote_cwd),
            &config,
            /*show_all*/ false,
        );

        assert_eq!(local_filter, Some(config.cwd.as_path()));
        assert_eq!(show_all_filter, None);
        assert_eq!(remote_filter, Some(remote_cwd));
        Ok(())
    }

    #[tokio::test]
    async fn fork_last_filters_latest_session_by_cwd_unless_show_all() -> color_eyre::Result<()> {
        let temp_dir = TempDir::new()?;
        let project_cwd = temp_dir.path().join("project");
        let other_cwd = temp_dir.path().join("other-project");
        std::fs::create_dir_all(&project_cwd)?;
        std::fs::create_dir_all(&other_cwd)?;

        let config = ConfigBuilder::default()
            .codex_home(temp_dir.path().to_path_buf())
            .harness_overrides(ConfigOverrides {
                cwd: Some(project_cwd.clone()),
                ..Default::default()
            })
            .build()
            .await?;
        let model_provider = config.model_provider_id.as_str();
        let project_thread_id = write_session_rollout(
            temp_dir.path(),
            "2025-01-02T10-00-00",
            "2025-01-02T10:00:00Z",
            "older project session",
            model_provider,
            &project_cwd,
        )?;
        let other_thread_id = write_session_rollout(
            temp_dir.path(),
            "2025-01-02T12-00-00",
            "2025-01-02T12:00:00Z",
            "newer other project session",
            model_provider,
            &other_cwd,
        )?;

        let mut app_server = AppServerSession::new(
            codex_app_server_client::AppServerClient::InProcess(
                start_test_embedded_app_server(config.clone()).await?,
            ),
            ThreadParamsMode::Embedded,
        );
        let filter_cwd = latest_session_cwd_filter(
            /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, &config,
            /*show_all*/ false,
        );
        let scoped_target = lookup_latest_session_target_with_app_server(
            &mut app_server,
            &config,
            filter_cwd,
            /*include_non_interactive*/ false,
        )
        .await?
        .expect("expected project-scoped fork --last target");
        let show_all_filter_cwd = latest_session_cwd_filter(
            /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, &config,
            /*show_all*/ true,
        );
        let show_all_target = lookup_latest_session_target_with_app_server(
            &mut app_server,
            &config,
            show_all_filter_cwd,
            /*include_non_interactive*/ false,
        )
        .await?
        .expect("expected global fork --last target");
        app_server.shutdown().await?;

        assert_eq!(scoped_target.thread_id, project_thread_id);
        assert_eq!(show_all_target.thread_id, other_thread_id);
        Ok(())
    }

    #[tokio::test]
    async fn latest_session_lookup_falls_back_for_rollout_missing_from_state_db()
    -> color_eyre::Result<()> {
        let temp_dir = TempDir::new()?;
        let project_cwd = temp_dir.path().join("project");
        std::fs::create_dir_all(&project_cwd)?;
        let config = ConfigBuilder::default()
            .codex_home(temp_dir.path().to_path_buf())
            .harness_overrides(ConfigOverrides {
                cwd: Some(project_cwd.clone()),
                ..Default::default()
            })
            .build()
            .await?;
        let mut app_server = AppServerSession::new(
            codex_app_server_client::AppServerClient::InProcess(
                start_test_embedded_app_server(config.clone()).await?,
            ),
            ThreadParamsMode::Embedded,
        );

        // Simulate a legacy writer creating a rollout after the state DB backfill completed.
        let thread_id = write_session_rollout(
            temp_dir.path(),
            "2025-01-02T10-00-00",
            "2025-01-02T10:00:00Z",
            "legacy writer session",
            config.model_provider_id.as_str(),
            &project_cwd,
        )?;

        let target = lookup_latest_session_target_with_app_server(
            &mut app_server,
            &config,
            Some(project_cwd.as_path()),
            /*include_non_interactive*/ false,
        )
        .await?
        .expect("expected scan-and-repair fallback to find the rollout");
        app_server.shutdown().await?;

        assert_eq!(target.thread_id, thread_id);
        Ok(())
    }

    #[tokio::test]
    async fn config_cwd_for_app_server_target_omits_cwd_for_remote_sessions() -> std::io::Result<()>
    {
        let remote_only_cwd = if cfg!(windows) {
            Path::new(r"C:\definitely\not\local\to\this\test")
        } else {
            Path::new("/definitely/not/local/to/this/test")
        };
        let target = AppServerTarget::Remote {
            endpoint: RemoteAppServerEndpoint::UnixSocket {
                socket_path: AbsolutePathBuf::relative_to_current_dir("codex.sock")?,
            },
        };
        let environment_manager = EnvironmentManager::default_for_tests();

        let config_cwd =
            config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?;

        assert_eq!(config_cwd, None);
        Ok(())
    }

    #[tokio::test]
    async fn config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd() -> std::io::Result<()>
    {
        let temp_dir = TempDir::new()?;
        let target = AppServerTarget::Embedded;
        let environment_manager = EnvironmentManager::default_for_tests();

        let config_cwd =
            config_cwd_for_app_server_target(Some(temp_dir.path()), &target, &environment_manager)?;

        assert_eq!(
            config_cwd,
            Some(AbsolutePathBuf::from_absolute_path(dunce::canonicalize(
                temp_dir.path()
            )?)?)
        );
        Ok(())
    }

    #[tokio::test]
    async fn config_cwd_for_app_server_target_canonicalizes_local_daemon_cli_cwd()
    -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let target = AppServerTarget::LocalDaemon {
            endpoint: RemoteAppServerEndpoint::UnixSocket {
                socket_path: AbsolutePathBuf::relative_to_current_dir("codex.sock")?,
            },
        };
        let environment_manager = EnvironmentManager::default_for_tests();

        let config_cwd =
            config_cwd_for_app_server_target(Some(temp_dir.path()), &target, &environment_manager)?;

        assert_eq!(
            config_cwd,
            Some(AbsolutePathBuf::from_absolute_path(dunce::canonicalize(
                temp_dir.path()
            )?)?)
        );
        Ok(())
    }

    #[tokio::test]
    async fn config_cwd_for_app_server_target_errors_for_missing_embedded_cli_cwd()
    -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let missing = temp_dir.path().join("missing");
        let target = AppServerTarget::Embedded;
        let environment_manager = EnvironmentManager::default_for_tests();

        let err = config_cwd_for_app_server_target(Some(&missing), &target, &environment_manager)
            .expect_err("missing embedded cwd should fail");

        assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
        Ok(())
    }

    #[tokio::test]
    async fn config_cwd_for_app_server_target_omits_cwd_for_remote_exec_server()
    -> std::io::Result<()> {
        let remote_only_cwd = if cfg!(windows) {
            Path::new(r"C:\definitely\not\local\to\this\test")
        } else {
            Path::new("/definitely/not/local/to/this/test")
        };
        let target = AppServerTarget::Embedded;
        let environment_manager = EnvironmentManager::create_for_tests(
            Some("ws://127.0.0.1:8765".to_string()),
            Some(ExecServerRuntimePaths::new(
                std::env::current_exe().expect("current exe"),
                /*codex_linux_sandbox_exe*/ None,
            )?),
        )
        .await;

        let config_cwd =
            config_cwd_for_app_server_target(Some(remote_only_cwd), &target, &environment_manager)?;

        assert_eq!(config_cwd, None);
        Ok(())
    }

    #[tokio::test]
    #[serial]
    async fn windows_shows_trust_prompt_without_sandbox() -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let mut config = build_config(&temp_dir).await?;
        config.active_project = ProjectConfig { trust_level: None };
        config.set_windows_sandbox_enabled(/*value*/ false);

        let should_show = should_show_trust_screen(&config);
        assert!(
            should_show,
            "Trust prompt should be shown when project trust is undecided"
        );
        Ok(())
    }

    #[tokio::test]
    async fn embedded_app_server_supports_thread_start_rpc() -> color_eyre::Result<()> {
        let temp_dir = TempDir::new()?;
        let config = build_config(&temp_dir).await?;
        let app_server = start_test_embedded_app_server(config).await?;
        let response: ThreadStartResponse = app_server
            .request_typed(ClientRequest::ThreadStart {
                request_id: RequestId::Integer(1),
                params: ThreadStartParams {
                    ephemeral: Some(true),
                    ..ThreadStartParams::default()
                },
            })
            .await
            .expect("thread/start should succeed");
        assert!(!response.thread.id.is_empty());

        app_server.shutdown().await?;
        Ok(())
    }

    #[tokio::test]
    async fn lookup_session_target_by_name_uses_backend_title_search() -> color_eyre::Result<()> {
        Box::pin(async {
            let temp_dir = TempDir::new()?;
            let config = build_config(&temp_dir).await?;
            let thread_id = ThreadId::new();
            let rollout_path = temp_dir
                .path()
                .join("sessions/2025/02/01")
                .join(format!("rollout-2025-02-01T10-00-00-{thread_id}.jsonl"));
            let rollout_dir = rollout_path.parent().expect("rollout parent");
            std::fs::create_dir_all(rollout_dir)?;
            std::fs::write(&rollout_path, "")?;

            let state_runtime = codex_state::StateRuntime::init(
                config.codex_home.to_path_buf(),
                config.model_provider_id.clone(),
            )
            .await
            .map_err(std::io::Error::other)?;
            state_runtime
                .mark_backfill_complete(/*last_watermark*/ None)
                .await
                .map_err(std::io::Error::other)?;

            let session_cwd = temp_dir.path().join("project");
            std::fs::create_dir_all(&session_cwd)?;
            let created_at = chrono::DateTime::parse_from_rfc3339("2025-02-01T10:00:00Z")
                .expect("timestamp should parse")
                .with_timezone(&chrono::Utc);
            let mut builder = codex_state::ThreadMetadataBuilder::new(
                thread_id,
                rollout_path.clone(),
                created_at,
                serde_json::from_value(serde_json::json!("cli"))
                    .expect("cli session source should deserialize"),
            );
            builder.cwd = session_cwd;
            let mut metadata = builder.build(config.model_provider_id.as_str());
            metadata.title = "saved-session".to_string();
            metadata.first_user_message = Some("preview text".to_string());
            state_runtime
                .upsert_thread(&metadata)
                .await
                .map_err(std::io::Error::other)?;

            let mut app_server = AppServerSession::new(
                codex_app_server_client::AppServerClient::InProcess(
                    start_test_embedded_app_server(config).await?,
                ),
                ThreadParamsMode::Embedded,
            );
            let target =
                lookup_session_target_by_name_with_app_server(&mut app_server, "saved-session")
                    .await?;
            let target = target.expect("name lookup should find the saved thread");
            assert_eq!(target.path, Some(rollout_path));
            assert_eq!(target.thread_id, thread_id);

            app_server.shutdown().await?;
            Ok(())
        })
        .await
    }

    #[tokio::test]
    async fn embedded_app_server_start_failure_is_returned() -> color_eyre::Result<()> {
        let temp_dir = TempDir::new()?;
        let config = build_config(&temp_dir).await?;
        let result = start_embedded_app_server_with(
            Arg0DispatchPaths::default(),
            config,
            Vec::new(),
            LoaderOverrides::default(),
            /*strict_config*/ false,
            CloudConfigBundleLoader::default(),
            codex_feedback::CodexFeedback::new(),
            /*log_db*/ None,
            /*state_db*/ None,
            Arc::new(EnvironmentManager::default_for_tests()),
            |_args| async { Err(std::io::Error::other("boom")) },
        )
        .await;
        let err = match result {
            Ok(_) => panic!("startup failure should be returned"),
            Err(err) => err,
        };

        assert!(
            err.to_string()
                .contains("failed to start embedded app server"),
            "error should preserve the embedded app server startup context"
        );
        Ok(())
    }

    #[tokio::test]
    async fn embedded_state_db_failure_is_typed_for_cli_recovery() -> color_eyre::Result<()> {
        let temp_dir = TempDir::new()?;
        let mut config = build_config(&temp_dir).await?;
        let occupied_sqlite_home = temp_dir.path().join("sqlite-home");
        std::fs::write(&occupied_sqlite_home, "occupied")?;
        config.sqlite_home = occupied_sqlite_home.clone();

        let err =
            match init_state_db_for_app_server_target(&config, &AppServerTarget::Embedded).await {
                Ok(_) => panic!("embedded startup should surface state db init failures"),
                Err(err) => err,
            };
        let startup_error = err
            .get_ref()
            .and_then(|err| err.downcast_ref::<LocalStateDbStartupError>())
            .expect("state db startup failure should retain its typed context");

        assert_eq!(
            startup_error.state_db_path(),
            codex_state::state_db_path(occupied_sqlite_home.as_path()).as_path()
        );
        assert!(
            startup_error
                .detail()
                .contains("failed to initialize state runtime"),
            "startup error should preserve the underlying state db failure"
        );
        Ok(())
    }

    #[tokio::test]
    async fn embedded_state_db_corruption_preserves_failed_database_for_cli_recovery()
    -> color_eyre::Result<()> {
        let temp_dir = TempDir::new()?;
        let mut config = build_config(&temp_dir).await?;
        let sqlite_home = temp_dir.path().join("sqlite-home");
        std::fs::create_dir_all(&sqlite_home)?;
        let logs_db_path = codex_state::logs_db_path(&sqlite_home);
        std::fs::write(&logs_db_path, "not a sqlite database")?;
        config.sqlite_home = sqlite_home;

        let err =
            match init_state_db_for_app_server_target(&config, &AppServerTarget::Embedded).await {
                Ok(_) => panic!("embedded startup should surface state db init failures"),
                Err(err) => err,
            };
        let startup_error = err
            .get_ref()
            .and_then(|err| err.downcast_ref::<LocalStateDbStartupError>())
            .expect("state db startup failure should retain its typed context");

        assert_eq!(startup_error.database_path(), logs_db_path.as_path());
        assert!(
            codex_state::sqlite_error_detail_is_corruption(startup_error.detail()),
            "startup error should preserve the SQLite corruption cause, got: {}",
            startup_error.detail()
        );
        Ok(())
    }

    #[tokio::test]
    #[serial]
    async fn windows_shows_trust_prompt_with_sandbox() -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let mut config = build_config(&temp_dir).await?;
        config.active_project = ProjectConfig { trust_level: None };
        config.set_windows_sandbox_enabled(/*value*/ true);

        let should_show = should_show_trust_screen(&config);
        if cfg!(target_os = "windows") {
            assert!(
                should_show,
                "Windows trust prompt should be shown on native Windows with sandbox enabled"
            );
        } else {
            assert!(
                should_show,
                "Non-Windows should still show trust prompt when project is untrusted"
            );
        }
        Ok(())
    }
    #[tokio::test]
    async fn untrusted_project_skips_trust_prompt() -> std::io::Result<()> {
        use codex_protocol::config_types::TrustLevel;
        let temp_dir = TempDir::new()?;
        let mut config = build_config(&temp_dir).await?;
        config.active_project = ProjectConfig {
            trust_level: Some(TrustLevel::Untrusted),
        };

        let should_show = should_show_trust_screen(&config);
        assert!(
            !should_show,
            "Trust prompt should not be shown for projects explicitly marked as untrusted"
        );
        Ok(())
    }

    #[tokio::test]
    async fn config_rebuild_changes_trust_defaults_with_cwd() -> std::io::Result<()> {
        let temp_dir = TempDir::new()?;
        let codex_home = temp_dir.path().to_path_buf();
        let trusted = temp_dir.path().join("trusted");
        let untrusted = temp_dir.path().join("untrusted");
        std::fs::create_dir_all(&trusted)?;
        std::fs::create_dir_all(&untrusted)?;

        // TOML keys need escaped backslashes on Windows paths.
        let trusted_display = trusted.display().to_string().replace('\\', "\\\\");
        let untrusted_display = untrusted.display().to_string().replace('\\', "\\\\");
        let config_toml = format!(
            r#"[projects."{trusted_display}"]
trust_level = "trusted"

[projects."{untrusted_display}"]
trust_level = "untrusted"
"#
        );
        std::fs::write(temp_dir.path().join("config.toml"), config_toml)?;

        let trusted_overrides = ConfigOverrides {
            cwd: Some(trusted.clone()),
            ..Default::default()
        };
        let trusted_config = ConfigBuilder::default()
            .loader_overrides(LoaderOverrides::without_managed_config_for_tests())
            .codex_home(codex_home.clone())
            .harness_overrides(trusted_overrides.clone())
            .build()
            .await?;
        assert_eq!(
            AskForApproval::from(trusted_config.permissions.approval_policy.value()),
            AskForApproval::OnRequest
        );

        let untrusted_overrides = ConfigOverrides {
            cwd: Some(untrusted),
            ..trusted_overrides
        };
        let untrusted_config = ConfigBuilder::default()
            .loader_overrides(LoaderOverrides::without_managed_config_for_tests())
            .codex_home(codex_home)
            .harness_overrides(untrusted_overrides)
            .build()
            .await?;
        assert_eq!(
            AskForApproval::from(untrusted_config.permissions.approval_policy.value()),
            AskForApproval::UnlessTrusted
        );
        Ok(())
    }

    /// Regression: theme must be configured from the *final* config.
    ///
    /// `run_ratatui_app` can reload config during onboarding and again
    /// during session resume/fork.  The syntax theme override (stored in
    /// a `OnceLock`) must use the final config's `tui_theme`, not the
    /// initial one — otherwise users resuming a thread in a project with
    /// a different theme get the wrong highlighting.
    ///
    /// We verify the invariant indirectly: `validate_theme_name` (the
    /// pure validation core of `set_theme_override`) must be called with
    /// the *final* config's theme, and its warning must land in the
    /// final config's `startup_warnings`.
    #[tokio::test]
    async fn theme_warning_uses_final_config() -> std::io::Result<()> {
        use crate::render::highlight::validate_theme_name;

        let temp_dir = TempDir::new()?;

        // initial_config has a valid theme — no warning.
        let initial_config = build_config(&temp_dir).await?;
        assert!(initial_config.tui_theme.is_none());

        // Simulate resume/fork reload: the final config has an invalid theme.
        let mut config = build_config(&temp_dir).await?;
        config.tui_theme = Some("bogus-theme".into());

        // Theme override must use the final config (not initial_config).
        // This mirrors the real call site in run_ratatui_app.
        if let Some(w) = validate_theme_name(config.tui_theme.as_deref(), Some(temp_dir.path())) {
            config.startup_warnings.push(w);
        }

        assert_eq!(
            config.startup_warnings.len(),
            1,
            "warning from final config's invalid theme should be present"
        );
        assert!(
            config.startup_warnings[0].contains("bogus-theme"),
            "warning should reference the final config's theme name"
        );
        Ok(())
    }
}
