mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Add a configurable F8 shortcut for voice conversations (#46071)
## What changed - Bind `F8` to start or stop a voice conversation through the existing voice toggle handler, preserving its start guards and the composer draft. - Expose `tui.keymap.chat.toggle_voice` in the configuration schema and keymap picker, with support for remapping, unbinding, and key chords. - Let the default yield to existing `F8` bindings and overlapping chord prefixes, and validate explicit bindings against shortcut conflicts and reserved text input. - Handle the shortcut only on key presses when no modal or popup is active. ## Testing Add tests for default and custom bindings, conflicts, chord dispatch, live remapping and unbinding, draft preservation, start guards, and popup and key-event handling. Add a snapshot for the voice shortcut editor. GitOrigin-RevId: e5e373e8eefa89bec4d5cbdca801616e312134b5
This commit is contained in:
@@ -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<KeybindingsSpec>,
|
||||
/// Toggle the microphone in an active voice conversation.
|
||||
pub toggle_voice_mute: Option<KeybindingsSpec>,
|
||||
/// Interrupt the active turn.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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::<codex_config::types::TuiKeymap>(&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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<KeyBinding>,
|
||||
/// Toggle capture in the active voice session.
|
||||
pub(crate) toggle_voice_mute: Vec<KeyBinding>,
|
||||
chord_hints: Arc<RuntimeChordKeymap>,
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::<TuiKeymap>(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::<TuiKeymap>(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::<TuiKeymap>(&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::<TuiKeymap>("[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::<TuiKeymap>(&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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user