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.
This commit is contained in:
Felipe Coury
2026-04-23 10:35:34 -03:00
parent d6a09dddcd
commit ca773db697
6 changed files with 22 additions and 469 deletions
@@ -1143,7 +1143,14 @@ mod tests {
fn resolved_request_dismisses_overlay_without_emitting_abort() {
let (tx, mut rx) = unbounded_channel::<AppEvent>();
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 {
@@ -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::<AppEvent>();
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::<AppEvent>();
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);
+1 -1
View File
@@ -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;
-466
View File
@@ -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 <absolute-directory-path>".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<TextElement>,
) {
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;
@@ -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")]
{
@@ -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())
);
}