From ca773db697c4afd6871310f663d6dcd5afe638fc Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Fri, 17 Apr 2026 19:56:20 -0300 Subject: [PATCH] fix(tui): resolve keymap rebase fallout Remove the duplicate slash-command dispatcher left by the rebase and wire the vim/keymap commands into the extracted dispatcher. Update affected keymap test callsites and footer snapshot so the rebased TUI builds against the current list keymap API. --- .../tui/src/bottom_pane/approval_overlay.rs | 9 +- .../src/bottom_pane/list_selection_view.rs | 6 + codex-rs/tui/src/bottom_pane/mod.rs | 2 +- codex-rs/tui/src/chatwidget.rs | 466 ------------------ codex-rs/tui/src/chatwidget/slash_dispatch.rs | 6 + .../tui/src/chatwidget/tests/plan_mode.rs | 2 +- 6 files changed, 22 insertions(+), 469 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 2ae87fcaca..f5d9855eb4 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -1143,7 +1143,14 @@ mod tests { fn resolved_request_dismisses_overlay_without_emitting_abort() { let (tx, mut rx) = unbounded_channel::(); let tx = AppEventSender::new(tx); - let mut view = ApprovalOverlay::new(make_exec_request(), tx, Features::with_defaults()); + let keymap = crate::keymap::RuntimeKeymap::defaults(); + let mut view = ApprovalOverlay::new( + make_exec_request(), + tx, + Features::with_defaults(), + keymap.approval, + keymap.list, + ); assert!( view.dismiss_app_server_request(&ResolvedAppServerRequest::ExecApproval { diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index 7b3dfd3d03..92f60719dd 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -1561,6 +1561,7 @@ mod tests { ..Default::default() }, tx, + crate::keymap::RuntimeKeymap::defaults().list, ); view.set_search_query("beta".to_string()); @@ -1623,6 +1624,7 @@ mod tests { ..Default::default() }, tx, + crate::keymap::RuntimeKeymap::defaults().list, ); assert_eq!(view.active_tab_id(), Some("beta")); @@ -1693,6 +1695,7 @@ mod tests { ..Default::default() }, tx, + crate::keymap::RuntimeKeymap::defaults().list, ); let (wrapped_tx_raw, _wrapped_rx) = unbounded_channel::(); let wrapped_tx = AppEventSender::new(wrapped_tx_raw); @@ -1711,6 +1714,7 @@ mod tests { ..Default::default() }, wrapped_tx, + crate::keymap::RuntimeKeymap::defaults().list, ); let rendered = render_lines_with_width(&single_line_view, /*width*/ 36); @@ -1766,6 +1770,7 @@ mod tests { ..Default::default() }, auto_tx, + crate::keymap::RuntimeKeymap::defaults().list, ); let (widened_tx_raw, _widened_rx) = unbounded_channel::(); let widened_tx = AppEventSender::new(widened_tx_raw); @@ -1778,6 +1783,7 @@ mod tests { ..Default::default() }, widened_tx, + crate::keymap::RuntimeKeymap::defaults().list, ); let auto_rendered = render_lines_with_width(&auto_view, /*width*/ 48); diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 463810c206..d67cca4b30 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -92,9 +92,9 @@ mod skills_toggle_view; pub(crate) mod slash_commands; pub(crate) use footer::CollaborationModeIndicator; pub(crate) use list_selection_view::ColumnWidthMode; -pub(crate) use list_selection_view::SelectionRowDisplay; #[cfg(test)] pub(crate) use list_selection_view::ListSelectionView; +pub(crate) use list_selection_view::SelectionRowDisplay; pub(crate) use list_selection_view::SelectionToggle; pub(crate) use list_selection_view::SelectionViewParams; pub(crate) use list_selection_view::SideContentWidth; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index b6085c9986..847a170870 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -5657,472 +5657,6 @@ impl ChatWidget { self.last_agent_markdown.as_deref() } - fn dispatch_command(&mut self, cmd: SlashCommand) { - if !cmd.available_during_task() && self.bottom_pane.is_task_running() { - let message = format!( - "'/{}' is disabled while a task is in progress.", - cmd.command() - ); - self.add_to_history(history_cell::new_error_event(message)); - self.bottom_pane.drain_pending_submission_state(); - self.request_redraw(); - return; - } - match cmd { - SlashCommand::Feedback => { - if !self.config.feedback_enabled { - let params = crate::bottom_pane::feedback_disabled_params(); - self.bottom_pane.show_selection_view(params); - self.request_redraw(); - return; - } - // Step 1: pick a category (UI built in feedback_view) - let params = - crate::bottom_pane::feedback_selection_params(self.app_event_tx.clone()); - self.bottom_pane.show_selection_view(params); - self.request_redraw(); - } - SlashCommand::New => { - self.app_event_tx.send(AppEvent::NewSession); - } - SlashCommand::Clear => { - self.app_event_tx.send(AppEvent::ClearUi); - } - SlashCommand::Resume => { - self.app_event_tx.send(AppEvent::OpenResumePicker); - } - SlashCommand::Fork => { - self.app_event_tx.send(AppEvent::ForkCurrentSession); - } - SlashCommand::Init => { - let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME); - if init_target.exists() { - let message = format!( - "{DEFAULT_PROJECT_DOC_FILENAME} already exists here. Skipping /init to avoid overwriting it." - ); - self.add_info_message(message, /*hint*/ None); - return; - } - const INIT_PROMPT: &str = include_str!("../prompt_for_init_command.md"); - self.submit_user_message(INIT_PROMPT.to_string().into()); - } - SlashCommand::Compact => { - self.clear_token_usage(); - if !self.bottom_pane.is_task_running() { - self.bottom_pane.set_task_running(/*running*/ true); - } - self.app_event_tx.compact(); - } - SlashCommand::Review => { - self.open_review_popup(); - } - SlashCommand::Rename => { - self.session_telemetry - .counter("codex.thread.rename", /*inc*/ 1, &[]); - self.show_rename_prompt(); - } - SlashCommand::Model => { - self.open_model_popup(); - } - SlashCommand::Fast => { - let next_tier = if matches!(self.config.service_tier, Some(ServiceTier::Fast)) { - None - } else { - Some(ServiceTier::Fast) - }; - self.set_service_tier_selection(next_tier); - } - SlashCommand::Realtime => { - if !self.realtime_conversation_enabled() { - return; - } - if self.realtime_conversation.is_live() { - self.stop_realtime_conversation_from_ui(); - } else { - self.start_realtime_conversation(); - } - } - SlashCommand::Settings => { - if !self.realtime_audio_device_selection_enabled() { - return; - } - self.open_realtime_audio_popup(); - } - SlashCommand::Personality => { - self.open_personality_popup(); - } - SlashCommand::Plan => { - if !self.collaboration_modes_enabled() { - self.add_info_message( - "Collaboration modes are disabled.".to_string(), - Some("Enable collaboration modes to use /plan.".to_string()), - ); - return; - } - if let Some(mask) = collaboration_modes::plan_mask(self.model_catalog.as_ref()) { - self.set_collaboration_mask(mask); - } else { - self.add_info_message( - "Plan mode unavailable right now.".to_string(), - /*hint*/ None, - ); - } - } - SlashCommand::Collab => { - if !self.collaboration_modes_enabled() { - self.add_info_message( - "Collaboration modes are disabled.".to_string(), - Some("Enable collaboration modes to use /collab.".to_string()), - ); - return; - } - self.open_collaboration_modes_popup(); - } - SlashCommand::Agent | SlashCommand::MultiAgents => { - self.app_event_tx.send(AppEvent::OpenAgentPicker); - } - SlashCommand::Approvals => { - self.open_permissions_popup(); - } - SlashCommand::Permissions => { - self.open_permissions_popup(); - } - SlashCommand::Vim => { - self.toggle_vim_mode_and_notify(); - } - SlashCommand::Keymap => { - self.open_keymap_picker(); - } - SlashCommand::ElevateSandbox => { - #[cfg(target_os = "windows")] - { - let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); - let windows_degraded_sandbox_enabled = - matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); - if !windows_degraded_sandbox_enabled - || !crate::legacy_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - { - // This command should not be visible/recognized outside degraded mode, - // but guard anyway in case something dispatches it directly. - return; - } - - let Some(preset) = builtin_approval_presets() - .into_iter() - .find(|preset| preset.id == "auto") - else { - // Avoid panicking in interactive UI; treat this as a recoverable - // internal error. - self.add_error_message( - "Internal error: missing the 'auto' approval preset.".to_string(), - ); - return; - }; - - if let Err(err) = self - .config - .permissions - .approval_policy - .can_set(&preset.approval) - { - self.add_error_message(err.to_string()); - return; - } - - self.session_telemetry.counter( - "codex.windows_sandbox.setup_elevated_sandbox_command", - /*inc*/ 1, - &[], - ); - self.app_event_tx - .send(AppEvent::BeginWindowsSandboxElevatedSetup { preset }); - } - #[cfg(not(target_os = "windows"))] - { - let _ = &self.session_telemetry; - // Not supported; on non-Windows this command should never be reachable. - }; - } - SlashCommand::SandboxReadRoot => { - self.add_error_message( - "Usage: /sandbox-add-read-dir ".to_string(), - ); - } - SlashCommand::Experimental => { - self.open_experimental_popup(); - } - SlashCommand::Quit | SlashCommand::Exit => { - self.request_quit_without_confirmation(); - } - SlashCommand::Logout => { - if let Err(e) = codex_login::logout( - &self.config.codex_home, - self.config.cli_auth_credentials_store_mode, - ) { - tracing::error!("failed to logout: {e}"); - } - self.request_quit_without_confirmation(); - } - // SlashCommand::Undo => { - // self.app_event_tx.send(AppEvent::CodexOp(Op::Undo)); - // } - SlashCommand::Copy => { - self.copy_last_agent_markdown(); - } - SlashCommand::Diff => { - self.add_diff_in_progress(); - let tx = self.app_event_tx.clone(); - tokio::spawn(async move { - let text = match get_git_diff().await { - Ok((is_git_repo, diff_text)) => { - if is_git_repo { - diff_text - } else { - "`/diff` — _not inside a git repository_".to_string() - } - } - Err(e) => format!("Failed to compute diff: {e}"), - }; - tx.send(AppEvent::DiffResult(text)); - }); - } - SlashCommand::Mention => { - self.insert_str("@"); - } - SlashCommand::Skills => { - self.open_skills_menu(); - } - SlashCommand::Status => { - if self.should_prefetch_rate_limits() { - let request_id = self.next_rate_limit_refresh_request_id(); - self.add_status_output(/*refreshing_rate_limits*/ true, Some(request_id)); - self.app_event_tx.send(AppEvent::RefreshRateLimits { - origin: RateLimitRefreshOrigin::StatusCommand { request_id }, - }); - } else { - self.add_status_output( - /*refreshing_rate_limits*/ false, /*request_id*/ None, - ); - } - } - SlashCommand::DebugConfig => { - self.add_debug_config_output(); - } - SlashCommand::Title => { - self.open_terminal_title_setup(); - } - SlashCommand::Statusline => { - self.open_status_line_setup(); - } - SlashCommand::Theme => { - self.open_theme_picker(); - } - SlashCommand::Ps => { - self.add_ps_output(); - } - SlashCommand::Stop => { - self.clean_background_terminals(); - } - SlashCommand::MemoryDrop => { - self.add_app_server_stub_message("Memory maintenance"); - } - SlashCommand::MemoryUpdate => { - self.add_app_server_stub_message("Memory maintenance"); - } - SlashCommand::Mcp => { - self.add_mcp_output(); - } - SlashCommand::Apps => { - self.add_connectors_output(); - } - SlashCommand::Plugins => { - self.add_plugins_output(); - } - SlashCommand::Rollout => { - if let Some(path) = self.rollout_path() { - self.add_info_message( - format!("Current rollout path: {}", path.display()), - /*hint*/ None, - ); - } else { - self.add_info_message( - "Rollout path is not available yet.".to_string(), - /*hint*/ None, - ); - } - } - SlashCommand::TestApproval => { - use std::collections::HashMap; - - use codex_protocol::protocol::ApplyPatchApprovalRequestEvent; - use codex_protocol::protocol::FileChange; - - self.on_apply_patch_approval_request( - "1".to_string(), - ApplyPatchApprovalRequestEvent { - call_id: "1".to_string(), - turn_id: "turn-1".to_string(), - changes: HashMap::from([ - ( - PathBuf::from("/tmp/test.txt"), - FileChange::Add { - content: "test".to_string(), - }, - ), - ( - PathBuf::from("/tmp/test2.txt"), - FileChange::Update { - unified_diff: "+test\n-test2".to_string(), - move_path: None, - }, - ), - ]), - reason: None, - grant_root: Some(PathBuf::from("/tmp")), - }, - ); - } - } - } - - fn dispatch_command_with_args( - &mut self, - cmd: SlashCommand, - args: String, - _text_elements: Vec, - ) { - if !cmd.supports_inline_args() { - self.dispatch_command(cmd); - return; - } - if !cmd.available_during_task() && self.bottom_pane.is_task_running() { - let message = format!( - "'/{}' is disabled while a task is in progress.", - cmd.command() - ); - self.add_to_history(history_cell::new_error_event(message)); - self.request_redraw(); - return; - } - - let trimmed = args.trim(); - match cmd { - SlashCommand::Fast => { - if trimmed.is_empty() { - self.dispatch_command(cmd); - return; - } - match trimmed.to_ascii_lowercase().as_str() { - "on" => self.set_service_tier_selection(Some(ServiceTier::Fast)), - "off" => self.set_service_tier_selection(/*service_tier*/ None), - "status" => { - let status = if matches!(self.config.service_tier, Some(ServiceTier::Fast)) - { - "on" - } else { - "off" - }; - self.add_info_message( - format!("Fast mode is {status}."), - /*hint*/ None, - ); - } - _ => { - self.add_error_message("Usage: /fast [on|off|status]".to_string()); - } - } - } - SlashCommand::Rename if !trimmed.is_empty() => { - self.session_telemetry - .counter("codex.thread.rename", /*inc*/ 1, &[]); - let Some((prepared_args, _prepared_elements)) = self - .bottom_pane - .prepare_inline_args_submission(/*record_history*/ false) - else { - return; - }; - let Some(name) = crate::legacy_core::util::normalize_thread_name(&prepared_args) - else { - self.add_error_message("Thread name cannot be empty.".to_string()); - return; - }; - self.app_event_tx.set_thread_name(name); - self.bottom_pane.drain_pending_submission_state(); - } - SlashCommand::Plan if !trimmed.is_empty() => { - self.dispatch_command(cmd); - if self.active_mode_kind() != ModeKind::Plan { - return; - } - let Some((prepared_args, prepared_elements)) = self - .bottom_pane - .prepare_inline_args_submission(/*record_history*/ true) - else { - return; - }; - let local_images = self - .bottom_pane - .take_recent_submission_images_with_placeholders(); - let remote_image_urls = self.take_remote_image_urls(); - let user_message = UserMessage { - text: prepared_args, - local_images, - remote_image_urls, - text_elements: prepared_elements, - mention_bindings: self.bottom_pane.take_recent_submission_mention_bindings(), - }; - if self.is_session_configured() { - self.reasoning_buffer.clear(); - self.full_reasoning_buffer.clear(); - self.set_status_header(String::from("Working")); - self.submit_user_message(user_message); - } else { - self.queue_user_message(user_message); - } - } - SlashCommand::Review if !trimmed.is_empty() => { - let Some((prepared_args, _prepared_elements)) = self - .bottom_pane - .prepare_inline_args_submission(/*record_history*/ false) - else { - return; - }; - self.submit_op(AppCommand::review(ReviewRequest { - target: ReviewTarget::Custom { - instructions: prepared_args, - }, - user_facing_hint: None, - })); - self.bottom_pane.drain_pending_submission_state(); - } - SlashCommand::Resume if !trimmed.is_empty() => { - let Some((prepared_args, _prepared_elements)) = self - .bottom_pane - .prepare_inline_args_submission(/*record_history*/ false) - else { - return; - }; - self.app_event_tx - .send(AppEvent::ResumeSessionByIdOrName(prepared_args)); - self.bottom_pane.drain_pending_submission_state(); - } - SlashCommand::SandboxReadRoot if !trimmed.is_empty() => { - let Some((prepared_args, _prepared_elements)) = self - .bottom_pane - .prepare_inline_args_submission(/*record_history*/ false) - else { - return; - }; - self.app_event_tx - .send(AppEvent::BeginWindowsSandboxGrantReadRoot { - path: prepared_args, - }); - self.bottom_pane.drain_pending_submission_state(); - } - _ => self.dispatch_command(cmd), - } - } - fn show_rename_prompt(&mut self) { if !self.ensure_thread_rename_allowed() { return; diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index febd8aef4f..490d9f5d1e 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -223,6 +223,12 @@ impl ChatWidget { SlashCommand::Permissions => { self.open_permissions_popup(); } + SlashCommand::Vim => { + self.toggle_vim_mode_and_notify(); + } + SlashCommand::Keymap => { + self.open_keymap_picker(); + } SlashCommand::ElevateSandbox => { #[cfg(target_os = "windows")] { diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index d7d74ff6ac..b6b957943e 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -1350,7 +1350,7 @@ async fn collaboration_modes_defaults_to_code_on_startup() { assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Default); assert_eq!( chat.current_model(), - codex_core::test_support::get_model_offline(chat.config.model.as_deref()) + crate::legacy_core::test_support::get_model_offline(chat.config.model.as_deref()) ); }