Codex Handbook
tui/src/app/event_dispatch.rs 2324 lines
//! AppEvent dispatch for the TUI app.//!//! This module contains the exhaustive `AppEvent` dispatcher and exit-mode handling. Large domain//! actions are delegated to focused app submodules so the central match remains the routing layer.use super::resize_reflow::trailing_run_start;use super::*;use crate::config_update::format_config_error;use crate::external_agent_config_migration_flow::ExternalAgentConfigMigrationFlowOutcome;#[cfg(target_os = "windows")]use codex_config::types::WindowsSandboxModeToml;const SHUTDOWN_FIRST_EXIT_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 2);impl App {    pub(super) async fn handle_event(        &mut self,        tui: &mut tui::Tui,        app_server: &mut AppServerSession,        event: AppEvent,    ) -> Result<AppRunControl> {        match event {            AppEvent::NewSession => {                self.start_fresh_session_with_summary_hint(                    tui, app_server, /*session_start_source*/ None,                    /*initial_user_message*/ None,                )                .await;            }            AppEvent::StartupThreadStarted { result } => {                self.handle_startup_thread_started(app_server, result)                    .await?;            }            AppEvent::ClearUi => {                self.clear_terminal_ui(tui, /*redraw_header*/ false)?;                self.reset_app_ui_state_after_clear();                self.start_fresh_session_with_summary_hint(                    tui,                    app_server,                    Some(ThreadStartSource::Clear),                    /*initial_user_message*/ None,                )                .await;            }            AppEvent::RawOutputModeChanged { enabled } => {                self.apply_raw_output_mode(tui, enabled, /*notify*/ false);            }            AppEvent::ClearUiAndSubmitUserMessage { text } => {                self.clear_terminal_ui(tui, /*redraw_header*/ false)?;                self.reset_app_ui_state_after_clear();                self.start_fresh_session_with_summary_hint(                    tui,                    app_server,                    Some(ThreadStartSource::Clear),                    crate::chatwidget::create_initial_user_message(                        Some(text),                        Vec::new(),                        Vec::new(),                    ),                )                .await;            }            AppEvent::OpenResumePicker => {                let picker_app_server = match crate::start_app_server_for_picker(                    &self.config,                    &self.app_server_target,                    self.state_db.clone(),                    self.environment_manager.clone(),                )                .await                {                    Ok(app_server) => app_server,                    Err(err) => {                        self.chat_widget.add_error_message(format!(                            "Failed to start TUI session picker: {err}"                        ));                        return Ok(AppRunControl::Continue);                    }                };                match crate::resume_picker::run_resume_picker_from_existing_session_with_app_server(                    tui,                    &self.config,                    /*show_all*/ false,                    /*include_non_interactive*/ false,                    picker_app_server,                )                .await?                {                    SessionSelection::Resume(target_session) => {                        match self                            .resume_target_session(tui, app_server, target_session)                            .await?                        {                            AppRunControl::Continue => {}                            AppRunControl::Exit(reason) => {                                return Ok(AppRunControl::Exit(reason));                            }                        }                    }                    SessionSelection::Exit | SessionSelection::StartFresh => {                        self.refresh_in_memory_config_from_disk_best_effort(                            "closing the session picker",                        )                        .await;                    }                    SessionSelection::Fork(_) => {}                }                // Leaving alt-screen may blank the inline viewport; force a redraw either way.                tui.frame_requester().schedule_frame();            }            AppEvent::OpenExternalAgentConfigMigration => {                match crate::external_agent_config_migration_flow::handle_external_agent_config_migration_prompt(                    tui,                    app_server,                    &self.config,                )                .await                {                    Ok(ExternalAgentConfigMigrationFlowOutcome::Started(message)) => {                        self.chat_widget.add_info_message(message, /*hint*/ None);                    }                    Ok(ExternalAgentConfigMigrationFlowOutcome::NoItems) => {                        self.chat_widget.add_info_message(                            crate::external_agent_config_migration_flow::EXTERNAL_AGENT_CONFIG_MIGRATION_NO_ITEMS_MESSAGE                                .to_string(),                            /*hint*/ None,                        );                    }                    Ok(ExternalAgentConfigMigrationFlowOutcome::Cancelled) => {}                    Err(error_message) => {                        self.chat_widget.add_error_message(error_message);                    }                }                tui.frame_requester().schedule_frame();            }            AppEvent::ResumeSessionByIdOrName(id_or_name) => {                match crate::lookup_session_target_with_app_server(app_server, &id_or_name).await? {                    Some(target_session) => {                        return self                            .resume_target_session(tui, app_server, target_session)                            .await;                    }                    None => {                        self.chat_widget.add_error_message(format!(                            "No saved chat found matching '{id_or_name}'."                        ));                    }                }            }            AppEvent::ArchiveCurrentThread => {                return Ok(self.archive_current_thread(app_server).await);            }            AppEvent::DeleteCurrentThread => {                return Ok(self.delete_current_thread(app_server).await);            }            AppEvent::ForkCurrentSession => {                self.session_telemetry.counter(                    "codex.thread.fork",                    /*inc*/ 1,                    &[("source", "slash_command")],                );                let summary = session_summary(                    self.chat_widget.token_usage(),                    self.chat_widget.thread_id(),                    self.chat_widget.thread_name(),                    self.chat_widget.rollout_path().as_deref(),                );                self.chat_widget                    .add_plain_history_lines(vec!["/fork".magenta().into()]);                if let Some(thread_id) = self.chat_widget.thread_id() {                    self.refresh_in_memory_config_from_disk_best_effort("forking the thread")                        .await;                    match app_server.fork_thread(self.config.clone(), thread_id).await {                        Ok(forked) => {                            self.shutdown_current_thread(app_server).await;                            match self                                .replace_chat_widget_with_app_server_thread(                                    tui, app_server, forked, /*initial_user_message*/ None,                                )                                .await                            {                                Ok(()) => {                                    if let Some(summary) = summary {                                        let mut lines: Vec<Line<'static>> = Vec::new();                                        if let Some(usage_line) = summary.usage_line {                                            lines.push(usage_line.into());                                        }                                        if let Some(command) = summary.resume_hint {                                            let spans = vec![                                                "To continue this session, run ".into(),                                                command.cyan(),                                            ];                                            lines.push(spans.into());                                        }                                        self.chat_widget.add_plain_history_lines(lines);                                    }                                }                                Err(err) => {                                    self.chat_widget.add_error_message(format!(                                        "Failed to attach to forked app-server thread: {err}"                                    ));                                }                            }                        }                        Err(err) => {                            self.chat_widget.add_error_message(format!(                                "Failed to fork current session through the app server: {err}"                            ));                        }                    }                } else {                    self.chat_widget.add_error_message(                        "A thread must contain at least one turn before it can be forked."                            .to_string(),                    );                }                tui.frame_requester().schedule_frame();            }            AppEvent::BeginInitialHistoryReplayBuffer => {                self.begin_initial_history_replay_buffer();            }            AppEvent::BeginThreadSwitchHistoryReplayBuffer => {                self.begin_thread_switch_history_replay_buffer();            }            AppEvent::InsertHistoryCell(cell) => {                self.insert_history_cell(tui, cell);            }            AppEvent::EndInitialHistoryReplayBuffer => {                self.finish_initial_history_replay_buffer(tui);            }            AppEvent::ConsolidateAgentMessage {                source,                cwd,                scrollback_reflow,                deferred_history_cell,            } => {                self.handle_consolidate_agent_message(                    tui,                    source,                    cwd,                    scrollback_reflow,                    deferred_history_cell,                )?;                self.chat_widget.note_stream_consolidation_completed();                self.insert_pending_usage_output_after_stream_shutdown(tui);            }            AppEvent::ConsolidateProposedPlan(source) => {                let end = self.transcript_cells.len();                let start = trailing_run_start::<history_cell::ProposedPlanStreamCell>(                    &self.transcript_cells,                );                let consolidated: Arc<dyn HistoryCell> =                    Arc::new(history_cell::new_proposed_plan(source, &self.config.cwd));                if start < end {                    self.transcript_cells                        .splice(start..end, std::iter::once(consolidated.clone()));                    if let Some(Overlay::Transcript(t)) = &mut self.overlay {                        t.consolidate_cells(start..end, consolidated.clone());                        tui.frame_requester().schedule_frame();                    }                    self.finish_required_stream_reflow(tui)?;                } else {                    self.transcript_cells.push(consolidated.clone());                    if let Some(Overlay::Transcript(t)) = &mut self.overlay {                        t.insert_cell(consolidated.clone());                        tui.frame_requester().schedule_frame();                    }                    self.insert_history_cell_lines(                        tui,                        consolidated.as_ref(),                        self.chat_widget                            .history_wrap_width(tui.terminal.last_known_screen_size.width),                    );                    self.maybe_finish_stream_reflow(tui)?;                }                self.chat_widget.note_stream_consolidation_completed();                self.insert_pending_usage_output_after_stream_shutdown(tui);            }            AppEvent::ApplyThreadRollback { num_turns } => {                if self.apply_non_pending_thread_rollback(num_turns) {                    tui.frame_requester().schedule_frame();                }            }            AppEvent::StartCommitAnimation => {                if self                    .commit_anim_running                    .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)                    .is_ok()                {                    let tx = self.app_event_tx.clone();                    let running = self.commit_anim_running.clone();                    thread::spawn(move || {                        while running.load(Ordering::Relaxed) {                            thread::sleep(COMMIT_ANIMATION_TICK);                            tx.send(AppEvent::CommitTick);                        }                    });                }            }            AppEvent::StopCommitAnimation => {                self.commit_anim_running.store(false, Ordering::Release);            }            AppEvent::CommitTick => {                self.chat_widget.on_commit_tick();            }            AppEvent::Exit(mode) => {                if mode == ExitMode::ShutdownFirst {                    self.show_shutdown_feedback(tui)?;                }                return Ok(self.handle_exit_mode(app_server, mode).await);            }            AppEvent::Logout => match app_server.logout_account().await {                Ok(()) => {                    self.show_shutdown_feedback(tui)?;                    return Ok(self                        .handle_exit_mode(app_server, ExitMode::ShutdownFirst)                        .await);                }                Err(err) => {                    tracing::error!("failed to logout: {err}");                    self.chat_widget                        .add_error_message(format!("Logout failed: {err}"));                }            },            AppEvent::FatalExitRequest(message) => {                return Ok(AppRunControl::Exit(ExitReason::Fatal(message)));            }            AppEvent::CodexOp(op) => {                self.chat_widget.prepare_local_op_submission(&op);                self.submit_active_thread_op(app_server, op).await?;            }            AppEvent::RestoreCancelledTurn(prompt) => {                self.apply_cancelled_turn_edit(prompt);            }            AppEvent::AppendMessageHistoryEntry { thread_id, text } => {                self.append_message_history_entry(thread_id, text);            }            AppEvent::SyncThreadGitBranch { thread_id, branch } => {                if let Err(err) = app_server                    .thread_metadata_update_branch(thread_id, branch)                    .await                {                    tracing::warn!("failed to sync thread git branch from directive: {err}");                }            }            AppEvent::LookupMessageHistoryEntry {                thread_id,                offset,                log_id,            } => {                self.lookup_message_history_entry(thread_id, offset, log_id)                    .await?;            }            AppEvent::ApproveRecentAutoReviewDenial { thread_id, id } => {                self.chat_widget                    .approve_recent_auto_review_denial(thread_id, id);            }            AppEvent::SubmitThreadOp { thread_id, op } => {                self.submit_thread_op(app_server, thread_id, op).await?;            }            AppEvent::ThreadHistoryEntryResponse { thread_id, event } => {                self.enqueue_thread_history_entry_response(thread_id, event)                    .await?;            }            AppEvent::DiffResult(text) => {                // Clear the in-progress state in the bottom pane                self.chat_widget.on_diff_complete();                // Enter alternate screen using TUI helper and build pager lines                let _ = tui.enter_alt_screen();                let pager_lines: Vec<ratatui::text::Line<'static>> = if text.trim().is_empty() {                    vec!["No changes detected.".italic().into()]                } else {                    text.lines().map(ansi_escape_line).collect()                };                self.overlay = Some(Overlay::new_static_with_lines(                    pager_lines,                    "D I F F".to_string(),                    self.keymap.pager.clone(),                ));                tui.frame_requester().schedule_frame();            }            AppEvent::OpenAppLink {                app_id,                title,                description,                instructions,                url,                is_installed,                is_enabled,            } => {                self.chat_widget                    .open_app_link_view(crate::bottom_pane::AppLinkViewParams {                        app_id,                        title,                        description,                        instructions,                        url,                        is_installed,                        is_enabled,                        suggest_reason: None,                        suggestion_type: None,                        elicitation_target: None,                    });            }            AppEvent::OpenUrlInBrowser { url } => {                self.open_url_in_browser(url);            }            AppEvent::OpenDesktopThread { thread_id } => {                self.open_desktop_thread(thread_id);            }            AppEvent::PetSelected { pet_id } => {                self.handle_pet_selected(tui, pet_id);            }            AppEvent::PetDisabled => {                self.handle_pet_disabled(tui).await;            }            AppEvent::PetPreviewRequested { pet_id } => {                self.chat_widget.start_pet_picker_preview(pet_id);            }            AppEvent::PetPreviewLoaded { request_id, result } => {                self.handle_pet_preview_loaded(tui, request_id, result);            }            AppEvent::PetSelectionLoaded {                request_id,                pet_id,                result,            } => {                return self                    .handle_pet_selection_loaded(tui, request_id, pet_id, result)                    .await;            }            AppEvent::ConfiguredPetLoaded { pet_id, result } => {                self.handle_configured_pet_loaded(tui, pet_id, result);            }            AppEvent::RefreshConnectors { force_refetch } => {                self.chat_widget.refresh_connectors(force_refetch);            }            AppEvent::FetchConnectorsList { force_refetch } => {                self.fetch_connectors_list(app_server, force_refetch);            }            AppEvent::PluginInstallAuthAdvance { refresh_connectors } => {                if refresh_connectors {                    self.chat_widget.refresh_connectors(/*force_refetch*/ true);                }                self.chat_widget.advance_plugin_install_auth_flow();            }            AppEvent::PluginInstallAuthAbandon => {                self.chat_widget.abandon_plugin_install_auth_flow();            }            AppEvent::FetchPluginsList { cwd } => {                self.fetch_plugins_list(app_server, cwd);            }            AppEvent::FetchHooksList { cwd } => {                self.fetch_hooks_list(app_server, cwd);            }            AppEvent::OpenMarketplaceAddPrompt => {                self.chat_widget.open_marketplace_add_prompt();            }            AppEvent::OpenMarketplaceAddLoading { source } => {                self.chat_widget.open_marketplace_add_loading_popup(&source);            }            AppEvent::OpenMarketplaceRemoveConfirm {                marketplace_name,                marketplace_display_name,            } => {                self.chat_widget.open_marketplace_remove_confirmation(                    marketplace_name,                    marketplace_display_name,                );            }            AppEvent::OpenMarketplaceRemoveLoading {                marketplace_display_name,            } => {                self.chat_widget                    .open_marketplace_remove_loading_popup(&marketplace_display_name);            }            AppEvent::OpenMarketplaceUpgradeLoading { marketplace_name } => {                self.chat_widget                    .open_marketplace_upgrade_loading_popup(marketplace_name.as_deref());            }            AppEvent::OpenPluginDetailLoading {                plugin_display_name,            } => {                self.chat_widget                    .open_plugin_detail_loading_popup(&plugin_display_name);            }            AppEvent::OpenPluginInstallLoading {                plugin_display_name,            } => {                self.chat_widget                    .open_plugin_install_loading_popup(&plugin_display_name);            }            AppEvent::OpenPluginUninstallLoading {                plugin_display_name,            } => {                self.chat_widget                    .open_plugin_uninstall_loading_popup(&plugin_display_name);            }            AppEvent::PluginsLoaded { cwd, result } => {                self.chat_widget.on_plugins_loaded(cwd, result);            }            AppEvent::PluginRemoteSectionsLoaded {                cwd,                marketplaces,                section_errors,            } => {                self.chat_widget.on_plugin_remote_sections_loaded(                    cwd,                    marketplaces,                    section_errors,                );            }            AppEvent::HooksLoaded { cwd, result } => {                self.chat_widget.on_hooks_loaded(cwd, result);            }            AppEvent::FetchMarketplaceAdd { cwd, source } => {                self.fetch_marketplace_add(app_server, cwd, source);            }            AppEvent::FetchMarketplaceUpgrade {                cwd,                marketplace_name,            } => {                self.fetch_marketplace_upgrade(app_server, cwd, marketplace_name);            }            AppEvent::MarketplaceAddLoaded {                cwd,                source,                result,            } => {                let add_succeeded = result.is_ok();                self.chat_widget                    .on_marketplace_add_loaded(cwd.clone(), source, result);                if add_succeeded && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path() {                    self.fetch_plugins_list(app_server, cwd);                }            }            AppEvent::MarketplaceUpgradeLoaded { cwd, result } => {                let marketplace_contents_changed =                    matches!(&result, Ok(response) if !response.upgraded_roots.is_empty());                if marketplace_contents_changed {                    self.refresh_plugin_mentions_after_config_write();                }                self.chat_widget                    .on_marketplace_upgrade_loaded(cwd.clone(), result);                if self.chat_widget.config_ref().cwd.as_path() == cwd.as_path() {                    self.fetch_plugins_list(app_server, cwd);                }            }            AppEvent::FetchMarketplaceRemove {                cwd,                marketplace_name,                marketplace_display_name,            } => {                self.fetch_marketplace_remove(                    app_server,                    cwd,                    marketplace_name,                    marketplace_display_name,                );            }            AppEvent::MarketplaceRemoveLoaded {                cwd,                marketplace_name,                marketplace_display_name,                result,            } => {                let remove_succeeded = result.is_ok();                self.chat_widget.on_marketplace_remove_loaded(                    cwd.clone(),                    marketplace_name,                    marketplace_display_name,                    result,                );                if remove_succeeded && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path()                {                    self.refresh_plugin_mentions_after_config_write();                    self.fetch_plugins_list(app_server, cwd);                }            }            AppEvent::FetchPluginDetail { cwd, params } => {                self.fetch_plugin_detail(app_server, cwd, params);            }            AppEvent::PluginDetailLoaded { cwd, result } => {                self.chat_widget.on_plugin_detail_loaded(cwd, result);            }            AppEvent::FetchPluginInstall {                cwd,                location,                plugin_name,                plugin_display_name,            } => {                self.fetch_plugin_install(                    app_server,                    cwd,                    location,                    plugin_name,                    plugin_display_name,                );            }            AppEvent::FetchPluginUninstall {                cwd,                plugin_id,                plugin_display_name,            } => {                self.fetch_plugin_uninstall(app_server, cwd, plugin_id, plugin_display_name);            }            AppEvent::SetPluginEnabled {                cwd,                plugin_id,                enabled,            } => {                self.set_plugin_enabled(app_server, cwd, plugin_id, enabled);            }            AppEvent::PluginInstallLoaded {                cwd,                location,                plugin_name,                plugin_display_name,                result,            } => {                let install_succeeded = result.is_ok();                if install_succeeded {                    self.refresh_plugin_mentions_after_config_write();                }                let should_refresh_plugin_detail = self.chat_widget.on_plugin_install_loaded(                    cwd.clone(),                    location.clone(),                    plugin_name.clone(),                    plugin_display_name,                    result,                );                if install_succeeded && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path()                {                    self.fetch_plugins_list(app_server, cwd.clone());                    if should_refresh_plugin_detail {                        let (marketplace_path, remote_marketplace_name) =                            location.into_request_params();                        self.fetch_plugin_detail(                            app_server,                            cwd,                            PluginReadParams {                                marketplace_path,                                remote_marketplace_name,                                plugin_name,                            },                        );                    }                }            }            AppEvent::PluginEnabledSet {                cwd,                plugin_id,                enabled,                result,            } => {                let queued_enabled = self                    .pending_plugin_enabled_writes                    .get_mut(&plugin_id)                    .and_then(Option::take);                let should_apply_result = if let Some(queued_enabled) = queued_enabled                    && (result.is_err() || queued_enabled != enabled)                {                    self.spawn_plugin_enabled_write(                        app_server,                        cwd.clone(),                        plugin_id.clone(),                        queued_enabled,                    );                    false                } else {                    true                };                if should_apply_result {                    self.pending_plugin_enabled_writes.remove(&plugin_id);                    let update_succeeded = result.is_ok();                    if update_succeeded {                        self.refresh_plugin_mentions_after_config_write();                    }                    self.chat_widget                        .on_plugin_enabled_set(cwd, plugin_id, enabled, result);                }            }            AppEvent::FetchMcpInventory { detail, thread_id } => {                self.fetch_mcp_inventory(app_server, detail, thread_id);            }            AppEvent::McpInventoryLoaded {                result,                detail,                thread_id,            } => {                self.handle_mcp_inventory_result(result, detail, thread_id);            }            AppEvent::SkillsListLoaded { result } => {                self.handle_skills_list_result(                    result.map_err(|err| color_eyre::eyre::eyre!(err)),                    "failed to load skills on startup",                );            }            AppEvent::StartFileSearch(query) => {                self.file_search.on_user_query(query);            }            AppEvent::FileSearchResult { query, matches } => {                self.chat_widget.apply_file_search_result(query, matches);            }            AppEvent::RefreshRateLimits { origin } => {                self.refresh_rate_limits(app_server, origin);            }            AppEvent::RefreshTokenActivity { request_id } => {                self.refresh_token_activity(app_server, request_id);            }            AppEvent::OpenThreadGoalMenu { thread_id } => {                self.open_thread_goal_menu(app_server, thread_id).await;            }            AppEvent::OpenThreadGoalEditor { thread_id } => {                self.open_thread_goal_editor(app_server, thread_id).await;            }            AppEvent::SetThreadGoalDraft {                thread_id,                draft,                mode,            } => {                self.set_thread_goal_draft(app_server, thread_id, draft, mode)                    .await;            }            AppEvent::SetThreadGoalStatus { thread_id, status } => {                self.set_thread_goal_status(app_server, thread_id, status)                    .await;            }            AppEvent::ClearThreadGoal { thread_id } => {                self.clear_thread_goal(app_server, thread_id).await;            }            AppEvent::SendAddCreditsNudgeEmail { credit_type } => {                if self                    .chat_widget                    .start_add_credits_nudge_email_request(credit_type)                {                    self.send_add_credits_nudge_email(app_server, credit_type);                }            }            AppEvent::AddCreditsNudgeEmailFinished { result } => {                self.chat_widget                    .finish_add_credits_nudge_email_request(result);            }            AppEvent::RateLimitsLoaded { origin, result } => match result {                Ok(response) => {                    let rate_limit_reset_credits = response.rate_limit_reset_credits.clone();                    for snapshot in app_server_rate_limit_snapshots(response) {                        self.chat_widget.on_rate_limit_snapshot(Some(snapshot));                    }                    match origin {                        RateLimitRefreshOrigin::StartupPrefetch {                            reset_hint_request_id,                        } => {                            if self.chat_widget.finish_rate_limit_reset_hint_refresh(                                reset_hint_request_id,                                rate_limit_reset_credits.ok_or_else(|| {                                    "account/rateLimits/read response did not include rateLimitResetCredits"                                        .to_string()                                }),                            ) {                                self.insert_pending_usage_output_if_ready(tui);                            }                            tui.frame_requester().schedule_frame();                        }                        RateLimitRefreshOrigin::ResetConsume { request_id } => {                            self.chat_widget.finish_post_consume_reset_credits_refresh(                                request_id,                                rate_limit_reset_credits.ok_or_else(|| {                                    "account/rateLimits/read response did not include rateLimitResetCredits"                                        .to_string()                                }),                            );                            tui.frame_requester().schedule_frame();                        }                        RateLimitRefreshOrigin::StatusCommand { request_id } => {                            self.chat_widget                                .finish_status_rate_limit_refresh(request_id);                        }                    }                }                Err(err) => {                    tracing::warn!("account/rateLimits/read failed during TUI refresh: {err}");                    match origin {                        RateLimitRefreshOrigin::StartupPrefetch {                            reset_hint_request_id,                        } => {                            self.chat_widget.finish_rate_limit_reset_hint_refresh(                                reset_hint_request_id,                                Err(err),                            );                        }                        RateLimitRefreshOrigin::ResetConsume { request_id } => {                            self.chat_widget                                .finish_post_consume_reset_credits_refresh(request_id, Err(err));                        }                        RateLimitRefreshOrigin::StatusCommand { request_id } => {                            self.chat_widget                                .finish_status_rate_limit_refresh(request_id);                        }                    }                }            },            AppEvent::OpenTokenActivity => {                self.chat_widget                    .add_token_activity_output(crate::chatwidget::TokenActivityView::Daily);            }            AppEvent::OpenRateLimitResetCredits => {                let request_id = self.chat_widget.show_rate_limit_reset_loading_popup();                self.refresh_rate_limit_reset_credits(app_server, request_id);            }            AppEvent::RateLimitResetCreditsLoaded { request_id, result } => {                if let Err(err) = &result {                    tracing::warn!(                        "account/rateLimits/read failed during reset-credit refresh: {err}"                    );                }                self.chat_widget                    .finish_rate_limit_reset_credits_refresh(request_id, result);            }            AppEvent::ConsumeRateLimitResetCredit { idempotency_key } => {                let request_id = self.chat_widget.show_rate_limit_reset_consuming_popup();                self.consume_rate_limit_reset_credit(app_server, request_id, idempotency_key);            }            AppEvent::RateLimitResetCreditConsumed {                request_id,                idempotency_key,                result,            } => {                if let Err(err) = &result {                    tracing::warn!(                        "account/rateLimitResetCredit/consume failed during TUI request: {err}"                    );                }                if self.chat_widget.finish_rate_limit_reset_consume(                    request_id,                    idempotency_key,                    result,                ) {                    self.refresh_rate_limits(                        app_server,                        RateLimitRefreshOrigin::ResetConsume { request_id },                    );                }            }            AppEvent::TokenActivityLoaded { request_id, result } => {                if let Err(err) = &result {                    tracing::warn!("account/usage/read failed during TUI refresh: {err}");                }                if self                    .chat_widget                    .finish_token_activity_refresh(request_id, result)                {                    // Commit synchronously so an already queued /clear cannot overtake this card.                    // Do not route through ChatWidget::add_to_history: /usage may complete during                    // active work, and flushing an in-progress tool cell would corrupt its lifecycle.                    // If an answer stream is active, keep the settled card transient until its                    // provisional transcript cells have been consolidated.                    self.insert_pending_usage_output_if_ready(tui);                }            }            AppEvent::CommitPendingUsageOutput => {                self.insert_pending_usage_output_if_ready(tui);            }            AppEvent::CommitPendingUsageOutputAfterStreamShutdown => {                self.insert_pending_usage_output_after_stream_shutdown(tui);            }            AppEvent::ConnectorsLoaded { result, is_final } => {                self.chat_widget.on_connectors_loaded(result, is_final);            }            AppEvent::UpdateReasoningEffort(effort) => {                self.on_update_reasoning_effort(effort.clone());                self.sync_active_thread_reasoning_setting(app_server, effort)                    .await;            }            AppEvent::UpdateModel(model) => {                self.chat_widget.set_model(&model);                self.sync_active_thread_model_setting(app_server, model)                    .await;                self.sync_active_thread_service_tier_to_cached_session()                    .await;            }            AppEvent::UpdatePersonality(personality) => {                self.on_update_personality(personality);                self.sync_active_thread_personality_setting(app_server, personality)                    .await;            }            AppEvent::OpenReasoningPopup { model } => {                self.chat_widget.open_reasoning_popup(model);            }            AppEvent::OpenPlanReasoningScopePrompt { model, effort } => {                self.chat_widget                    .open_plan_reasoning_scope_prompt(model, effort);            }            AppEvent::OpenAllModelsPopup { models } => {                self.chat_widget.open_all_models_popup(models);            }            AppEvent::OpenFullAccessConfirmation {                preset,                return_to_permissions,                profile_selection,            } => {                self.chat_widget.open_full_access_confirmation(                    preset,                    return_to_permissions,                    profile_selection,                );            }            AppEvent::OpenWorldWritableWarningConfirmation {                preset,                profile_selection,                sample_paths,                extra_count,                failed_scan,            } => {                self.chat_widget.open_world_writable_warning_confirmation(                    preset,                    profile_selection,                    sample_paths,                    extra_count,                    failed_scan,                );            }            AppEvent::OpenFeedbackNote {                category,                include_logs,            } => {                self.chat_widget.open_feedback_note(category, include_logs);            }            AppEvent::OpenFeedbackConsent { category } => {                self.chat_widget.open_feedback_consent(category);            }            AppEvent::SubmitFeedback {                category,                reason,                turn_id,                include_logs,            } => {                self.submit_feedback(app_server, category, reason, turn_id, include_logs);            }            AppEvent::FeedbackSubmitted {                origin_thread_id,                category,                include_logs,                result,            } => {                self.handle_feedback_submitted(origin_thread_id, category, include_logs, result)                    .await;            }            AppEvent::LaunchExternalEditor => {                if self.chat_widget.external_editor_state() == ExternalEditorState::Active {                    self.launch_external_editor(tui).await;                }            }            AppEvent::OpenWindowsSandboxEnablePrompt {                preset,                profile_selection,            } => {                self.chat_widget                    .open_windows_sandbox_enable_prompt(preset, profile_selection);            }            AppEvent::OpenWindowsSandboxFallbackPrompt {                preset,                profile_selection,            } => {                self.session_telemetry.counter(                    "codex.windows_sandbox.fallback_prompt_shown",                    /*inc*/ 1,                    &[],                );                self.chat_widget.clear_windows_sandbox_setup_status();                if let Some(started_at) = self.windows_sandbox.setup_started_at.take() {                    self.session_telemetry.record_duration(                        "codex.windows_sandbox.elevated_setup_duration_ms",                        started_at.elapsed(),                        &[("result", "failure")],                    );                }                self.chat_widget                    .open_windows_sandbox_fallback_prompt(preset, profile_selection);            }            AppEvent::BeginWindowsSandboxElevatedSetup {                preset,                profile_selection,            } => {                #[cfg(any(target_os = "windows", test))]                if !self.chat_widget.windows_sandbox_mode_allowed(                    codex_config::types::WindowsSandboxModeToml::Elevated,                ) {                    tracing::warn!(                        "refusing to set up elevated Windows sandbox mode disallowed by requirements"                    );                    self.chat_widget.add_info_message(                        "That Windows sandbox option is disallowed by requirements.".to_string(),                        /*hint*/ None,                    );                    return Ok(AppRunControl::Continue);                }                #[cfg(target_os = "windows")]                {                    let setup_permissions = match self                        .windows_setup_permissions(&preset, profile_selection.as_ref())                        .await                    {                        Ok(setup_permissions) => setup_permissions,                        Err(err) => {                            tracing::warn!(                                error = %err,                                "failed to resolve permission profile for elevated Windows sandbox setup"                            );                            self.chat_widget.add_error_message(format!(                                "Failed to prepare Windows sandbox for the selected permission profile: {err}"                            ));                            return Ok(AppRunControl::Continue);                        }                    };                    let permission_profile = setup_permissions.permission_profile;                    let workspace_roots = setup_permissions.workspace_roots;                    let command_cwd = self.config.cwd.clone();                    let env_map: std::collections::HashMap<String, String> =                        std::env::vars().collect();                    let codex_home = self.config.codex_home.clone();                    let tx = self.app_event_tx.clone();                    // If the elevated setup already ran on this machine, don't prompt for                    // elevation again - just flip the config to use the elevated path.                    if crate::windows_sandbox::sandbox_setup_is_complete(codex_home.as_path()) {                        tx.send(AppEvent::EnableWindowsSandboxForAgentMode {                            preset,                            mode: WindowsSandboxEnableMode::Elevated,                            profile_selection,                        });                        return Ok(AppRunControl::Continue);                    }                    self.chat_widget.show_windows_sandbox_setup_status();                    self.windows_sandbox.setup_started_at = Some(Instant::now());                    let session_telemetry = self.session_telemetry.clone();                    tokio::task::spawn_blocking(move || {                        let result = crate::windows_sandbox::run_elevated_setup(                            &permission_profile,                            workspace_roots.as_slice(),                            command_cwd.as_path(),                            &env_map,                            codex_home.as_path(),                        );                        let event = match result {                            Ok(()) => {                                session_telemetry.counter(                                    "codex.windows_sandbox.elevated_setup_success",                                    /*inc*/ 1,                                    &[],                                );                                AppEvent::EnableWindowsSandboxForAgentMode {                                    preset: preset.clone(),                                    mode: WindowsSandboxEnableMode::Elevated,                                    profile_selection: profile_selection.clone(),                                }                            }                            Err(err) => {                                let mut code_tag: Option<String> = None;                                let mut message_tag: Option<String> = None;                                if let Some((code, message)) =                                    crate::windows_sandbox::elevated_setup_failure_details(&err)                                {                                    code_tag = Some(code);                                    message_tag = Some(message);                                }                                let mut tags: Vec<(&str, &str)> = Vec::new();                                if let Some(code) = code_tag.as_deref() {                                    tags.push(("code", code));                                }                                if let Some(message) = message_tag.as_deref() {                                    tags.push(("message", message));                                }                                session_telemetry.counter(                                    crate::windows_sandbox::elevated_setup_failure_metric_name(                                        &err,                                    ),                                    /*inc*/ 1,                                    &tags,                                );                                tracing::error!(                                    error = %err,                                    "failed to run elevated Windows sandbox setup"                                );                                AppEvent::OpenWindowsSandboxFallbackPrompt {                                    preset,                                    profile_selection,                                }                            }                        };                        tx.send(event);                    });                }                #[cfg(not(target_os = "windows"))]                {                    let _ = (preset, profile_selection);                }            }            AppEvent::BeginWindowsSandboxLegacySetup {                preset,                profile_selection,            } => {                #[cfg(any(target_os = "windows", test))]                if !self.chat_widget.windows_sandbox_mode_allowed(                    codex_config::types::WindowsSandboxModeToml::Unelevated,                ) {                    tracing::warn!(                        "refusing to set up unelevated Windows sandbox mode disallowed by requirements"                    );                    self.chat_widget.add_info_message(                        "That Windows sandbox option is disallowed by requirements.".to_string(),                        /*hint*/ None,                    );                    return Ok(AppRunControl::Continue);                }                #[cfg(target_os = "windows")]                {                    let setup_permissions = match self                        .windows_setup_permissions(&preset, profile_selection.as_ref())                        .await                    {                        Ok(setup_permissions) => setup_permissions,                        Err(err) => {                            tracing::warn!(                                error = %err,                                "failed to resolve permission profile for legacy Windows sandbox setup"                            );                            self.chat_widget.add_error_message(format!(                                "Failed to prepare Windows sandbox for the selected permission profile: {err}"                            ));                            return Ok(AppRunControl::Continue);                        }                    };                    let permission_profile = setup_permissions.permission_profile;                    let workspace_roots = setup_permissions.workspace_roots;                    let command_cwd = self.config.cwd.clone();                    let env_map: std::collections::HashMap<String, String> =                        std::env::vars().collect();                    let codex_home = self.config.codex_home.clone();                    let tx = self.app_event_tx.clone();                    let session_telemetry = self.session_telemetry.clone();                    self.chat_widget.show_windows_sandbox_setup_status();                    tokio::task::spawn_blocking(move || {                        if let Err(err) =                            codex_windows_sandbox::run_windows_sandbox_legacy_preflight(                                &permission_profile,                                workspace_roots.as_slice(),                                codex_home.as_path(),                                command_cwd.as_path(),                                &env_map,                            )                        {                            session_telemetry.counter(                                "codex.windows_sandbox.legacy_setup_preflight_failed",                                /*inc*/ 1,                                &[],                            );                            tracing::warn!(                                error = %err,                                "failed to preflight non-admin Windows sandbox setup"                            );                        }                        tx.send(AppEvent::EnableWindowsSandboxForAgentMode {                            preset,                            mode: WindowsSandboxEnableMode::Legacy,                            profile_selection,                        });                    });                }                #[cfg(not(target_os = "windows"))]                {                    let _ = (preset, profile_selection);                }            }            AppEvent::BeginWindowsSandboxGrantReadRoot { path } => {                #[cfg(target_os = "windows")]                {                    self.chat_widget                        .add_to_history(history_cell::new_info_event(                            format!("Granting sandbox read access to {path} ..."),                            /*hint*/ None,                        ));                    let permission_profile = self.config.permissions.effective_permission_profile();                    let workspace_roots = self.config.effective_workspace_roots();                    let command_cwd = self.config.cwd.clone();                    let env_map: std::collections::HashMap<String, String> =                        std::env::vars().collect();                    let codex_home = self.config.codex_home.clone();                    let tx = self.app_event_tx.clone();                    tokio::task::spawn_blocking(move || {                        let requested_path = PathBuf::from(path);                        let event = match crate::windows_sandbox::grant_read_root_non_elevated(                            &permission_profile,                            workspace_roots.as_slice(),                            command_cwd.as_path(),                            &env_map,                            codex_home.as_path(),                            requested_path.as_path(),                        ) {                            Ok(canonical_path) => AppEvent::WindowsSandboxGrantReadRootCompleted {                                path: canonical_path,                                error: None,                            },                            Err(err) => AppEvent::WindowsSandboxGrantReadRootCompleted {                                path: requested_path,                                error: Some(err.to_string()),                            },                        };                        tx.send(event);                    });                }                #[cfg(not(target_os = "windows"))]                {                    let _ = path;                }            }            AppEvent::WindowsSandboxGrantReadRootCompleted { path, error } => match error {                Some(err) => {                    self.chat_widget                        .add_to_history(history_cell::new_error_event(format!("Error: {err}")));                }                None => {                    self.chat_widget                        .add_to_history(history_cell::new_info_event(                            format!("Sandbox read access granted for {}", path.display()),                            /*hint*/ None,                        ));                }            },            AppEvent::EnableWindowsSandboxForAgentMode {                preset,                mode,                profile_selection,            } => {                #[cfg(target_os = "windows")]                {                    self.chat_widget.clear_windows_sandbox_setup_status();                    if let Some(started_at) = self.windows_sandbox.setup_started_at.take() {                        self.session_telemetry.record_duration(                            "codex.windows_sandbox.elevated_setup_duration_ms",                            started_at.elapsed(),                            &[("result", "success")],                        );                    }                    let selected_mode = match mode {                        WindowsSandboxEnableMode::Elevated => WindowsSandboxModeToml::Elevated,                        WindowsSandboxEnableMode::Legacy => WindowsSandboxModeToml::Unelevated,                    };                    let elevated_enabled = selected_mode == WindowsSandboxModeToml::Elevated;                    if !self.chat_widget.windows_sandbox_mode_allowed(selected_mode) {                        tracing::warn!(                            ?selected_mode,                            "refusing to persist Windows sandbox mode disallowed by requirements"                        );                        self.chat_widget.add_info_message(                            "That Windows sandbox option is disallowed by requirements."                                .to_string(),                            /*hint*/ None,                        );                        return Ok(AppRunControl::Continue);                    }                    let edits =                        crate::config_update::build_windows_sandbox_mode_edits(elevated_enabled);                    match crate::config_update::write_config_batch(                        app_server.request_handle(),                        edits,                    )                    .await                    {                        Ok(response) if response.status == WriteStatus::OkOverridden => {                            self.sync_windows_sandbox_after_overridden_write(app_server, &response)                                .await;                        }                        Ok(_) => {                            if elevated_enabled {                                self.config.set_windows_sandbox_enabled(/*value*/ false);                                self.config                                    .set_windows_elevated_sandbox_enabled(/*value*/ true);                            } else {                                self.config.set_windows_sandbox_enabled(/*value*/ true);                                self.config                                    .set_windows_elevated_sandbox_enabled(/*value*/ false);                            }                            self.chat_widget.set_windows_sandbox_mode(                                self.config.permissions.windows_sandbox_mode,                            );                            let windows_sandbox_level =                                crate::windows_sandbox::level_from_config(&self.config);                            if let Some((sample_paths, extra_count, failed_scan)) =                                self.chat_widget.world_writable_warning_details()                            {                                self.app_event_tx.send(AppEvent::CodexOp(                                    AppCommand::override_turn_context(                                        /*cwd*/ None,                                        /*approval_policy*/ None,                                        /*approvals_reviewer*/ None,                                        /*permission_profile*/ None,                                        /*active_permission_profile*/ None,                                        #[cfg(target_os = "windows")]                                        Some(windows_sandbox_level),                                        /*model*/ None,                                        /*effort*/ None,                                        /*summary*/ None,                                        /*service_tier*/ None,                                        /*collaboration_mode*/ None,                                        /*personality*/ None,                                    ),                                ));                                self.app_event_tx.send(                                    AppEvent::OpenWorldWritableWarningConfirmation {                                        preset: Some(preset.clone()),                                        profile_selection: profile_selection.clone(),                                        sample_paths,                                        extra_count,                                        failed_scan,                                    },                                );                            } else if let Some(selection) = profile_selection {                                self.app_event_tx.send(AppEvent::CodexOp(                                    AppCommand::override_turn_context(                                        /*cwd*/ None,                                        /*approval_policy*/ None,                                        /*approvals_reviewer*/ None,                                        /*permission_profile*/ None,                                        /*active_permission_profile*/ None,                                        #[cfg(target_os = "windows")]                                        Some(windows_sandbox_level),                                        /*model*/ None,                                        /*effort*/ None,                                        /*summary*/ None,                                        /*service_tier*/ None,                                        /*collaboration_mode*/ None,                                        /*personality*/ None,                                    ),                                ));                                if self.apply_permission_profile_selection(selection).await {                                    self.chat_widget.submit_initial_user_message_if_pending();                                }                                self.chat_widget.add_plain_history_lines(vec![                                    Line::from(vec!["• ".dim(), "Sandbox ready".into()]),                                    Line::from(vec![                                        "  ".into(),                                        "Codex can now safely edit files and execute commands in your computer"                                            .dark_gray(),                                    ]),                                ]);                            } else {                                self.app_event_tx.send(AppEvent::CodexOp(                                    AppCommand::override_turn_context(                                        /*cwd*/ None,                                        Some(AskForApproval::from(preset.approval)),                                        Some(self.config.approvals_reviewer),                                        Some(preset.permission_profile.clone()),                                        Some(preset.active_permission_profile.clone()),                                        #[cfg(target_os = "windows")]                                        Some(windows_sandbox_level),                                        /*model*/ None,                                        /*effort*/ None,                                        /*summary*/ None,                                        /*service_tier*/ None,                                        /*collaboration_mode*/ None,                                        /*personality*/ None,                                    ),                                ));                                self.app_event_tx.send(AppEvent::UpdateAskForApprovalPolicy(                                    AskForApproval::from(preset.approval),                                ));                                self.app_event_tx                                    .send(AppEvent::UpdateActivePermissionProfile(                                        preset.active_permission_profile.clone(),                                    ));                                self.chat_widget.add_plain_history_lines(vec![                                    Line::from(vec!["• ".dim(), "Sandbox ready".into()]),                                    Line::from(vec![                                        "  ".into(),                                        "Codex can now safely edit files and execute commands in your computer"                                            .dark_gray(),                                    ]),                                ]);                            }                        }                        Err(err) => {                            tracing::error!(                                error = %err,                                "failed to enable Windows sandbox feature"                            );                            self.chat_widget.add_error_message(format!(                                "Failed to enable the Windows sandbox feature: {err}"                            ));                        }                    }                }                #[cfg(not(target_os = "windows"))]                {                    let _ = (preset, mode, profile_selection);                }            }            AppEvent::PersistModelSelection { model, effort } => {                match crate::config_update::write_config_batch(                    app_server.request_handle(),                    crate::config_update::build_model_selection_edits(                        model.as_str(),                        effort.as_ref(),                    ),                )                .await                {                    Ok(_) => {                        let effort_label = effort                            .as_ref()                            .map(std::string::ToString::to_string)                            .unwrap_or_else(|| "default".to_string());                        tracing::info!("Selected model: {model}, Selected effort: {effort_label}");                        let mut message = format!("Model changed to {model}");                        if let Some(label) = Self::reasoning_label_for(&model, effort.as_ref()) {                            message.push(' ');                            message.push_str(&label);                        }                        self.chat_widget.add_info_message(message, /*hint*/ None);                    }                    Err(err) => {                        let error = format_config_error(&err);                        tracing::error!(                            error = %error,                            "failed to persist model selection"                        );                        self.chat_widget                            .add_error_message(format!("Failed to save default model: {error}"));                    }                }            }            AppEvent::PluginUninstallLoaded {                cwd,                plugin_id: _plugin_id,                plugin_display_name,                result,            } => {                let uninstall_succeeded = result.is_ok();                if uninstall_succeeded {                    self.refresh_plugin_mentions_after_config_write();                }                self.chat_widget.on_plugin_uninstall_loaded(                    cwd.clone(),                    plugin_display_name,                    result,                );                if uninstall_succeeded                    && self.chat_widget.config_ref().cwd.as_path() == cwd.as_path()                {                    self.fetch_plugins_list(app_server, cwd);                }            }            AppEvent::RefreshPluginMentions => {                self.refresh_plugin_mentions(app_server);            }            AppEvent::PluginMentionsLoaded { mut plugins } => {                if !self.config.features.enabled(Feature::Plugins) {                    plugins = None;                }                self.chat_widget.on_plugin_mentions_loaded(plugins);            }            AppEvent::PersistPersonalitySelection { personality } => {                match crate::config_update::write_config_batch(                    app_server.request_handle(),                    vec![crate::config_update::replace_config_value(                        "personality",                        serde_json::json!(personality.to_string()),                    )],                )                .await                {                    Ok(_) => {                        let label = Self::personality_label(personality);                        let message = format!("Personality set to {label}");                        self.chat_widget.add_info_message(message, /*hint*/ None);                    }                    Err(err) => {                        tracing::error!(                            error = %err,                            "failed to persist personality selection"                        );                        self.chat_widget.add_error_message(format!(                            "Failed to save default personality: {err}"                        ));                    }                }            }            AppEvent::PersistServiceTierSelection { service_tier } => {                self.refresh_status_line();                self.config.service_tier = service_tier.clone();                self.sync_active_thread_service_tier_to_cached_session()                    .await;                let edits = crate::config_update::build_service_tier_selection_edits(                    service_tier.as_deref(),                );                match crate::config_update::write_config_batch(app_server.request_handle(), edits)                    .await                {                    Ok(_) => {                        let message = if let Some(service_tier) = service_tier {                            format!("Service tier set to {service_tier}")                        } else {                            "Service tier cleared".to_string()                        };                        self.chat_widget.add_info_message(message, /*hint*/ None);                    }                    Err(err) => {                        tracing::error!(error = %err, "failed to persist service tier selection");                        self.chat_widget.add_error_message(format!(                            "Failed to save default service tier: {err}"                        ));                    }                }            }            AppEvent::UpdateAskForApprovalPolicy(policy) => {                let mut config = self.config.clone();                if !self.try_set_approval_policy_on_config(                    &mut config,                    policy,                    "Failed to set approval policy",                    "failed to set approval policy on app config",                ) {                    return Ok(AppRunControl::Continue);                }                self.config = config;                let approval_policy =                    AskForApproval::from(self.config.permissions.approval_policy.value());                self.runtime_approval_policy_override = Some(approval_policy);                self.chat_widget.set_approval_policy(approval_policy);                self.sync_active_thread_permission_settings_to_cached_session()                    .await;            }            AppEvent::UpdateActivePermissionProfile(active_permission_profile) => {                let mut config = self.config.clone();                let Some(permission_profile) = self                    .try_set_builtin_active_permission_profile_on_config(                        &mut config,                        active_permission_profile.clone(),                        "Failed to set permission profile",                        "failed to set active permission profile on app config",                    )                else {                    return Ok(AppRunControl::Continue);                };                #[cfg(target_os = "windows")]                let permission_profile_is_managed_restricted =                    managed_filesystem_sandbox_is_restricted(&permission_profile);                let permission_profile_for_chat = permission_profile.clone();                self.config = config;                if let Err(err) = self                    .chat_widget                    .set_permission_profile_from_session_snapshot(                        PermissionProfileSnapshot::active(                            permission_profile_for_chat,                            active_permission_profile,                        ),                    )                {                    tracing::warn!(%err, "failed to set permission profile on chat config");                    self.chat_widget                        .add_error_message(format!("Failed to set permission profile: {err}"));                    return Ok(AppRunControl::Continue);                }                self.runtime_permission_profile_override =                    Some(RuntimePermissionProfileOverride::from_config(&self.config));                self.sync_active_thread_permission_settings_to_cached_session()                    .await;                self.chat_widget.submit_initial_user_message_if_pending();                // If a managed filesystem sandbox is active, run the Windows                // world-writable scan.                #[cfg(target_os = "windows")]                {                    // One-shot suppression if the user just confirmed continue.                    if self.windows_sandbox.skip_world_writable_scan_once {                        self.windows_sandbox.skip_world_writable_scan_once = false;                        return Ok(AppRunControl::Continue);                    }                    let should_check = crate::windows_sandbox::level_from_config(&self.config)                        != WindowsSandboxLevel::Disabled                        && permission_profile_is_managed_restricted                        && !self.chat_widget.world_writable_warning_hidden();                    if should_check {                        let cwd = self.config.cwd.clone();                        let workspace_roots = self.config.effective_workspace_roots();                        let env_map: std::collections::HashMap<String, String> =                            std::env::vars().collect();                        let tx = self.app_event_tx.clone();                        let logs_base_dir = self.config.codex_home.clone();                        let permission_profile =                            self.config.permissions.effective_permission_profile();                        Self::spawn_world_writable_scan(                            cwd,                            workspace_roots,                            env_map,                            logs_base_dir,                            permission_profile,                            tx,                        );                    }                }            }            AppEvent::SelectPermissionProfile(selection) => {                if self.apply_permission_profile_selection(selection).await {                    self.chat_widget.submit_initial_user_message_if_pending();                }            }            AppEvent::UpdateApprovalsReviewer(policy) => {                self.config.approvals_reviewer = policy;                self.chat_widget.set_approvals_reviewer(policy);                self.sync_active_thread_permission_settings_to_cached_session()                    .await;                if let Err(err) = crate::config_update::write_config_batch(                    app_server.request_handle(),                    vec![crate::config_update::replace_config_value(                        "approvals_reviewer",                        serde_json::json!(policy.to_string()),                    )],                )                .await                {                    tracing::error!(                        error = %err,                        "failed to persist approvals reviewer update"                    );                    self.chat_widget                        .add_error_message(format!("Failed to save approvals reviewer: {err}"));                }            }            AppEvent::UpdateFeatureFlags { updates } => {                self.update_feature_flags(app_server, updates).await;            }            AppEvent::UpdateMemorySettings {                use_memories,                generate_memories,            } => {                self.update_memory_settings_with_app_server(                    app_server,                    use_memories,                    generate_memories,                )                .await;            }            AppEvent::ResetMemories => {                self.reset_memories_with_app_server(app_server).await;            }            AppEvent::SkipNextWorldWritableScan => {                self.windows_sandbox.skip_world_writable_scan_once = true;            }            AppEvent::UpdateFullAccessWarningAcknowledged(ack) => {                self.chat_widget.set_full_access_warning_acknowledged(ack);            }            AppEvent::UpdateWorldWritableWarningAcknowledged(ack) => {                self.chat_widget                    .set_world_writable_warning_acknowledged(ack);            }            AppEvent::UpdateRateLimitSwitchPromptHidden(hidden) => {                self.chat_widget.set_rate_limit_switch_prompt_hidden(hidden);            }            AppEvent::UpdatePlanModeReasoningEffort(effort) => {                self.config.plan_mode_reasoning_effort = effort.clone();                self.chat_widget.set_plan_mode_reasoning_effort(effort);                self.sync_active_thread_plan_mode_reasoning_setting(app_server)                    .await;            }            AppEvent::PersistFullAccessWarningAcknowledged => {                if let Err(err) = ConfigEditsBuilder::for_config(&self.config)                    .set_hide_full_access_warning(/*acknowledged*/ true)                    .apply()                    .await                {                    tracing::error!(                        error = %err,                        "failed to persist full access warning acknowledgement"                    );                    self.chat_widget.add_error_message(format!(                        "Failed to save full access confirmation preference: {err}"                    ));                }            }            AppEvent::PersistWorldWritableWarningAcknowledged => {                if let Err(err) = ConfigEditsBuilder::for_config(&self.config)                    .set_hide_world_writable_warning(/*acknowledged*/ true)                    .apply()                    .await                {                    tracing::error!(                        error = %err,                        "failed to persist world-writable warning acknowledgement"                    );                    self.chat_widget.add_error_message(format!(                        "Failed to save Agent mode warning preference: {err}"                    ));                }            }            AppEvent::PersistRateLimitSwitchPromptHidden => {                if let Err(err) = ConfigEditsBuilder::for_config(&self.config)                    .set_hide_rate_limit_model_nudge(/*acknowledged*/ true)                    .apply()                    .await                {                    tracing::error!(                        error = %err,                        "failed to persist rate limit switch prompt preference"                    );                    self.chat_widget.add_error_message(format!(                        "Failed to save rate limit reminder preference: {err}"                    ));                }            }            AppEvent::PersistPlanModeReasoningEffort(effort) => {                let key_path = "plan_mode_reasoning_effort";                let edit = if let Some(effort) = effort {                    crate::config_update::replace_config_value(                        key_path,                        serde_json::json!(effort.to_string()),                    )                } else {                    crate::config_update::clear_config_value(key_path)                };                if let Err(err) = crate::config_update::write_config_batch(                    app_server.request_handle(),                    vec![edit],                )                .await                {                    tracing::error!(                        error = %err,                        "failed to persist plan mode reasoning effort"                    );                    self.chat_widget.add_error_message(format!(                        "Failed to save Plan mode reasoning effort: {err}"                    ));                }            }            AppEvent::PersistModelMigrationPromptAcknowledged {                from_model,                to_model,            } => {                if let Err(err) = ConfigEditsBuilder::for_config(&self.config)                    .record_model_migration_seen(from_model.as_str(), to_model.as_str())                    .apply()                    .await                {                    tracing::error!(                        error = %err,                        "failed to persist model migration prompt acknowledgement"                    );                    self.chat_widget.add_error_message(format!(                        "Failed to save model migration prompt preference: {err}"                    ));                }            }            AppEvent::OpenApprovalsPopup => {                self.chat_widget.open_approvals_popup();            }            AppEvent::OpenAgentPicker => {                self.open_agent_picker(app_server).await;            }            AppEvent::SelectAgentThread(thread_id) => {                self.select_agent_thread_and_discard_side(tui, app_server, thread_id)                    .await?;            }            AppEvent::StartSide {                parent_thread_id,                user_message,            } => {                return self                    .handle_start_side(tui, app_server, parent_thread_id, user_message)                    .await;            }            AppEvent::OpenSkillsList => {                self.chat_widget.open_skills_list();            }            AppEvent::OpenManageSkillsPopup => {                self.chat_widget.open_manage_skills_popup();            }            AppEvent::SetSkillEnabled { path, enabled } => {                match crate::config_update::write_skill_enabled(                    app_server.request_handle(),                    path.clone(),                    enabled,                )                .await                {                    Ok(()) => {                        self.chat_widget.update_skill_enabled(path, enabled);                    }                    Err(err) => {                        let path_display = path.display();                        self.chat_widget.add_error_message(format!(                            "Failed to update skill config for {path_display}: {err}"                        ));                    }                }            }            AppEvent::SetAppEnabled { id, enabled } => {                let edits = if enabled {                    vec![                        crate::config_update::clear_config_value(                            crate::config_update::app_scoped_key_path(&id, "enabled"),                        ),                        crate::config_update::clear_config_value(                            crate::config_update::app_scoped_key_path(&id, "disabled_reason"),                        ),                    ]                } else {                    vec![                        crate::config_update::replace_config_value(                            crate::config_update::app_scoped_key_path(&id, "enabled"),                            serde_json::json!(false),                        ),                        crate::config_update::replace_config_value(                            crate::config_update::app_scoped_key_path(&id, "disabled_reason"),                            serde_json::json!("user"),                        ),                    ]                };                match crate::config_update::write_config_batch(app_server.request_handle(), edits)                    .await                {                    Ok(_) => {                        self.chat_widget.update_connector_enabled(&id, enabled);                    }                    Err(err) => {                        self.chat_widget.add_error_message(format!(                            "Failed to update app config for {id}: {err}"                        ));                    }                }            }            AppEvent::SetHookEnabled { key, enabled } => {                self.set_hook_enabled(app_server, key, enabled);            }            AppEvent::TrustHook { key, current_hash } => {                self.trust_hook(app_server, key, current_hash);            }            AppEvent::TrustHooks { updates } => {                self.trust_hooks(app_server, updates);            }            AppEvent::HookEnabledSet {                key,                enabled,                result,            } => {                let queued_enabled = self                    .pending_hook_enabled_writes                    .get_mut(&key)                    .and_then(Option::take);                let should_apply_result = if let Some(queued_enabled) = queued_enabled                    && (result.is_err() || queued_enabled != enabled)                {                    self.spawn_hook_enabled_write(app_server, key.clone(), queued_enabled);                    false                } else {                    true                };                if should_apply_result {                    self.pending_hook_enabled_writes.remove(&key);                    if let Err(err) = result {                        self.chat_widget.add_error_message(err);                    }                }            }            AppEvent::HookTrusted { result } => {                if let Err(err) = result {                    self.chat_widget.add_error_message(err);                }            }            AppEvent::OpenPermissionsPopup => {                self.chat_widget.open_permissions_popup();            }            AppEvent::OpenReviewBranchPicker(cwd) => {                self.chat_widget.show_review_branch_picker(&cwd).await;            }            AppEvent::OpenReviewCommitPicker(cwd) => {                self.chat_widget.show_review_commit_picker(&cwd).await;            }            AppEvent::OpenReviewCustomPrompt => {                self.chat_widget.show_review_custom_prompt();            }            AppEvent::SubmitUserMessageWithMode {                text,                collaboration_mode,            } => {                self.chat_widget                    .submit_user_message_with_mode(text, collaboration_mode);            }            AppEvent::ManageSkillsClosed => {                self.chat_widget.handle_manage_skills_closed();            }            AppEvent::FullScreenApprovalRequest(request) => match request {                ApprovalRequest::ApplyPatch { cwd, changes, .. } => {                    let _ = tui.enter_alt_screen();                    let diff_summary = DiffSummary::new(changes, cwd);                    self.overlay = Some(Overlay::new_static_with_renderables(                        vec![diff_summary.into()],                        "P A T C H".to_string(),                        self.keymap.pager.clone(),                    ));                }                ApprovalRequest::Exec { command, .. } => {                    let _ = tui.enter_alt_screen();                    let full_cmd = strip_bash_lc_and_escape(&command);                    let full_cmd_lines = highlight_bash_to_lines(&full_cmd);                    self.overlay = Some(Overlay::new_static_with_lines(                        full_cmd_lines,                        "E X E C".to_string(),                        self.keymap.pager.clone(),                    ));                }                ApprovalRequest::Permissions {                    environment_id,                    permissions,                    reason,                    ..                } => {                    let _ = tui.enter_alt_screen();                    let mut lines = Vec::new();                    if let Some(environment_id) = environment_id {                        lines.push(Line::from(vec![                            "Environment: ".into(),                            environment_id.bold(),                        ]));                        lines.push(Line::from(""));                    }                    if let Some(reason) = reason {                        lines.push(Line::from(vec!["Reason: ".into(), reason.italic()]));                        lines.push(Line::from(""));                    }                    if let Some(rule_line) =                        crate::bottom_pane::format_requested_permissions_rule(&permissions)                    {                        lines.push(Line::from(vec![                            "Permission rule: ".into(),                            rule_line.cyan(),                        ]));                    }                    self.overlay = Some(Overlay::new_static_with_renderables(                        vec![Box::new(Paragraph::new(lines).wrap(Wrap { trim: false }))],                        "P E R M I S S I O N S".to_string(),                        self.keymap.pager.clone(),                    ));                }                ApprovalRequest::McpElicitation {                    server_name,                    message,                    ..                } => {                    let _ = tui.enter_alt_screen();                    let paragraph = Paragraph::new(vec![                        Line::from(vec!["Server: ".into(), server_name.bold()]),                        Line::from(""),                        Line::from(message),                    ])                    .wrap(Wrap { trim: false });                    self.overlay = Some(Overlay::new_static_with_renderables(                        vec![Box::new(paragraph)],                        "E L I C I T A T I O N".to_string(),                        self.keymap.pager.clone(),                    ));                }            },            AppEvent::StatusLineSetup {                items,                use_theme_colors,            } => {                let ids = items.iter().map(ToString::to_string).collect::<Vec<_>>();                let items_edit = crate::legacy_core::config::edit::status_line_items_edit(&ids);                let colors_edit =                    crate::legacy_core::config::edit::status_line_use_colors_edit(use_theme_colors);                let apply_result = ConfigEditsBuilder::for_config(&self.config)                    .with_edits([items_edit, colors_edit])                    .apply()                    .await;                match apply_result {                    Ok(()) => {                        self.config.tui_status_line = Some(ids.clone());                        self.config.tui_status_line_use_colors = use_theme_colors;                        self.chat_widget.setup_status_line(items, use_theme_colors);                    }                    Err(err) => {                        let error = format_config_error(&err);                        tracing::error!(error = %error, "failed to persist status line settings; keeping previous selection");                        self.chat_widget.add_error_message(format!(                            "Failed to save status line settings: {error}"                        ));                    }                }            }            AppEvent::StatusLineBranchUpdated { cwd, branch } => {                self.chat_widget.set_status_line_branch(cwd, branch);                self.refresh_status_line();            }            AppEvent::StatusLineGitSummaryUpdated { cwd, summary } => {                self.chat_widget.set_status_line_git_summary(cwd, summary);                self.refresh_status_line();            }            AppEvent::StatusLineSetupCancelled => {                self.chat_widget.cancel_status_line_setup();            }            AppEvent::TerminalTitleSetup { items } => {                let ids = items.iter().map(ToString::to_string).collect::<Vec<_>>();                let edit = crate::legacy_core::config::edit::terminal_title_items_edit(&ids);                let apply_result = ConfigEditsBuilder::for_config(&self.config)                    .with_edits([edit])                    .apply()                    .await;                match apply_result {                    Ok(()) => {                        self.config.tui_terminal_title = Some(ids.clone());                        self.chat_widget.setup_terminal_title(items);                    }                    Err(err) => {                        tracing::error!(error = %err, "failed to persist terminal title items; keeping previous selection");                        self.chat_widget.revert_terminal_title_setup_preview();                        self.chat_widget.add_error_message(format!(                            "Failed to save terminal title items: {err}"                        ));                    }                }            }            AppEvent::TerminalTitleSetupPreview { items } => {                self.chat_widget.preview_terminal_title(items);            }            AppEvent::TerminalTitleSetupCancelled => {                self.chat_widget.cancel_terminal_title_setup();            }            AppEvent::SyntaxThemeSelected { name } => {                let edit = crate::legacy_core::config::edit::syntax_theme_edit(&name);                let apply_result = ConfigEditsBuilder::for_config(&self.config)                    .with_edits([edit])                    .apply()                    .await;                match apply_result {                    Ok(()) => {                        // Ensure the selected theme is active in the current                        // session.  The preview callback covers arrow-key                        // navigation, but if the user presses Enter without                        // navigating, the runtime theme must still be applied.                        if let Some(theme) = crate::render::highlight::resolve_theme_by_name(                            &name,                            Some(&self.config.codex_home),                        ) {                            crate::render::highlight::set_syntax_theme(theme);                        }                        self.sync_tui_theme_selection(name);                        self.refresh_status_line();                        tui.frame_requester().schedule_frame();                    }                    Err(err) => {                        self.restore_runtime_theme_from_config();                        self.refresh_status_line();                        tracing::error!(error = %err, "failed to persist theme selection");                        self.chat_widget                            .add_error_message(format!("Failed to save theme: {err}"));                    }                }            }            AppEvent::SyntaxThemePreviewed => {                self.refresh_status_line();                tui.frame_requester().schedule_frame();            }            AppEvent::OpenKeymapActionMenu { context, action } => {                self.chat_widget                    .open_keymap_action_menu(context, action, &self.keymap);            }            AppEvent::OpenKeymapReplaceBindingMenu { context, action } => {                self.chat_widget                    .open_keymap_replace_binding_menu(context, action, &self.keymap);            }            AppEvent::OpenKeymapCapture {                context,                action,                intent,            } => {                self.chat_widget                    .open_keymap_capture(context, action, intent, &self.keymap);            }            AppEvent::OpenKeymapDebug => {                self.chat_widget.open_keymap_debug(&self.keymap);            }            AppEvent::KeymapCaptured {                context,                action,                key,                intent,            } => {                self.apply_keymap_capture(context, action, key, intent)                    .await;            }            AppEvent::KeymapCleared { context, action } => {                self.apply_keymap_clear(context, action).await;            }        }        Ok(AppRunControl::Continue)    }    async fn apply_keymap_capture(        &mut self,        context: String,        action: String,        key: String,        intent: crate::app_event::KeymapEditIntent,    ) {        let outcome = match crate::keymap_setup::keymap_with_edit(            &self.config.tui_keymap,            &self.keymap,            &context,            &action,            &key,            &intent,        ) {            Ok(outcome) => outcome,            Err(err) => {                self.chat_widget.add_error_message(err);                return;            }        };        let (keymap_config, bindings, message) = match outcome {            crate::keymap_setup::KeymapEditOutcome::Updated {                keymap_config,                bindings,                message,            } => (*keymap_config, bindings, message),            crate::keymap_setup::KeymapEditOutcome::Unchanged { message } => {                self.chat_widget.add_info_message(message, /*hint*/ None);                return;            }        };        let runtime_keymap = match RuntimeKeymap::from_config(&keymap_config) {            Ok(runtime_keymap) => runtime_keymap,            Err(err) => {                let params = crate::keymap_setup::build_keymap_conflict_params(                    context, action, key, intent, err,                );                self.chat_widget.show_selection_view(params);                return;            }        };        let edit =            crate::legacy_core::config::edit::keymap_bindings_edit(&context, &action, &bindings);        match ConfigEditsBuilder::for_config(&self.config)            .with_edits([edit])            .apply()            .await        {            Ok(()) => {                self.config.tui_keymap = keymap_config.clone();                self.keymap = runtime_keymap.clone();                self.chat_widget                    .apply_keymap_update(keymap_config, &runtime_keymap);                self.chat_widget                    .return_to_keymap_picker(&context, &action, &runtime_keymap);                self.chat_widget.add_info_message(message, /*hint*/ None);            }            Err(err) => {                tracing::error!(error = %err, "failed to persist keymap binding");                self.chat_widget                    .add_error_message(format!("Failed to save shortcut: {err}"));            }        }    }    fn refresh_plugin_mentions_after_config_write(&mut self) {        self.chat_widget.refresh_plugin_mentions();        self.chat_widget.submit_op(AppCommand::reload_user_config());    }    async fn apply_keymap_clear(&mut self, context: String, action: String) {        let keymap_config = match crate::keymap_setup::keymap_without_custom_binding(            &self.config.tui_keymap,            &context,            &action,        ) {            Ok(keymap_config) => keymap_config,            Err(err) => {                self.chat_widget.add_error_message(err);                return;            }        };        let runtime_keymap = match RuntimeKeymap::from_config(&keymap_config) {            Ok(runtime_keymap) => runtime_keymap,            Err(err) => {                self.chat_widget                    .add_error_message(format!("Failed to refresh shortcuts: {err}"));                return;            }        };        let edit = crate::legacy_core::config::edit::keymap_binding_clear_edit(&context, &action);        match ConfigEditsBuilder::for_config(&self.config)            .with_edits([edit])            .apply()            .await        {            Ok(()) => {                self.config.tui_keymap = keymap_config.clone();                self.keymap = runtime_keymap.clone();                self.chat_widget                    .apply_keymap_update(keymap_config, &runtime_keymap);                self.chat_widget                    .return_to_keymap_picker(&context, &action, &runtime_keymap);                self.chat_widget.add_info_message(                    format!("Removed custom shortcut for `{context}.{action}`."),                    /*hint*/ None,                );            }            Err(err) => {                tracing::error!(error = %err, "failed to clear keymap binding");                self.chat_widget                    .add_error_message(format!("Failed to remove shortcut: {err}"));            }        }    }    pub(super) async fn handle_exit_mode(        &mut self,        app_server: &mut AppServerSession,        mode: ExitMode,    ) -> AppRunControl {        match mode {            ExitMode::ShutdownFirst => {                // Mark the thread we are explicitly shutting down for exit so                // its shutdown completion does not trigger agent failover.                self.pending_shutdown_exit_thread_id =                    self.active_thread_id.or(self.chat_widget.thread_id());                if self.pending_shutdown_exit_thread_id.is_some() {                    // This is a UI escape-hatch budget, not a protocol                    // deadline. A healthy local thread/unsubscribe round trip                    // should finish comfortably inside two seconds, while a                    // longer wait makes Ctrl+C feel broken when the app-server                    // is already wedged.                    if tokio::time::timeout(                        SHUTDOWN_FIRST_EXIT_TIMEOUT,                        self.shutdown_current_thread(app_server),                    )                    .await                    .is_err()                    {                        tracing::warn!("timed out waiting for app-server thread shutdown");                    }                }                self.pending_shutdown_exit_thread_id = None;                AppRunControl::Exit(ExitReason::UserRequested)            }            ExitMode::Immediate => {                self.pending_shutdown_exit_thread_id = None;                AppRunControl::Exit(ExitReason::UserRequested)            }        }    }    pub(super) async fn archive_current_thread(        &mut self,        app_server: &mut AppServerSession,    ) -> AppRunControl {        let Some(thread_id) = self.active_thread_id.or(self.chat_widget.thread_id()) else {            self.chat_widget                .add_error_message("A thread must start before it can be archived.".to_string());            return AppRunControl::Continue;        };        if self.side_threads.contains_key(&thread_id) {            self.chat_widget.add_error_message(                "'/archive' is unavailable in side conversations. Press Ctrl+C to return to the main thread first."                    .to_string(),            );            return AppRunControl::Continue;        }        match app_server.thread_archive(thread_id).await {            Ok(()) => AppRunControl::Exit(ExitReason::UserRequested),            Err(err) => {                self.chat_widget                    .add_error_message(format!("Failed to archive current thread: {err}"));                AppRunControl::Continue            }        }    }    pub(super) async fn delete_current_thread(        &mut self,        app_server: &mut AppServerSession,    ) -> AppRunControl {        let Some(thread_id) = self.active_thread_id.or(self.chat_widget.thread_id()) else {            self.chat_widget                .add_error_message("A thread must start before it can be deleted.".to_string());            return AppRunControl::Continue;        };        if self.side_threads.contains_key(&thread_id) {            self.chat_widget.add_error_message(                "'/delete' is unavailable in side conversations. Press Ctrl+C to return to the main thread first."                    .to_string(),            );            return AppRunControl::Continue;        }        match app_server.thread_delete(thread_id).await {            Ok(()) => AppRunControl::Exit(ExitReason::UserRequested),            Err(err) => {                self.chat_widget                    .add_error_message(format!("Failed to delete current thread: {err}"));                AppRunControl::Continue            }        }    }}