mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
Add directional conversation focus shortcuts in workspace mode (#59287)
## What changed - Add configurable `focus_pane_left` and `focus_pane_right` actions, defaulting to `leader left` / `leader h` and `leader right` / `leader l`. - Move focus to the parent or open side conversation without reopening a hidden pane. Preserve the current interaction when no neighbor is available, and accept fresh typing after a no-op shortcut while suppressing reported key repeats. - Expose the actions in the keymap picker and configuration schema, and prevent workspace-only pane bindings from claiming keys in classic mode. ## Testing Add regression coverage for pane navigation, preserved transcript selection and search, fresh typing after a no-op, key capture, and binding conflicts and limits. GitOrigin-RevId: 7f7629d15cffcd35d1353b36c5c9208dbb1034e8
This commit is contained in:
@@ -107,7 +107,11 @@ impl KeymapActionId {
|
||||
self.context == KeymapContext::Global
|
||||
&& matches!(
|
||||
self.action,
|
||||
"focus_main_composer" | "focus_sidebar_content" | "hide_pane"
|
||||
"focus_main_composer"
|
||||
| "focus_sidebar_content"
|
||||
| "hide_pane"
|
||||
| "focus_pane_left"
|
||||
| "focus_pane_right"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -306,6 +310,8 @@ define_runtime_action_bindings! {
|
||||
focus_main_composer,
|
||||
focus_sidebar_content,
|
||||
hide_pane,
|
||||
focus_pane_left,
|
||||
focus_pane_right,
|
||||
open_warnings => runtime_only,
|
||||
open_external_editor,
|
||||
copy,
|
||||
|
||||
@@ -56,6 +56,7 @@ const TRANSCRIPT_ACTIONS: u32 = 1 << 20;
|
||||
const BACKEND_CYCLING: u32 = 1 << 21;
|
||||
const SIDE_PANE: u32 = 1 << 22;
|
||||
const MAIN_COMPOSER_FOCUS: u32 = 1 << 23;
|
||||
const PANE_DIRECTION: u32 = 1 << 24;
|
||||
|
||||
const TRANSCRIPT_BROWSING: u32 = 1 << 14;
|
||||
|
||||
@@ -158,6 +159,10 @@ impl KeymapContextSet {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn with_pane_directions(self) -> Self {
|
||||
Self(self.0 | PANE_DIRECTION)
|
||||
}
|
||||
|
||||
pub(crate) const fn with_main_composer_focus(self) -> Self {
|
||||
Self(self.0 | MAIN_COMPOSER_FOCUS)
|
||||
}
|
||||
@@ -172,10 +177,10 @@ impl KeymapContextSet {
|
||||
return self.0 & BACKEND_CYCLING != 0;
|
||||
}
|
||||
if action.is_side_pane() {
|
||||
let required = if action.action == "focus_main_composer" {
|
||||
MAIN_COMPOSER_FOCUS
|
||||
} else {
|
||||
SIDE_PANE
|
||||
let required = match action.action {
|
||||
"focus_pane_left" | "focus_pane_right" => PANE_DIRECTION,
|
||||
"focus_main_composer" => MAIN_COMPOSER_FOCUS,
|
||||
_ => SIDE_PANE,
|
||||
};
|
||||
return self.0 & required != 0;
|
||||
}
|
||||
|
||||
@@ -26,15 +26,20 @@ pub(in crate::keymap) fn register(
|
||||
.filter(|binding| binding.is_leader())
|
||||
.count(),
|
||||
);
|
||||
use KeyCode::Char;
|
||||
use KeymapContext::Chat;
|
||||
use KeymapContext::Global;
|
||||
for (context, name, completion) in [
|
||||
(Global, "change_host", 'b'),
|
||||
(Global, "select_transcript", '['),
|
||||
(Chat, "open_presets", 'p'),
|
||||
(Global, "focus_main_composer", 'c'),
|
||||
(Global, "focus_sidebar_content", 's'),
|
||||
(Global, "hide_pane", '-'),
|
||||
for (context, name, completion, spec) in [
|
||||
(Global, "change_host", Char('b'), "b"),
|
||||
(Global, "select_transcript", Char('['), "["),
|
||||
(Chat, "open_presets", Char('p'), "p"),
|
||||
(Global, "focus_main_composer", Char('c'), "c"),
|
||||
(Global, "focus_sidebar_content", Char('s'), "s"),
|
||||
(Global, "hide_pane", Char('-'), "-"),
|
||||
(Global, "focus_pane_left", KeyCode::Left, "left"),
|
||||
(Global, "focus_pane_left", Char('h'), "h"),
|
||||
(Global, "focus_pane_right", KeyCode::Right, "right"),
|
||||
(Global, "focus_pane_right", Char('l'), "l"),
|
||||
] {
|
||||
if remaining == 0 {
|
||||
break;
|
||||
@@ -56,9 +61,9 @@ pub(in crate::keymap) fn register(
|
||||
action,
|
||||
chord: KeyChord {
|
||||
prefix,
|
||||
completion: crate::key_hint::plain(KeyCode::Char(completion)),
|
||||
completion: crate::key_hint::plain(completion),
|
||||
},
|
||||
spec: format!("leader {completion}"),
|
||||
spec: format!("leader {spec}"),
|
||||
};
|
||||
if !resolved.chords.bindings.iter().any(|existing| {
|
||||
binding.overlaps(existing.action) && existing.chord == binding.chord
|
||||
|
||||
@@ -187,6 +187,42 @@ fn allows_shared_prefixes_with_distinct_completions() {
|
||||
config.global.copy = Some(binding("ctrl-x ctrl-o"));
|
||||
|
||||
RuntimeKeymap::from_config(&config).expect("prefix trie can branch on the second stroke");
|
||||
|
||||
// With 16 leaders and 56 explicit completions, only eight more aliases fit per leader.
|
||||
let mut config = TuiKeymap::default();
|
||||
config.global.leader = Some(KeybindingsSpec::Many(
|
||||
(1..=16)
|
||||
.map(|n| KeybindingSpec(format!("super-f{n}")))
|
||||
.collect(),
|
||||
));
|
||||
config.global.open_transcript = Some(KeybindingsSpec::Many(
|
||||
["", "shift-", "alt-"]
|
||||
.into_iter()
|
||||
.flat_map(|modifier| {
|
||||
(1..=19).map(move |n| KeybindingSpec(format!("leader {modifier}f{n}")))
|
||||
})
|
||||
.take(56)
|
||||
.collect(),
|
||||
));
|
||||
let runtime = RuntimeKeymap::for_session(&config, /*workspace*/ true).unwrap();
|
||||
assert_eq!(
|
||||
runtime
|
||||
.chords
|
||||
.bindings
|
||||
.iter()
|
||||
.filter(|b| b.is_leader())
|
||||
.count(),
|
||||
super::MAX_LEADER_CHORD_BINDINGS
|
||||
);
|
||||
assert_eq!(
|
||||
crate::keymap_setup::active_binding_specs(&runtime, "global", "focus_pane_left").unwrap(),
|
||||
["leader left", "leader h"]
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.primary_hint(KeymapContext::Global, "focus_pane_right")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user