use super::resize_reflow::trailing_run_start;
use super::session_lifecycle::ThreadAttachPresentation;
use super::*;
use crate::tui_internal::app_server_session::ForkGoalContinuation;
use crate::tui_internal::config_update::format_config_error;
use crate::tui_internal::external_agent_config_migration_flow::ExternalAgentConfigMigrationFlowOutcome;
#[cfg(target_os = "windows")]
use lemurclaw_core::config::types::WindowsSandboxModeToml;
const SHUTDOWN_FIRST_EXIT_TIMEOUT: Duration = Duration::from_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, None,
None,
)
.await;
}
AppEvent::StartupThreadStarted { result } => {
self.handle_startup_thread_started(app_server, result)
.await?;
}
AppEvent::ClearUi => {
self.clear_terminal_ui(tui, false)?;
self.reset_app_ui_state_after_clear();
self.start_fresh_session_with_summary_hint(
tui,
app_server,
Some(ThreadStartSource::Clear),
None,
)
.await;
}
AppEvent::RawOutputModeChanged { enabled } => {
self.apply_raw_output_mode(tui, enabled, false);
}
AppEvent::ClearUiAndSubmitUserMessage { text } => {
self.clear_terminal_ui(tui, false)?;
self.reset_app_ui_state_after_clear();
self.start_fresh_session_with_summary_hint(
tui,
app_server,
Some(ThreadStartSource::Clear),
crate::tui_internal::chatwidget::create_initial_user_message(
Some(text),
Vec::new(),
Vec::new(),
),
)
.await;
}
AppEvent::OpenResumePicker => {
let picker_app_server = match crate::tui_internal::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::tui_internal::resume_picker::run_resume_picker_from_existing_session_with_app_server(
tui,
&self.config,
false,
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(_) => {}
}
tui.frame_requester().schedule_frame();
}
AppEvent::OpenExternalAgentConfigMigration => {
match crate::tui_internal::external_agent_config_migration_flow::handle_external_agent_config_migration_prompt(
tui,
app_server,
&self.config,
)
.await
{
Ok(ExternalAgentConfigMigrationFlowOutcome::Started(lines)) => {
self.chat_widget.add_plain_history_lines(lines);
}
Ok(ExternalAgentConfigMigrationFlowOutcome::NoItems) => {
self.chat_widget.add_info_message(
crate::tui_internal::external_agent_config_migration_flow::EXTERNAL_AGENT_CONFIG_MIGRATION_NO_ITEMS_MESSAGE
.to_string(),
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::tui_internal::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",
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;
let mut fork_config = self.config.clone();
fork_config.model = Some(self.chat_widget.current_model().to_string());
fork_config.model_reasoning_effort =
self.chat_widget.current_reasoning_effort();
match app_server.fork_thread(fork_config, thread_id).await {
Ok(forked) => {
self.shutdown_current_thread(app_server).await;
match self
.replace_chat_widget_with_app_server_thread(
tui,
forked,
ThreadAttachPresentation::SessionLineage,
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::ForkSessionForPromptEdit {
thread_id,
nth_user_message,
mut prompt,
} => {
if self.chat_widget.thread_id() != Some(thread_id) {
return Ok(AppRunControl::Continue);
}
self.session_telemetry.counter(
"codex.thread.fork",
1,
&[("source", "transcript")],
);
self.refresh_in_memory_config_from_disk_best_effort("forking the thread")
.await;
let config = self.fresh_session_config();
let started = match app_server
.thread_read(thread_id, true)
.await
{
Ok(thread) => match crate::tui_internal::app_backtrack::backtrack_fork_before_turn_id(
&thread.turns,
nth_user_message,
&mut prompt,
) {
Ok(Some(before_turn_id)) => {
app_server
.fork_thread_at(
config.clone(),
thread_id,
None,
Some(before_turn_id),
ForkGoalContinuation::StartIfIdle,
)
.await
}
Ok(None) => {
app_server
.start_thread_with_session_start_source(
&config, None,
)
.await
}
Err(err) => Err(err),
},
Err(err) => Err(err),
};
match started {
Ok(forked) => {
self.shutdown_current_thread(app_server).await;
match self
.replace_chat_widget_with_app_server_thread(
tui,
forked,
ThreadAttachPresentation::PromptEdit,
None,
)
.await
{
Ok(()) => self.chat_widget.restore_user_message_to_composer(prompt),
Err(err) => {
self.restore_backtrack_prompt_after_branch_error(prompt, err);
}
}
}
Err(err) => {
self.restore_backtrack_prompt_after_branch_error(prompt, err);
}
}
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,
inline_visualization_context,
scrollback_reflow,
deferred_history_cell,
} => {
self.handle_consolidate_agent_message(
tui,
source,
cwd,
inline_visualization_context,
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::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) => {
if matches!(&op, AppCommand::UserTurn { .. }) {
self.handle_draw_pre_render(tui)?;
if self.transcript_reflow.has_pending_reflow() {
self.transcript_reflow.schedule_immediate();
self.maybe_run_resize_reflow(tui)?;
}
self.chat_widget.pre_draw_tick();
self.render_chat_widget_frame(tui)?;
}
self.chat_widget.prepare_local_op_submission(&op);
self.submit_active_thread_op(app_server, op).await?;
}
AppEvent::RetrySafetyBufferedTurn {
thread_id,
turn_id,
model,
turn,
prompt,
} => {
self.retry_safety_buffered_turn(
tui,
app_server,
super::safety_buffering::SafetyBufferedRetry {
thread_id,
turn_id,
model,
turn,
prompt,
},
)
.await;
}
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::LookupMessageHistoryBatch {
thread_id,
cursor,
log_id,
} => {
self.lookup_message_history_batch(thread_id, cursor, 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) => {
self.chat_widget.on_diff_complete();
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::tui_internal::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( 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::OpenPluginsList { cwd, response } => {
self.chat_widget.open_plugins_list(cwd, response);
}
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::RefreshStatusLineWorkspaceHeadline { request_id } => {
self.refresh_status_line_workspace_headline(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,
hard_stop_generation,
result,
} => match result {
Ok(response) => {
let rate_limit_reset_credits = response.rate_limit_reset_credits.clone();
let snapshots = if hard_stop_generation == self.rate_limit_hard_stop_generation
{
app_server_rate_limit_snapshots(response)
} else {
Vec::new()
};
match origin {
RateLimitRefreshOrigin::StartupPrefetch {
reset_hint_request_id,
} => {
if self.chat_widget.finish_rate_limit_reset_hint_refresh(
reset_hint_request_id,
snapshots,
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,
snapshots,
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, snapshots);
}
RateLimitRefreshOrigin::UsageMenu { request_id } => {
self.chat_widget.finish_usage_menu_rate_limit_refresh(
request_id,
snapshots,
rate_limit_reset_credits.ok_or_else(|| {
"account/rateLimits/read response did not include rateLimitResetCredits"
.to_string()
}),
);
}
RateLimitRefreshOrigin::ResetPicker { request_id } => {
self.chat_widget.finish_rate_limit_reset_credits_refresh(
request_id,
snapshots,
rate_limit_reset_credits.ok_or_else(|| {
"account/rateLimits/read response did not include rateLimitResetCredits"
.to_string()
}),
);
}
}
}
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,
Vec::new(),
Err(err),
);
}
RateLimitRefreshOrigin::ResetConsume { request_id } => {
self.chat_widget.finish_post_consume_reset_credits_refresh(
request_id,
Vec::new(),
Err(err),
);
}
RateLimitRefreshOrigin::StatusCommand { request_id } => {
self.chat_widget
.finish_status_rate_limit_refresh(request_id, Vec::new());
}
RateLimitRefreshOrigin::UsageMenu { request_id } => {
self.chat_widget.finish_usage_menu_rate_limit_refresh(
request_id,
Vec::new(),
Err(err),
);
}
RateLimitRefreshOrigin::ResetPicker { request_id } => {
self.chat_widget.finish_rate_limit_reset_credits_refresh(
request_id,
Vec::new(),
Err(err),
);
}
}
}
},
AppEvent::OpenTokenActivity => {
self.chat_widget
.add_token_activity_output(crate::tui_internal::chatwidget::TokenActivityView::Daily);
}
AppEvent::OpenRateLimitResetCredits => {
let request_id = self.chat_widget.show_rate_limit_reset_loading_popup();
self.refresh_rate_limits(
app_server,
RateLimitRefreshOrigin::ResetPicker { request_id },
);
}
AppEvent::OpenRateLimitResetConfirmation {
picker_request_id,
confirmation_gate,
credit_id,
reset_title,
reset_detail,
reset_description,
} => {
self.chat_widget.show_rate_limit_reset_confirmation(
picker_request_id,
confirmation_gate,
credit_id,
reset_title,
reset_detail,
reset_description,
);
}
AppEvent::ConsumeRateLimitResetCredit {
idempotency_key,
credit_id,
} => {
if let Some(request_id) = self
.chat_widget
.start_rate_limit_reset_consumption(&idempotency_key)
{
self.consume_rate_limit_reset_credit(
app_server,
request_id,
idempotency_key,
credit_id,
);
}
}
AppEvent::RateLimitResetCreditConsumed {
request_id,
idempotency_key,
credit_id,
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,
credit_id,
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)
{
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::SettingsSelectionClosed => {
self.app_event_tx.send(AppEvent::SettingsSelectionSettled);
}
AppEvent::SettingsSelectionSettled => {
if self.chat_widget.no_modal_or_popup_active() {
self.chat_widget
.set_queue_autosend_suppressed( false);
self.chat_widget.maybe_send_next_queued_input();
}
}
AppEvent::OpenReasoningPopup { model } => {
self.chat_widget.open_reasoning_popup(model);
}
AppEvent::OpenAdvancedReasoningPopup { model } => {
self.chat_widget.open_advanced_reasoning_popup(model);
}
AppEvent::ApplyAdvancedReasoning { model, effort } => {
let default_effort =
self.on_apply_advanced_reasoning(model.as_str(), effort.clone());
if let Some(mut params) =
self.active_thread_model_setting_update_params(model.clone())
{
params.effort = Some(effort.clone());
self.send_thread_settings_update(app_server, params).await;
}
self.sync_active_thread_service_tier_to_cached_session()
.await;
if let Some(default_effort) = default_effort.as_ref()
&& let Err(err) = crate::tui_internal::config_update::write_config_batch(
app_server.request_handle(),
crate::tui_internal::config_update::build_model_selection_edits(
model.as_str(),
Some(default_effort),
),
)
.await
{
let error = format_config_error(&err);
tracing::error!(error = %error, "failed to persist conversation model");
self.chat_widget
.add_error_message(format!("Failed to save default model: {error}"));
} else {
self.chat_widget.add_info_message(
format!("Model changed to {model} {effort} for this conversation"),
None,
);
}
}
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",
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(
lemurclaw_core::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(),
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 crate::tui_internal::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::tui_internal::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",
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::tui_internal::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::tui_internal::windows_sandbox::elevated_setup_failure_metric_name(
&err,
),
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(
lemurclaw_core::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(),
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) =
lemurclaw_core::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",
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} ..."),
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::tui_internal::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()),
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(),
None,
);
return Ok(AppRunControl::Continue);
}
let edits =
crate::tui_internal::config_update::build_windows_sandbox_mode_edits(elevated_enabled);
match crate::tui_internal::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( false);
self.config
.set_windows_elevated_sandbox_enabled( true);
} else {
self.config.set_windows_sandbox_enabled( true);
self.config
.set_windows_elevated_sandbox_enabled( false);
}
self.chat_widget.set_windows_sandbox_mode(
self.config.permissions.windows_sandbox_mode,
);
let windows_sandbox_level =
crate::tui_internal::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(
None,
None,
None,
None,
None,
#[cfg(target_os = "windows")]
Some(windows_sandbox_level),
None,
None,
None,
None,
None,
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(
None,
None,
None,
None,
None,
#[cfg(target_os = "windows")]
Some(windows_sandbox_level),
None,
None,
None,
None,
None,
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(
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),
None,
None,
None,
None,
None,
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::tui_internal::config_update::write_config_batch(
app_server.request_handle(),
crate::tui_internal::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, 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::tui_internal::config_update::write_config_batch(
app_server.request_handle(),
vec![crate::tui_internal::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, 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::tui_internal::config_update::build_service_tier_selection_edits(
service_tier.as_deref(),
);
match crate::tui_internal::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, 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();
#[cfg(target_os = "windows")]
{
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::tui_internal::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::tui_internal::config_update::write_config_batch(
app_server.request_handle(),
vec![crate::tui_internal::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::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.on_update_plan_mode_reasoning_effort(effort);
self.sync_active_thread_plan_mode_reasoning_setting(app_server)
.await;
}
AppEvent::PersistWorldWritableWarningAcknowledged => {
if let Err(err) = ConfigEditsBuilder::for_config(&self.config)
.set_hide_world_writable_warning( 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( 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::tui_internal::config_update::replace_config_value(
key_path,
serde_json::json!(effort.to_string()),
)
} else {
crate::tui_internal::config_update::clear_config_value(key_path)
};
if let Err(err) = crate::tui_internal::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::tui_internal::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::tui_internal::config_update::clear_config_value(
crate::tui_internal::config_update::app_scoped_key_path(&id, "enabled"),
),
crate::tui_internal::config_update::clear_config_value(
crate::tui_internal::config_update::app_scoped_key_path(&id, "disabled_reason"),
),
]
} else {
vec![
crate::tui_internal::config_update::replace_config_value(
crate::tui_internal::config_update::app_scoped_key_path(&id, "enabled"),
serde_json::json!(false),
),
crate::tui_internal::config_update::replace_config_value(
crate::tui_internal::config_update::app_scoped_key_path(&id, "disabled_reason"),
serde_json::json!("user"),
),
]
};
match crate::tui_internal::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(request) => {
let _ = tui.enter_alt_screen();
let diff_summary = DiffSummary::new(request.changes, request.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(request) => {
let _ = tui.enter_alt_screen();
let full_cmd = strip_bash_lc_and_escape(&request.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(request) => {
let _ = tui.enter_alt_screen();
let mut lines = Vec::new();
if let Some(environment_id) = request.environment_id {
lines.push(Line::from(vec![
"Environment: ".into(),
environment_id.bold(),
]));
lines.push(Line::from(""));
}
if let Some(reason) = request.reason {
lines.push(Line::from(vec!["Reason: ".into(), reason.italic()]));
lines.push(Line::from(""));
}
if let Some(rule_line) =
crate::tui_internal::bottom_pane::format_requested_permissions_rule(&request.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(request) => {
let _ = tui.enter_alt_screen();
let paragraph = Paragraph::new(vec![
Line::from(vec!["Server: ".into(), request.server_name.bold()]),
Line::from(""),
Line::from(request.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::tui_internal::legacy_core::config::edit::status_line_items_edit(&ids);
let colors_edit =
crate::tui_internal::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::StatusLineWorkspaceHeadlineUpdated { request_id, result } => {
if self
.chat_widget
.set_status_line_workspace_headline(request_id, result)
{
tui.frame_requester().schedule_frame();
}
}
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::tui_internal::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::tui_internal::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(()) => {
if let Some(theme) = crate::tui_internal::render::highlight::resolve_theme_by_name(
&name,
Some(&self.config.codex_home),
) {
crate::tui_internal::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::tui_internal::app_event::KeymapEditIntent,
) {
let outcome = match crate::tui_internal::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::tui_internal::keymap_setup::KeymapEditOutcome::Updated {
keymap_config,
bindings,
message,
} => (*keymap_config, bindings, message),
crate::tui_internal::keymap_setup::KeymapEditOutcome::Unchanged { message } => {
self.chat_widget.add_info_message(message, None);
return;
}
};
let runtime_keymap = match RuntimeKeymap::from_config(&keymap_config) {
Ok(runtime_keymap) => runtime_keymap,
Err(err) => {
let params = crate::tui_internal::keymap_setup::build_keymap_conflict_params(
context, action, key, intent, err,
);
self.chat_widget.show_selection_view(params);
return;
}
};
let edit =
crate::tui_internal::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, 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::tui_internal::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::tui_internal::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}`."),
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 => {
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() {
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
}
}
}
}