diff --git a/codex-rs/config/src/tui_keymap.rs b/codex-rs/config/src/tui_keymap.rs index a1a6550b18..67ba13f867 100644 --- a/codex-rs/config/src/tui_keymap.rs +++ b/codex-rs/config/src/tui_keymap.rs @@ -123,6 +123,8 @@ pub struct TuiGlobalKeymap { #[serde(deny_unknown_fields)] #[schemars(deny_unknown_fields)] pub struct TuiChatKeymap { + /// Start or stop a voice conversation. + pub toggle_voice: Option, /// Toggle the microphone in an active voice conversation. pub toggle_voice_mute: Option, /// Interrupt the active turn. diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 7aa4f94219..e53a95e2d5 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -4139,6 +4139,7 @@ "previous_permission_mode": null, "prompt_stack_back": null, "skip_question": null, + "toggle_voice": null, "toggle_voice_mute": null }, "composer": { @@ -4649,6 +4650,14 @@ ], "description": "Skip the focused question." }, + "toggle_voice": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Start or stop a voice conversation." + }, "toggle_voice_mute": { "allOf": [ { @@ -5008,6 +5017,7 @@ "previous_permission_mode": null, "prompt_stack_back": null, "skip_question": null, + "toggle_voice": null, "toggle_voice_mute": null } }, diff --git a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs index b7ddeb167d..be68e78b0b 100644 --- a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs +++ b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs @@ -2743,14 +2743,13 @@ async fn paginated_workflows_never_request_full_thread_history() -> Result<()> { .await?; app_server.remember_thread_history_mode(paginated_thread_id, ThreadHistoryMode::Legacy); - let resumed = app_server - .resume_thread( - &app.local_settings, - app.config.clone(), - paginated_thread_id, - crate::app_server_session::ResumeModelSettings::RestoreFromThread, - ) - .await?; + let resumed = Box::pin(app_server.resume_thread( + &app.local_settings, + app.config.clone(), + paginated_thread_id, + crate::app_server_session::ResumeModelSettings::RestoreFromThread, + )) + .await?; assert_eq!(resumed.session.thread_id, paginated_thread_id); assert!(recorded_params(&requests, "thread/read").is_empty()); let resume_requests = recorded_params(&requests, "thread/resume"); @@ -2764,18 +2763,16 @@ async fn paginated_workflows_never_request_full_thread_history() -> Result<()> { ) .await?; assert!(!cells.is_empty()); - app_server - .fork_thread(&app.local_settings, app.config.clone(), paginated_thread_id) + Box::pin(app_server.fork_thread(&app.local_settings, app.config.clone(), paginated_thread_id)) .await?; let mut side_config = app.config.clone(); side_config.ephemeral = true; - app_server - .fork_side_thread( - &crate::local_settings::LocalSettings::from(&side_config), - side_config, - paginated_thread_id, - ) - .await?; + Box::pin(app_server.fork_side_thread( + &crate::local_settings::LocalSettings::from(&side_config), + side_config, + paginated_thread_id, + )) + .await?; let paginated_reads = recorded_params(&requests, "thread/read"); assert!(!paginated_reads.is_empty()); diff --git a/codex-rs/tui/src/chatwidget/interaction.rs b/codex-rs/tui/src/chatwidget/interaction.rs index de29f1da10..c767c8460a 100644 --- a/codex-rs/tui/src/chatwidget/interaction.rs +++ b/codex-rs/tui/src/chatwidget/interaction.rs @@ -78,6 +78,14 @@ impl ChatWidget { return; } + if key_event.kind == KeyEventKind::Press + && self.chat_keymap.toggle_voice.is_pressed(key_event) + && self.bottom_pane.no_modal_or_popup_active() + { + self.toggle_realtime_conversation(); + return; + } + if self.handle_realtime_microphone_shortcut(key_event) { return; } diff --git a/codex-rs/tui/src/chatwidget/realtime/recording_controls_tests.rs b/codex-rs/tui/src/chatwidget/realtime/recording_controls_tests.rs index b288ae1029..d924338602 100644 --- a/codex-rs/tui/src/chatwidget/realtime/recording_controls_tests.rs +++ b/codex-rs/tui/src/chatwidget/realtime/recording_controls_tests.rs @@ -717,3 +717,69 @@ async fn compact_voice_meters_keep_real_speaker_history_when_the_microphone_is_m chat.update_realtime_footer(); assert!(!render_bottom_popup(&chat, /*width*/ 45).contains("codex")); } + +#[tokio::test] +async fn voice_toggle_shortcut_uses_the_slash_command_start_guard_and_preserves_draft() { + let (mut chat, _sender, mut events, mut ops) = make_chatwidget_manual_with_sender().await; + chat.set_side_conversation_active(/*active*/ true); + chat.bottom_pane + .set_composer_text("draft".to_string(), Vec::new(), Vec::new()); + + chat.handle_key_event(KeyEvent::new(KeyCode::F(8), KeyModifiers::NONE)); + + let Ok(AppEvent::InsertHistoryCell(cell)) = events.try_recv() else { + panic!("voice should report that side conversations are unsupported"); + }; + assert!( + cell.display_lines(/*width*/ 80) + .iter() + .any(|line| line.to_string().contains("side conversations")) + ); + assert!(render_bottom_popup(&chat, /*width*/ 80).contains("draft")); + assert!(!chat.realtime_conversation_is_running()); + assert!(ops.try_recv().is_err()); +} + +#[tokio::test] +async fn voice_toggle_shortcut_stops_only_on_press_outside_popups() { + let (mut chat, _sender, _events, mut ops) = make_chatwidget_manual_with_sender().await; + let thread_id = activate_voice(&mut chat); + let shortcut = KeyEvent::new(KeyCode::F(8), KeyModifiers::NONE); + for kind in [KeyEventKind::Repeat, KeyEventKind::Release] { + chat.handle_key_event(KeyEvent { kind, ..shortcut }); + assert!(chat.realtime_conversation_is_running()); + } + chat.bottom_pane + .set_composer_text("/".to_string(), Vec::new(), Vec::new()); + chat.handle_key_event(shortcut); + assert!(chat.realtime_conversation_is_running()); + assert!(ops.try_recv().is_err()); + chat.bottom_pane + .set_composer_text(String::new(), Vec::new(), Vec::new()); + + chat.handle_key_event(shortcut); + + assert!(!chat.realtime_conversation_is_running()); + assert!( + matches!(ops.try_recv(), Ok(AppCommand::RealtimeConversationStop { thread_id: stopped }) if stopped == thread_id) + ); +} + +#[tokio::test] +async fn voice_toggle_shortcut_respects_live_remapping_and_unbinding() { + let (mut chat, _sender, _events, mut ops) = make_chatwidget_manual_with_sender().await; + for (binding, stops) in [("'f9'", true), ("[]", false)] { + let config = toml::from_str::(&format!( + "[chat]\ntoggle_voice = {binding}" + )) + .unwrap(); + let runtime = crate::keymap::RuntimeKeymap::from_config(&config).unwrap(); + chat.apply_keymap_update(config, &runtime); + activate_voice(&mut chat); + chat.handle_key_event(KeyEvent::new(KeyCode::F(8), KeyModifiers::NONE)); + assert!(chat.realtime_conversation_is_running()); + chat.handle_key_event(KeyEvent::new(KeyCode::F(9), KeyModifiers::NONE)); + assert_eq!(chat.realtime_conversation_is_running(), !stops); + assert_eq!(ops.try_recv().is_ok(), stops); + } +} diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index 40404e8339..15d3b32b03 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -115,6 +115,8 @@ pub(crate) struct AppKeymap { /// handler code, not here. #[derive(Clone, Debug)] pub(crate) struct ChatKeymap { + /// Start or stop a voice conversation. + pub(crate) toggle_voice: Vec, /// Toggle capture in the active voice session. pub(crate) toggle_voice_mute: Vec, chord_hints: Arc, @@ -627,6 +629,13 @@ impl RuntimeKeymap { || configured_context_alias_is_used(&keymap.list, alias) || configured_context_alias_is_used(&keymap.approval, alias) }); + let voice_toggle_default_is_shadowed = keymap.chat.toggle_voice.is_none() + && (configured_main_surface_alias_is_used(keymap, "f8") + || configured_context_alias_is_used(&keymap.vim_search, "f8") + || chords.bindings.iter().any(|binding| { + binding.action.context.overlaps(KeymapContext::Chat) + && binding.chord.prefix.parts() == key_hint::plain(KeyCode::F(8)).parts() + })); // Preserve existing Ctrl+X shortcuts and chord prefixes when adding this default. let voice_mute_default_is_shadowed = keymap.chat.toggle_voice_mute.is_none() && (configured_main_surface_alias_is_used(keymap, "ctrl-x") @@ -689,6 +698,15 @@ impl RuntimeKeymap { }; let mut chat = ChatKeymap { + toggle_voice: if voice_toggle_default_is_shadowed { + Vec::new() + } else { + resolve_bindings( + keymap.chat.toggle_voice.as_ref(), + &defaults.chat.toggle_voice, + "tui.keymap.chat.toggle_voice", + )? + }, toggle_voice_mute: if voice_mute_default_is_shadowed { Vec::new() } else { @@ -1570,6 +1588,7 @@ impl RuntimeKeymap { }, chords: Arc::default(), chat: ChatKeymap { + toggle_voice: default_bindings![plain(KeyCode::F(8))], toggle_voice_mute: default_bindings![ctrl(KeyCode::Char('x'))], chord_hints: Arc::default(), interrupt_turn: default_bindings![plain(KeyCode::Esc)], @@ -1864,6 +1883,7 @@ impl RuntimeKeymap { /// backtracking, intentionally stay outside this configurable keymap. fn validate_conflicts(&self) -> Result<(), String> { for (action, bindings) in [ + ("toggle_voice", &self.chat.toggle_voice), ( "previous_permission_mode", &self.chat.previous_permission_mode, @@ -1883,14 +1903,15 @@ impl RuntimeKeymap { } } #[cfg(unix)] - if self - .app - .open_agents - .contains(&key_hint::ctrl(KeyCode::Char('z'))) - { - return Err( - "tui.keymap.global.open_agents: ctrl-z is reserved for suspend".to_string(), - ); + for (action, bindings) in [ + ("global.open_agents", &self.app.open_agents), + ("chat.toggle_voice", &self.chat.toggle_voice), + ] { + if bindings.contains(&key_hint::ctrl(KeyCode::Char('z'))) { + return Err(format!( + "tui.keymap.{action}: ctrl-z is reserved for suspend" + )); + } } if self.app.open_agents.iter().any(|binding| { matches!(binding.parts(), (KeyCode::Char(_), modifiers) @@ -1923,6 +1944,7 @@ impl RuntimeKeymap { ("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()), ("toggle_raw_output", self.app.toggle_raw_output.as_slice()), ("toggle_side_conversation", side_toggle_bindings.as_slice()), + ("chat.toggle_voice", self.chat.toggle_voice.as_slice()), ( "chat.toggle_voice_mute", self.chat.toggle_voice_mute.as_slice(), @@ -2077,6 +2099,7 @@ impl RuntimeKeymap { ), ("copy", self.app.copy.as_slice()), ("clear_terminal", self.app.clear_terminal.as_slice()), + ("chat.toggle_voice", self.chat.toggle_voice.as_slice()), ( "chat.toggle_voice_mute", self.chat.toggle_voice_mute.as_slice(), diff --git a/codex-rs/tui/src/keymap/bindings.rs b/codex-rs/tui/src/keymap/bindings.rs index 44c6ea7477..99a6d97823 100644 --- a/codex-rs/tui/src/keymap/bindings.rs +++ b/codex-rs/tui/src/keymap/bindings.rs @@ -260,6 +260,7 @@ define_runtime_action_bindings! { toggle_side_conversation, ], "chat" => Chat, chat, chat [ + toggle_voice, interrupt_turn, decrease_reasoning_effort, increase_reasoning_effort, diff --git a/codex-rs/tui/src/keymap/voice_tests.rs b/codex-rs/tui/src/keymap/voice_tests.rs index 7e66099edc..7e6e269a5d 100644 --- a/codex-rs/tui/src/keymap/voice_tests.rs +++ b/codex-rs/tui/src/keymap/voice_tests.rs @@ -83,3 +83,102 @@ fn voice_mute_chord_dispatches_only_in_its_active_context() { }; assert!(runtime.chat.toggle_voice_mute.is_pressed(dispatch)); } + +#[test] +fn voice_toggle_resolves_custom_bindings_unbinding_and_visible_hints() { + for (config, expected) in [ + ("", Some("f8")), + ("[chat]\ntoggle_voice = 'f9'", Some("f9")), + ("[chat]\ntoggle_voice = []", None), + ("[chat]\ntoggle_voice = 'f8 v'", Some("f8 v")), + ] { + let config = toml::from_str::(config).unwrap(); + let runtime = RuntimeKeymap::from_config(&config).unwrap(); + assert_eq!( + runtime + .primary_hint(KeymapContext::Chat, "toggle_voice") + .map(ShortcutHint::display_label), + expected.map(str::to_string), + ); + } +} + +#[test] +fn voice_toggle_default_yields_to_existing_shortcuts_and_chord_prefixes() { + for config in [ + "[editor]\nkill_line_end = 'f8'", + "[vim_search]\nnext = 'f8'", + "[chat]\ntoggle_voice_mute = 'f8'", + "[global]\nopen_transcript = 'f8 t'", + ] { + let config = toml::from_str::(config).unwrap(); + let runtime = RuntimeKeymap::from_config(&config).expect("existing binding stays valid"); + assert_eq!( + runtime.primary_hint(KeymapContext::Chat, "toggle_voice"), + None + ); + } + for (binding, conflict) in [ + ("ctrl-k", "kill_line_end"), + ("ctrl-c", "fixed.interrupt_or_quit"), + ("ctrl-t", "open_transcript"), + #[cfg(unix)] + ("ctrl-z", "ctrl-z is reserved for suspend"), + ] { + let config = + toml::from_str::(&format!("[chat]\ntoggle_voice = '{binding}'")).unwrap(); + assert!( + RuntimeKeymap::from_config(&config) + .unwrap_err() + .contains(conflict) + ); + } +} + +#[test] +fn voice_toggle_chord_dispatches_in_chat_without_an_active_voice_session() { + let config = toml::from_str::("[chat]\ntoggle_voice = 'f8 v'").unwrap(); + let runtime = RuntimeKeymap::from_config(&config).unwrap(); + let mut matcher = KeyChordMatcher::default(); + let now = tokio::time::Instant::now(); + let prefix = KeyEvent::new(KeyCode::F(8), KeyModifiers::NONE); + assert_eq!( + matcher.advance( + prefix, + &runtime.chords, + KeymapContextSet::new(KeymapContext::Pager), + now + ), + KeyChordMatch::PassThrough + ); + let contexts = KeymapContextSet::new(KeymapContext::Chat); + assert!(matches!( + matcher.advance(prefix, &runtime.chords, contexts, now), + KeyChordMatch::Pending(_) + )); + let KeyChordMatch::Completed(dispatch) = matcher.advance( + KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE), + &runtime.chords, + contexts, + now, + ) else { + panic!("voice toggle chord completes") + }; + assert!(runtime.chat.toggle_voice.is_pressed(dispatch)); +} + +#[test] +fn voice_toggle_reserves_text_input() { + for binding in ["v", "shift-v", "2", "space"] + .into_iter() + .chain(cfg!(windows).then_some("ctrl-alt-q")) + { + let config = + toml::from_str::(&format!("[chat]\ntoggle_voice = '{binding}'")).unwrap(); + assert!( + RuntimeKeymap::from_config(&config) + .expect_err("voice shortcuts must not intercept typing") + .contains("printable keys are reserved for text input") + ); + } +} diff --git a/codex-rs/tui/src/keymap_setup.rs b/codex-rs/tui/src/keymap_setup.rs index a2b5edc3bb..2610da3eb1 100644 --- a/codex-rs/tui/src/keymap_setup.rs +++ b/codex-rs/tui/src/keymap_setup.rs @@ -1172,6 +1172,22 @@ mod tests { assert_snapshot!("keymap_picker_redo", redo_row); } + #[test] + fn picker_voice_toggle_snapshot() { + assert_snapshot!( + "keymap_voice_toggle", + render_picker( + build_keymap_action_menu_params( + "chat".into(), + "toggle_voice".into(), + &RuntimeKeymap::defaults(), + &TuiKeymap::default(), + ), + /*width*/ 80, + ) + ); + } + #[test] fn picker_question_actions_snapshot() { let runtime = RuntimeKeymap::defaults(); diff --git a/codex-rs/tui/src/keymap_setup/actions.rs b/codex-rs/tui/src/keymap_setup/actions.rs index 0c809ea1ff..04a8590ed7 100644 --- a/codex-rs/tui/src/keymap_setup/actions.rs +++ b/codex-rs/tui/src/keymap_setup/actions.rs @@ -101,6 +101,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[ action("chat", "Chat", "edit_queued_message", "Move up through questions, then edit the last queued message."), action("chat", "Chat", "prompt_stack_back", "Move back through questions toward the composer."), action("chat", "Chat", "skip_question", "Skip the focused question."), + action("chat", "Chat", "toggle_voice", "Start or stop a voice conversation."), action("chat", "Chat", "toggle_voice_mute", "Mute or unmute the voice microphone."), action("composer", "Composer", "submit", "Submit the current composer draft."), action("composer", "Composer", "queue", "Queue the draft while a task is running."), @@ -282,6 +283,7 @@ pub(super) fn binding_slot<'a>( ("chat", "edit_queued_message") => Some(&mut keymap.chat.edit_queued_message), ("chat", "prompt_stack_back") => Some(&mut keymap.chat.prompt_stack_back), ("chat", "skip_question") => Some(&mut keymap.chat.skip_question), + ("chat", "toggle_voice") => Some(&mut keymap.chat.toggle_voice), ("chat", "toggle_voice_mute") => Some(&mut keymap.chat.toggle_voice_mute), ("composer", "submit") => Some(&mut keymap.composer.submit), ("composer", "queue") => Some(&mut keymap.composer.queue), 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 edc15b7906..31d3b5cf9a 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,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 146 actions, 1 customized, 5 unbound. + 147 actions, 1 customized, 5 unbound. [All] Common Customized (1) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug 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 ccad420a19..7eb43c0ffd 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,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 147 actions, 0 customized, 6 unbound. + 148 actions, 0 customized, 6 unbound. [All] Common Customized (0) Unbound (6) App Composer Editor Vim Navigation Agents Approval Debug 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 1161a41966..3f190d0405 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 @@ -2,11 +2,11 @@ source: tui/src/keymap_setup.rs expression: snapshot --- -tab: All (146 selectable) +tab: All (147 selectable) tab: Common (20 selectable) tab: Customized (0) (0 selectable) tab: Unbound (5) (5 selectable) -tab: App (17 selectable) +tab: App (18 selectable) tab: Composer (5 selectable) tab: Editor (17 selectable) tab: Vim (69 selectable) 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 85bbb42f4c..b8b07e81ac 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,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 146 actions, 1 customized, 6 unbound. + 147 actions, 1 customized, 6 unbound. [All] Common Customized (1) Unbound (6) App Composer Editor Vim Navigation Agents Approval Debug 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 20cad9e811..b5223dd452 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,7 +5,7 @@ expression: "render_picker(params, 78)" Keymap All configurable shortcuts. - 146 actions, 0 customized, 5 unbound. + 147 actions, 0 customized, 5 unbound. [All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug 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 fbfec35d5e..dd888c732d 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,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 146 actions, 0 customized, 5 unbound. + 147 actions, 0 customized, 5 unbound. [All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_question_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_question_actions.snap index 5041ccfa95..259f8fddc9 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_question_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_question_actions.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 146 actions, 0 customized, 5 unbound. + 147 actions, 0 customized, 5 unbound. [All] Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_voice_toggle.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_voice_toggle.snap new file mode 100644 index 0000000000..2e150df2cf --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_voice_toggle.snap @@ -0,0 +1,22 @@ +--- +source: tui/src/keymap_setup.rs +expression: "render_picker(build_keymap_action_menu_params(\"chat\".into(),\n\"toggle_voice\".into(), &RuntimeKeymap::defaults(), &TuiKeymap::default(),),\n80,)" +--- + + Edit Shortcut + Toggle Voice · Chat + Current f8 · Default keymap + Config `tui.keymap.chat.toggle_voice` + Start or stop a voice conversation. + +› 1. Replace binding Capture a replacement key for `f8`. + 2. Add alternate binding Keep `f8` and add another key. + 3. Replace with key chord Replace `f8` with a two-stroke key + chord. + 4. Add alternate key chord Keep `f8` and add a two-stroke key + chord. + – Remove custom binding (disabled) No custom root override to remove. + 5. Back to shortcuts Return to the shortcut list. + + Changes write the root `tui.keymap.*` override. + enter select · esc back