//! 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 } } }}