mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Open the agents overview from an empty composer (#41917)
## What changed - Open the agents overview with Left from an empty, focused composer in local-daemon sessions. - Keep the shortcut inactive while editing text, using transient input surfaces, or when Left has been explicitly remapped. - Show the contextual shortcut in the footer and shortcuts overlay. - Leave the configurable `open_agents` action unbound by default instead of assigning `Alt+A`. ## Testing - Add coverage for session eligibility, input-state guards, editor and Vim remaps, event dispatch, and footer rendering. GitOrigin-RevId: ef17db3d9d09c3a08c3f7353c5b8a02f2b61c696
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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<FrameRequester>,
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<ShortcutHint>,
|
||||
pub(crate) toggle_shortcuts: Option<ShortcutHint>,
|
||||
pub(crate) queue: Option<ShortcutHint>,
|
||||
pub(crate) insert_newline: Option<ShortcutHint>,
|
||||
@@ -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<Line<'st
|
||||
}
|
||||
}
|
||||
|
||||
if props.mode == FooterMode::ComposerEmpty
|
||||
&& let Some(key) = props.key_hints.agents
|
||||
&& let Some(line) = line.as_mut()
|
||||
{
|
||||
line.extend(vec![" · ".dim(), key.into(), " for agents".dim()]);
|
||||
}
|
||||
|
||||
line
|
||||
}
|
||||
|
||||
@@ -939,8 +968,13 @@ fn shortcut_overlay_lines(state: ShortcutsState) -> Vec<Line<'static>> {
|
||||
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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
+13
@@ -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 "
|
||||
+22
@@ -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 "
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" model · project · ← for agents "
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1071,6 +1071,7 @@ mod tests {
|
||||
))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
("Open Agents", Some("unbound"), false),
|
||||
("Toggle Vim Mode", Some("unbound"), false),
|
||||
("Previous Permission Mode", Some("unbound"), false),
|
||||
("Next Permission Mode", Some("unbound"), false),
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user