diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index 4a6a21f5a5..87dafc9351 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -449,6 +449,10 @@ impl App { chat_widget.last_terminal_title = previous_terminal_title; } chat_widget.remote_connection = self.chat_widget.remote_connection.clone(); + chat_widget.set_agents_navigation_enabled(matches!( + self.app_server_target, + AppServerTarget::LocalDaemon { .. } + )); chat_widget.inherit_backend_banner_state(&mut self.chat_widget); for (thread_id, entry) in self.agent_navigation.ordered_threads() { chat_widget.set_collab_agent_metadata( diff --git a/codex-rs/tui/src/app/startup.rs b/codex-rs/tui/src/app/startup.rs index 0316a0dffd..453ed90bd0 100644 --- a/codex-rs/tui/src/app/startup.rs +++ b/codex-rs/tui/src/app/startup.rs @@ -458,6 +458,10 @@ impl App { }; chat_widget.note_rendered_width(tui.terminal.last_known_screen_size.width); chat_widget.remote_connection = remote_connection; + chat_widget.set_agents_navigation_enabled(matches!( + app_server_target, + AppServerTarget::LocalDaemon { .. } + )); let thread_and_widget_ms = thread_and_widget_started_at.elapsed().as_millis(); chat_widget .maybe_prompt_windows_sandbox_enable(should_prompt_windows_sandbox_nux_at_startup); diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index 0f6d031a3a..a95b17749b 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -2,6 +2,8 @@ #[path = "tests/advanced_reasoning_tests.rs"] mod advanced_reasoning_tests; +#[path = "tests/agents_navigation_tests.rs"] +mod agents_navigation_tests; #[path = "tests/backend_banner_fallback_tests.rs"] mod backend_banner_fallback_tests; #[path = "tests/backend_banner_recovery_tests.rs"] diff --git a/codex-rs/tui/src/app/tests/agents_navigation_tests.rs b/codex-rs/tui/src/app/tests/agents_navigation_tests.rs new file mode 100644 index 0000000000..2b82a80a91 --- /dev/null +++ b/codex-rs/tui/src/app/tests/agents_navigation_tests.rs @@ -0,0 +1,46 @@ +use super::*; + +#[tokio::test] +async fn agents_navigation_requires_local_daemon() -> Result<()> { + let (mut app, mut events, _op_rx) = make_test_app_with_channels().await; + let mut tui = crate::tui::test_support::make_test_tui()?; + let mut app_server = start_config_write_test_app_server(&app).await?; + let endpoint = crate::RemoteAppServerEndpoint::UnixSocket { + socket_path: AbsolutePathBuf::relative_to_current_dir("codex.sock")?, + }; + for target in [ + AppServerTarget::Embedded, + AppServerTarget::Remote { + endpoint: endpoint.clone(), + }, + AppServerTarget::LocalDaemon { endpoint }, + ] { + let enabled = matches!(target, AppServerTarget::LocalDaemon { .. }); + app.app_server_target = target; + let init = app.chatwidget_init_for_forked_or_resumed_thread( + &mut tui, + app.config.clone(), + /*initial_user_message*/ None, + ); + app.replace_chat_widget(ChatWidget::new_with_app_event(init)); + while events.try_recv().is_ok() {} + app.handle_tui_event( + &mut tui, + &mut app_server, + TuiEvent::Key(KeyCode::Left.into()), + ) + .await?; + if enabled { + let event = events.try_recv()?; + assert_matches!(event, AppEvent::OpenAgentsOverview); + app.handle_event(&mut tui, &mut app_server, event).await?; + assert!(!app.chat_widget.no_modal_or_popup_active()); + } else { + assert!(events.try_recv().is_err()); + assert!(app.chat_widget.no_modal_or_popup_active()); + assert!(!render_bottom_popup(&app.chat_widget, /*width*/ 96).contains("for agents")); + } + } + app_server.shutdown().await?; + Ok(()) +} diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index ebecf189e4..3511283f2c 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -17,6 +17,8 @@ //! //! # Key Event Routing //! +//! Plain Left opens agents when the local-daemon composer is empty and available for input. +//! Explicit editor remaps take precedence. //! Most key handling goes through [`ChatComposer::handle_key_event`], which dispatches to a //! popup-specific handler if a popup is visible and otherwise to //! [`ChatComposer::handle_key_event_without_popup`]. After every handled key, we call @@ -290,6 +292,7 @@ use codex_protocol::user_input::ByteRange; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use codex_protocol::user_input::TextElement; +mod agents_navigation; mod attachment_state; mod completion_target; mod draft_state; @@ -494,6 +497,7 @@ pub(crate) struct ChatComposer { popups: PopupState, app_event_tx: AppEventSender, history: ChatComposerHistory, + agents_navigation_enabled: bool, footer: FooterState, has_focus: bool, frame_requester: Option, @@ -634,6 +638,7 @@ impl ChatComposer { popups: PopupState::default(), app_event_tx, history: ChatComposerHistory::new(), + agents_navigation_enabled: false, footer: FooterState { quit_shortcut_expires_at: None, quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')), @@ -2108,7 +2113,7 @@ impl ChatComposer { /// Handle key events when file search popup is visible. fn handle_key_event_with_file_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) { - if self.handle_shortcut_overlay_key(&key_event) { + if self.handle_empty_prompt_shortcut(&key_event) { return (InputResult::None, true); } if key_event.code == KeyCode::Esc { @@ -2194,7 +2199,7 @@ impl ChatComposer { /// Handle key events when the legacy skill mention popup is visible. fn handle_key_event_with_skill_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) { - if self.handle_shortcut_overlay_key(&key_event) { + if self.handle_empty_prompt_shortcut(&key_event) { return (InputResult::None, true); } self.footer.mode = reset_mode_after_activity(self.footer.mode); @@ -2276,7 +2281,7 @@ impl ChatComposer { &mut self, key_event: KeyEvent, ) -> (InputResult, bool) { - if self.handle_shortcut_overlay_key(&key_event) { + if self.handle_empty_prompt_shortcut(&key_event) { return (InputResult::None, true); } self.footer.mode = reset_mode_after_activity(self.footer.mode); @@ -3461,7 +3466,7 @@ impl ChatComposer { if self.attachments.selected_remote_image_index.is_some() { self.attachments.clear_remote_image_selection(); } - if self.handle_shortcut_overlay_key(&key_event) { + if self.handle_empty_prompt_shortcut(&key_event) { return (InputResult::None, true); } if self.draft.is_bash_mode && key_event.code == KeyCode::Esc { @@ -3808,17 +3813,23 @@ impl ChatComposer { .remove_deleted_local_placeholders(&removed_payloads, &mut self.draft.textarea); } - /// Handle the dedicated shortcut-overlay toggle key(s). + /// Handle empty-prompt agents navigation and the shortcut-overlay toggle. /// /// This only toggles when the composer is empty and no paste burst is in /// progress, so typing/pasting `?` still inserts text instead of opening /// help. The bound key list intentionally supports terminal-variant /// modifier reporting (for example `?` vs `shift-?`). - fn handle_shortcut_overlay_key(&mut self, key_event: &KeyEvent) -> bool { + fn handle_empty_prompt_shortcut(&mut self, key_event: &KeyEvent) -> bool { if key_event.kind != KeyEventKind::Press { return false; } + if key_hint::plain(KeyCode::Left).is_press(*key_event) && self.agents_navigation_available() + { + self.app_event_tx.send(AppEvent::OpenAgentsOverview); + return true; + } + let toggles = self.toggle_shortcuts_keys.is_pressed(*key_event) && self.is_empty() && !self.is_in_paste_burst(); @@ -3862,6 +3873,9 @@ impl ChatComposer { status_line_value: self.footer.status_line_value.clone(), status_line_enabled: self.footer.status_line_enabled, key_hints: FooterKeyHints { + agents: self + .agents_navigation_available() + .then_some(key_hint::plain(KeyCode::Left).into()), toggle_shortcuts: self.footer.toggle_shortcuts_key, queue: self.footer.queue_key, insert_newline: self.footer.insert_newline_key, @@ -4994,6 +5008,10 @@ impl ChatComposer { } } +#[cfg(test)] +#[path = "chat_composer/agents_navigation_tests.rs"] +mod agents_navigation_tests; + #[cfg(test)] #[path = "chat_composer_effort_tests.rs"] mod effort_tests; diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/agents_navigation.rs b/codex-rs/tui/src/bottom_pane/chat_composer/agents_navigation.rs new file mode 100644 index 0000000000..f2fdbf0dd2 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/chat_composer/agents_navigation.rs @@ -0,0 +1,29 @@ +//! Availability of the empty-prompt agents shortcut and its footer hint. +//! +//! Only local-daemon sessions enable this shortcut. Draft contents, transient input surfaces, +//! and explicit editor remaps take precedence over navigation. + +use super::*; + +impl ChatComposer { + pub(crate) fn set_agents_navigation_enabled(&mut self, enabled: bool) { + self.agents_navigation_enabled = enabled; + } + + pub(super) fn agents_navigation_available(&self) -> bool { + let move_left = if self.draft.textarea.is_vim_normal_mode() { + &self.vim_normal_keymap.move_left + } else { + &self.editor_keymap.move_left + }; + self.agents_navigation_enabled + && move_left.is_pressed(KeyCode::Left.into()) + && self.has_focus + && self.draft.input_enabled + && self.slash_commands_enabled() + && self.is_empty() + && !self.is_in_paste_burst() + && !self.popup_active() + && !self.draft.textarea.is_vim_operator_pending() + } +} diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/agents_navigation_tests.rs b/codex-rs/tui/src/bottom_pane/chat_composer/agents_navigation_tests.rs new file mode 100644 index 0000000000..59bc80a59e --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/chat_composer/agents_navigation_tests.rs @@ -0,0 +1,108 @@ +use super::tests::new_test_composer; +use super::tests::snapshot_composer_state_with_width; +use super::*; +use assert_matches::assert_matches; +use pretty_assertions::assert_eq; + +#[test] +fn left_navigates_only_from_an_empty_prompt() { + let (mut composer, mut events) = new_test_composer(); + composer.set_agents_navigation_enabled(/*enabled*/ true); + composer.insert_str("ab"); + composer.handle_key_event(KeyCode::Left.into()); + composer.insert_str("!"); + assert_eq!(composer.current_text(), "a!b"); + assert!(events.try_recv().is_err()); + + composer.clear_for_ctrl_c(); + for kind in [KeyEventKind::Repeat, KeyEventKind::Release] { + composer.handle_key_event(KeyEvent::new_with_kind( + KeyCode::Left, + KeyModifiers::NONE, + kind, + )); + } + assert!(events.try_recv().is_err()); + composer.handle_key_event(KeyCode::Left.into()); + assert_matches!(events.try_recv(), Ok(AppEvent::OpenAgentsOverview)); +} + +#[test] +fn left_respects_attachments_pastes_and_other_input_surfaces() { + let setups: [fn(&mut ChatComposer); 4] = [ + |composer| composer.config = ChatComposerConfig::plain_text(), + |composer| composer.set_remote_image_urls(vec!["https://example.com/image.png".into()]), + |composer| { + composer.handle_key_event(KeyCode::Char('x').into()); + }, + |composer| { + composer.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL)); + }, + ]; + for setup in setups { + let (mut composer, mut events) = new_test_composer(); + composer.set_agents_navigation_enabled(/*enabled*/ true); + setup(&mut composer); + composer.handle_key_event(KeyCode::Left.into()); + assert!( + !std::iter::from_fn(|| events.try_recv().ok()) + .any(|event| matches!(event, AppEvent::OpenAgentsOverview)) + ); + } +} + +#[test] +fn agents_navigation_hint_snapshots() { + for name in [ + "agents_navigation_enabled", + "agents_navigation_help", + "agents_navigation_status", + ] { + snapshot_composer_state_with_width( + name, + /*width*/ 80, + /*enhanced_keys_supported*/ false, + |composer| { + composer.set_agents_navigation_enabled(/*enabled*/ true); + match name { + "agents_navigation_help" => { + composer.handle_key_event(KeyCode::Char('?').into()); + } + "agents_navigation_status" => { + composer.set_status_line_enabled(/*enabled*/ true); + composer.set_status_line(Some(Line::from("model · project"))); + } + _ => {} + } + }, + ); + } +} + +#[test] +fn left_respects_editor_and_vim_history_remaps() { + use codex_config::types::KeybindingSpec; + use codex_config::types::KeybindingsSpec; + use codex_config::types::TuiKeymap; + + for vim in [false, true] { + let (mut composer, mut events) = new_test_composer(); + composer.set_agents_navigation_enabled(/*enabled*/ true); + composer.insert_str("previous"); + composer.handle_key_event(KeyCode::Enter.into()); + let mut config = TuiKeymap::default(); + if vim { + config.vim_normal.move_left = Some(KeybindingsSpec::One(KeybindingSpec("h".into()))); + config.vim_normal.move_up = Some(KeybindingsSpec::One(KeybindingSpec("left".into()))); + } else { + config.editor.move_left = Some(KeybindingsSpec::One(KeybindingSpec("ctrl-b".into()))); + config.editor.move_up = Some(KeybindingsSpec::One(KeybindingSpec("left".into()))); + } + composer.set_keymap_bindings(&RuntimeKeymap::from_config(&config).unwrap()); + composer.set_vim_enabled(vim); + assert!(composer.footer_props().key_hints.agents.is_none()); + composer.handle_key_event(KeyCode::Left.into()); + assert_eq!(composer.current_text(), "previous"); + assert!(events.try_recv().is_err()); + } +} diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs b/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs index d8a6201946..81734f2d08 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs @@ -211,7 +211,7 @@ impl ChatComposer { &mut self, key_event: KeyEvent, ) -> (InputResult, bool) { - if self.handle_shortcut_overlay_key(&key_event) { + if self.handle_empty_prompt_shortcut(&key_event) { return (InputResult::None, true); } if key_event.code == KeyCode::Esc { diff --git a/codex-rs/tui/src/bottom_pane/footer.rs b/codex-rs/tui/src/bottom_pane/footer.rs index 06eda95148..ab4366e74a 100644 --- a/codex-rs/tui/src/bottom_pane/footer.rs +++ b/codex-rs/tui/src/bottom_pane/footer.rs @@ -107,6 +107,7 @@ const FOOTER_CONTEXT_GAP_COLS: u16 = 1; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) struct FooterKeyHints { + pub(crate) agents: Option, pub(crate) toggle_shortcuts: Option, pub(crate) queue: Option, pub(crate) insert_newline: Option, @@ -122,6 +123,7 @@ impl FooterKeyHints { #[cfg(test)] pub(crate) fn default_bindings() -> Self { Self { + agents: None, toggle_shortcuts: Some(key_hint::plain(KeyCode::Char('?')).into()), queue: Some(key_hint::plain(KeyCode::Tab).into()), insert_newline: Some(key_hint::ctrl(KeyCode::Char('j')).into()), @@ -313,6 +315,13 @@ fn left_side_line( match state.hint { SummaryHintKind::None => {} SummaryHintKind::Shortcuts => { + if let Some(key) = key_hints.agents { + line.push_span(key); + line.push_span(" for agents".dim()); + if key_hints.toggle_shortcuts.is_some() { + line.push_span(" · ".dim()); + } + } if let Some(key) = key_hints.toggle_shortcuts { line.push_span(key); line.push_span(" for shortcuts".dim()); @@ -384,6 +393,19 @@ pub(crate) fn single_line_footer_layout( } }; let state_width = |state: LeftSideState| -> u16 { state_line(state).width() as u16 }; + if show_shortcuts_hint && key_hints.agents.is_some() { + let compact_line = left_side_line( + collaboration_mode_indicator, + default_state, + FooterKeyHints { + toggle_shortcuts: None, + ..key_hints + }, + ); + if can_show_left_with_context(area, compact_line.width() as u16, context_width) { + return (SummaryLeft::Custom(compact_line), true); + } + } // When the mode cycle hint is applicable (idle, non-queue mode), only show // the right-side context indicator if the "(shift+tab to cycle)" variant // can also fit. @@ -788,6 +810,13 @@ pub(crate) fn passive_footer_status_line(props: &FooterProps) -> Option Vec> { ordered.push(change_mode); } ordered.push(show_transcript); - let mut lines = build_columns(ordered); + if let Some(key) = state.key_hints.agents { + lines.push(Line::from(vec![ + key.into(), + " for agents (empty prompt)".into(), + ])); + } lines.push(Line::from("")); lines.push(Line::from(vec![ "customize shortcuts with ".into(), diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index a594384ff5..4e9e0c948e 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -380,6 +380,11 @@ impl BottomPane { self.request_redraw(); } + pub(crate) fn set_agents_navigation_enabled(&mut self, enabled: bool) { + self.composer.set_agents_navigation_enabled(enabled); + self.request_redraw(); + } + pub(crate) fn set_task_mentions_enabled(&mut self, enabled: bool) { self.composer.set_task_mentions_enabled(enabled); self.request_redraw(); diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__agents_navigation_enabled.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__agents_navigation_enabled.snap new file mode 100644 index 0000000000..2924342dce --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__agents_navigation_enabled.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" ← for agents · ? for shortcuts 100% context left " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__agents_navigation_help.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__agents_navigation_help.snap new file mode 100644 index 0000000000..5d02395387 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__agents_navigation_help.snap @@ -0,0 +1,22 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" / for commands ! for shell commands " +" ctrl + j for newline tab to submit message " +" @ for file paths ctrl + v to paste images " +" ctrl + g to edit in external editor esc esc to edit previous message " +" ctrl + r search history ctrl + c to exit " +" ⌥ + , reasoning down ⌥ + . reasoning up " +" ctrl + t to view transcript " +" ← for agents (empty prompt) " +" " +" customize shortcuts with /keymap " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__agents_navigation_status.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__agents_navigation_status.snap new file mode 100644 index 0000000000..eddb4ae501 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__agents_navigation_status.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/bottom_pane/chat_composer.rs +expression: terminal.backend() +--- +" " +"› Ask Codex to do anything " +" " +" " +" " +" " +" " +" " +" model · project · ← for agents " diff --git a/codex-rs/tui/src/chatwidget/interaction.rs b/codex-rs/tui/src/chatwidget/interaction.rs index ffa8c25306..015a69807f 100644 --- a/codex-rs/tui/src/chatwidget/interaction.rs +++ b/codex-rs/tui/src/chatwidget/interaction.rs @@ -4,6 +4,10 @@ use super::*; use crate::bottom_pane::BottomPaneView; impl ChatWidget { + pub(crate) fn set_agents_navigation_enabled(&mut self, enabled: bool) { + self.bottom_pane.set_agents_navigation_enabled(enabled); + } + pub(crate) fn keymap_contexts(&self) -> crate::keymap::KeymapContextSet { self.bottom_pane.keymap_contexts() } diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index fb79fd2c82..2f95913b21 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -599,25 +599,12 @@ impl RuntimeKeymap { || configured_context_alias_is_used(&keymap.list, alias) || configured_context_alias_is_used(&keymap.approval, alias) }); - let open_agents_default_is_shadowed = keymap.global.open_agents.is_none() - && (configured_main_surface_alias_is_used(keymap, "alt-a") - || configured_context_alias_is_used(&keymap.list, "alt-a") - || configured_context_alias_is_used(&keymap.approval, "alt-a") - || chords.bindings.iter().any(|binding| { - binding.action.context.overlaps(KeymapContext::Global) - && binding.chord.prefix.parts() == key_hint::alt(KeyCode::Char('a')).parts() - })); - let app = AppKeymap { - open_agents: if open_agents_default_is_shadowed { - Vec::new() - } else { - resolve_bindings( - keymap.global.open_agents.as_ref(), - &defaults.app.open_agents, - "tui.keymap.global.open_agents", - )? - }, + open_agents: resolve_bindings( + keymap.global.open_agents.as_ref(), + &defaults.app.open_agents, + "tui.keymap.global.open_agents", + )?, open_transcript: resolve_bindings( keymap.global.open_transcript.as_ref(), &defaults.app.open_transcript, @@ -1397,7 +1384,7 @@ impl RuntimeKeymap { fn built_in_defaults() -> Self { Self { app: AppKeymap { - open_agents: default_bindings![alt(KeyCode::Char('a'))], + open_agents: default_bindings![], open_transcript: default_bindings![ctrl(KeyCode::Char('t'))], open_external_editor: default_bindings![ctrl(KeyCode::Char('g'))], copy: default_bindings![ctrl(KeyCode::Char('o'))], @@ -3314,32 +3301,6 @@ mod tests { ); } - #[test] - fn agents_overview_default_yields_to_existing_custom_shortcuts() { - let mut keymap = TuiKeymap::default(); - keymap.global.open_transcript = Some(one("alt-a")); - - let runtime = RuntimeKeymap::from_config(&keymap).expect("existing keymap remains valid"); - - assert_eq!( - runtime.app.open_transcript, - vec![key_hint::alt(KeyCode::Char('a'))] - ); - assert!(runtime.app.open_agents.is_empty()); - - keymap.global.open_transcript = Some(one("alt-a ctrl-t")); - let runtime = RuntimeKeymap::from_config(&keymap).expect("existing chord remains valid"); - assert!(runtime.app.open_agents.is_empty()); - - keymap.global.open_transcript = None; - keymap.pager.scroll_up = Some(one("alt-a page-up")); - let runtime = RuntimeKeymap::from_config(&keymap).expect("pager chord remains valid"); - assert_eq!( - runtime.app.open_agents, - vec![key_hint::alt(KeyCode::Char('a'))] - ); - } - #[test] fn toggle_side_conversation_can_be_remapped_and_rejects_conflicts() { let mut keymap = TuiKeymap::default(); diff --git a/codex-rs/tui/src/keymap_setup.rs b/codex-rs/tui/src/keymap_setup.rs index 57528f4a4c..3c8b4ecd57 100644 --- a/codex-rs/tui/src/keymap_setup.rs +++ b/codex-rs/tui/src/keymap_setup.rs @@ -1071,6 +1071,7 @@ mod tests { )) .collect::>(), vec![ + ("Open Agents", Some("unbound"), false), ("Toggle Vim Mode", Some("unbound"), false), ("Previous Permission Mode", Some("unbound"), false), ("Next Permission Mode", Some("unbound"), false), diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap index 6b2c6717f2..0a6af7a34e 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_all_tab_search.snap @@ -2,7 +2,7 @@ source: tui/src/keymap_setup.rs expression: snapshot --- -Open Agents | alt-a | Global open_agents Open Agents Open the shared agent-session overview. alt-a Default +Open Agents | unbound | Global open_agents Open Agents Open the shared agent-session overview. unbound Default Open Transcript | ctrl-t | Global open_transcript Open Transcript Open the transcript overlay. ctrl-t Default Open External Editor | ctrl-g | Global open_external_editor Open External Editor Open the current draft in an external editor. ctrl-g Default Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard. ctrl-o Default diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap index 0f4999bb42..58432b15bd 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap @@ -5,12 +5,12 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 135 actions, 1 customized, 4 unbound. + 135 actions, 1 customized, 5 unbound. - [All] Common Customized (1) Unbound (4) App Composer Editor Vim Navigation Agents Approval Debug + [All] Common Customized (1) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug Type to search shortcuts -› Global Open Agents alt-a +› Global - Open Agents unbound Global Open Transcript ctrl-t Global Open External Editor ctrl-g Global Copy ctrl-o diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap index 421b7ea7fe..f95f620048 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap @@ -5,12 +5,12 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 136 actions, 0 customized, 5 unbound. + 136 actions, 0 customized, 6 unbound. - [All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug + [All] Common Customized (0) Unbound (6) App Composer Editor Vim Navigation Agents Approval Debug Type to search shortcuts -› Global Open Agents alt-a +› Global - Open Agents unbound Global Open Transcript ctrl-t Global Open External Editor ctrl-g Global Copy ctrl-o diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap index 46394f6ec6..2696436fc0 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap @@ -5,7 +5,7 @@ expression: snapshot tab: All (135 selectable) tab: Common (20 selectable) tab: Customized (0) (0 selectable) -tab: Unbound (4) (4 selectable) +tab: Unbound (5) (5 selectable) tab: App (14 selectable) tab: Composer (5 selectable) tab: Editor (17 selectable) @@ -14,7 +14,7 @@ tab: Navigation (20 selectable) tab: Agents (5 selectable) tab: Approval (8 selectable) tab: Debug (1 selectable) -Open Agents | alt-a | Global open_agents Open Agents Open the shared agent-session overview. alt-a Default +Open Agents | unbound | Global open_agents Open Agents Open the shared agent-session overview. unbound Default Open Transcript | ctrl-t | Global open_transcript Open Transcript Open the transcript overlay. ctrl-t Default Open External Editor | ctrl-g | Global open_external_editor Open External Editor Open the current draft in an external editor. ctrl-g Default Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard. ctrl-o Default diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_global_chord.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_global_chord.snap index 2b3b90ee23..d74a43b933 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_global_chord.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_global_chord.snap @@ -5,12 +5,12 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 135 actions, 1 customized, 4 unbound. + 135 actions, 1 customized, 5 unbound. - [All] Common Customized (1) Unbound (4) App Composer Editor Vim Navigation Agents Approval Debug + [All] Common Customized (1) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug Type to search shortcuts -› Global Open Agents alt-a +› Global - Open Agents unbound Global * Open Transcript ctrl-t, ctrl-x ctrl-t Global Open External Editor ctrl-g Global Copy ctrl-o diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap index f10d0c3da7..4cd05f21d8 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap @@ -5,13 +5,13 @@ expression: "render_picker(params, 78)" Keymap All configurable shortcuts. - 135 actions, 0 customized, 4 unbound. + 135 actions, 0 customized, 5 unbound. - [All] Common Customized (0) Unbound (4) App Composer Editor Vim + [All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug Type to search shortcuts -› Global Open Agents alt-a +› Global - Open Agents unbound Global Open Transcript ctrl-t Global Open External Editor ctrl-g Global Copy ctrl-o diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap index d0734ddea6..8450e8527b 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap @@ -5,12 +5,12 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 135 actions, 0 customized, 4 unbound. + 135 actions, 0 customized, 5 unbound. - [All] Common Customized (0) Unbound (4) App Composer Editor Vim Navigation Agents Approval Debug + [All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug Type to search shortcuts -› Global Open Agents alt-a +› Global - Open Agents unbound Global Open Transcript ctrl-t Global Open External Editor ctrl-g Global Copy ctrl-o