Add configurable shortcuts for the agents dashboard (#39142)

## What changed

- Add `tui.keymap.global.open_agents`, defaulting to `alt-a`, to open the shared agents overview.
- Add an `agents` keymap context for search, new task, rename, stop, and grouping actions.
- Expose the new actions in the keymap picker and render dashboard hints from the resolved bindings.
- Preserve existing custom `alt-a` bindings by disabling the new default when it would conflict.

## Testing

- Cover remapping, reserved keys, conflicts, and compatibility with existing bindings.
- Verify that configured dashboard shortcuts act on the selected filtered task.

GitOrigin-RevId: 98c48a332e06cb96dbf0e59647ce7959d45b9988
This commit is contained in:
Eric Traut authored and copyberry committed 2026-08-18 05:30:11 +00:00
1 parent 9a254ba1fa
commit f47f77ada6
19 files changed
+661 -110

No files matched your search

+21
View File
@@ -92,6 +92,8 @@ impl KeybindingsSpec {
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)] #[schemars(deny_unknown_fields)]
pub struct TuiGlobalKeymap { pub struct TuiGlobalKeymap {
/// Open the shared agent-session overview.
pub open_agents: Option<KeybindingsSpec>,
/// Open the transcript overlay. /// Open the transcript overlay.
pub open_transcript: Option<KeybindingsSpec>, pub open_transcript: Option<KeybindingsSpec>,
/// Open the external editor for the current draft. /// Open the external editor for the current draft.
@@ -367,6 +369,23 @@ pub struct TuiListKeymap {
pub cancel: Option<KeybindingsSpec>, pub cancel: Option<KeybindingsSpec>,
} }
/// Shortcuts specific to the shared agents overview.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(deny_unknown_fields)]
pub struct TuiAgentsKeymap {
/// Search the available agent tasks.
pub search: Option<KeybindingsSpec>,
/// Start composing a new agent task.
pub new_task: Option<KeybindingsSpec>,
/// Rename the selected task.
pub rename: Option<KeybindingsSpec>,
/// Stop the selected running task.
pub stop: Option<KeybindingsSpec>,
/// Toggle grouping tasks by status or project.
pub toggle_grouping: Option<KeybindingsSpec>,
}
/// Approval overlay keybindings. /// Approval overlay keybindings.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
@@ -423,6 +442,8 @@ pub struct TuiKeymap {
#[serde(default)] #[serde(default)]
pub list: TuiListKeymap, pub list: TuiListKeymap,
#[serde(default)] #[serde(default)]
pub agents: TuiAgentsKeymap,
#[serde(default)]
pub approval: TuiApprovalKeymap, pub approval: TuiApprovalKeymap,
} }
+1
View File
@@ -31,6 +31,7 @@ use serde::Serialize;
pub use crate::tui_keymap::KeybindingSpec; pub use crate::tui_keymap::KeybindingSpec;
pub use crate::tui_keymap::KeybindingsSpec; pub use crate::tui_keymap::KeybindingsSpec;
pub use crate::tui_keymap::MAX_FUNCTION_KEY; pub use crate::tui_keymap::MAX_FUNCTION_KEY;
pub use crate::tui_keymap::TuiAgentsKeymap;
pub use crate::tui_keymap::TuiApprovalKeymap; pub use crate::tui_keymap::TuiApprovalKeymap;
pub use crate::tui_keymap::TuiChatKeymap; pub use crate::tui_keymap::TuiChatKeymap;
pub use crate::tui_keymap::TuiComposerKeymap; pub use crate::tui_keymap::TuiComposerKeymap;
+78
View File
@@ -3469,6 +3469,13 @@
} }
], ],
"default": { "default": {
"agents": {
"new_task": null,
"rename": null,
"search": null,
"stop": null,
"toggle_grouping": null
},
"approval": { "approval": {
"approve": null, "approve": null,
"approve_for_prefix": null, "approve_for_prefix": null,
@@ -3514,6 +3521,7 @@
"global": { "global": {
"clear_terminal": null, "clear_terminal": null,
"copy": null, "copy": null,
"open_agents": null,
"open_external_editor": null, "open_external_editor": null,
"open_transcript": null, "open_transcript": null,
"queue": null, "queue": null,
@@ -3724,6 +3732,53 @@
}, },
"type": "object" "type": "object"
}, },
"TuiAgentsKeymap": {
"additionalProperties": false,
"description": "Shortcuts specific to the shared agents overview.",
"properties": {
"new_task": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Start composing a new agent task."
},
"rename": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Rename the selected task."
},
"search": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Search the available agent tasks."
},
"stop": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Stop the selected running task."
},
"toggle_grouping": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Toggle grouping tasks by status or project."
}
},
"type": "object"
},
"TuiApprovalKeymap": { "TuiApprovalKeymap": {
"additionalProperties": false, "additionalProperties": false,
"description": "Approval overlay keybindings.", "description": "Approval overlay keybindings.",
@@ -4044,6 +4099,14 @@
], ],
"description": "Copy the last agent response to the clipboard." "description": "Copy the last agent response to the clipboard."
}, },
"open_agents": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Open the shared agent-session overview."
},
"open_external_editor": { "open_external_editor": {
"allOf": [ "allOf": [
{ {
@@ -4123,6 +4186,20 @@
"additionalProperties": false, "additionalProperties": false,
"description": "Raw keymap configuration from `[tui.keymap]`.\n\nEach context contains action-level overrides. Missing actions inherit from built-in defaults, and selected chat/composer actions can fall back through `global` during runtime resolution.\n\nThis type is intentionally a persistence shape, not the structure used by input handlers. Runtime consumers should resolve it into `RuntimeKeymap` first so precedence, empty-list unbinding, and duplicate-key validation are applied consistently.", "description": "Raw keymap configuration from `[tui.keymap]`.\n\nEach context contains action-level overrides. Missing actions inherit from built-in defaults, and selected chat/composer actions can fall back through `global` during runtime resolution.\n\nThis type is intentionally a persistence shape, not the structure used by input handlers. Runtime consumers should resolve it into `RuntimeKeymap` first so precedence, empty-list unbinding, and duplicate-key validation are applied consistently.",
"properties": { "properties": {
"agents": {
"allOf": [
{
"$ref": "#/definitions/TuiAgentsKeymap"
}
],
"default": {
"new_task": null,
"rename": null,
"search": null,
"stop": null,
"toggle_grouping": null
}
},
"approval": { "approval": {
"allOf": [ "allOf": [
{ {
@@ -4202,6 +4279,7 @@
"default": { "default": {
"clear_terminal": null, "clear_terminal": null,
"copy": null, "copy": null,
"open_agents": null,
"open_external_editor": null, "open_external_editor": null,
"open_transcript": null, "open_transcript": null,
"queue": null, "queue": null,
@@ -14,6 +14,9 @@ use codex_app_server_protocol::ThreadActiveFlag;
use codex_app_server_protocol::ThreadUnsubscribeParams; use codex_app_server_protocol::ThreadUnsubscribeParams;
use codex_app_server_protocol::ThreadUnsubscribeResponse; use codex_app_server_protocol::ThreadUnsubscribeResponse;
use codex_app_server_protocol::ThreadUnsubscribeStatus; use codex_app_server_protocol::ThreadUnsubscribeStatus;
use codex_config::types::KeybindingSpec;
use codex_config::types::KeybindingsSpec;
use codex_config::types::TuiKeymap;
use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::SubAgentSource;
static OVERVIEW_TIMESTAMP: std::sync::LazyLock<i64> = static OVERVIEW_TIMESTAMP: std::sync::LazyLock<i64> =
@@ -273,6 +276,63 @@ async fn embedded_sessions_offer_to_start_a_background_server_without_migrating(
app_server.shutdown().await.expect("shutdown app server"); app_server.shutdown().await.expect("shutdown app server");
} }
#[tokio::test]
async fn filtered_dashboard_actions_use_configured_shortcuts() {
let mut app = make_test_app().await;
let mut keymap = TuiKeymap::default();
keymap.agents.search = Some(KeybindingsSpec::One(KeybindingSpec("f6".to_string())));
keymap.agents.stop = Some(KeybindingsSpec::One(KeybindingSpec("f10".to_string())));
app.keymap = crate::keymap::RuntimeKeymap::from_config(&keymap).expect("runtime keymap");
let first = ThreadId::new();
let second = ThreadId::new();
let (event_tx, mut event_rx) = tokio::sync::mpsc::unbounded_channel();
let mut view = AgentsOverviewView::new(
app.agents_overview_view(
vec![
overview_thread(
first,
/*parent_thread_id*/ None,
"First task",
ThreadStatus::Idle,
),
overview_thread(
second,
/*parent_thread_id*/ None,
"Second task",
ThreadStatus::Active {
active_flags: Vec::new(),
},
),
],
Some(first),
)
.rows
.clone(),
Some(first),
/*exit_on_cancel*/ false,
crate::app_event_sender::AppEventSender::new(event_tx),
app.keymap.clone(),
Arc::default(),
);
view.handle_key_event(KeyEvent::new(KeyCode::F(10), KeyModifiers::NONE));
assert!(event_rx.try_recv().is_err());
assert!(view.handle_paste("Do not dispatch this draft".to_string()));
view.handle_key_event(KeyEvent::new(KeyCode::F(6), KeyModifiers::NONE));
assert!(view.handle_paste("Second task".to_string()));
view.handle_key_event(KeyEvent::new(KeyCode::F(10), KeyModifiers::NONE));
assert!(matches!(
event_rx.try_recv(),
Ok(AppEvent::StopAgentsOverviewThread { thread_id }) if thread_id == second
));
view.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
assert!(matches!(
event_rx.try_recv(),
Ok(AppEvent::SelectAgentsOverviewThread { thread_id }) if thread_id == second
));
assert!(event_rx.try_recv().is_err());
}
#[tokio::test] #[tokio::test]
async fn failed_root_switch_keeps_background_requests_on_the_active_session() -> Result<()> { async fn failed_root_switch_keeps_background_requests_on_the_active_session() -> Result<()> {
let mut app = make_test_app().await; let mut app = make_test_app().await;
+143 -84
View File
@@ -5,7 +5,10 @@ use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender; use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::BottomPaneView; use crate::bottom_pane::BottomPaneView;
use crate::bottom_pane::ViewCompletion; use crate::bottom_pane::ViewCompletion;
use crate::key_hint::KeyBindingListExt;
use crate::key_hint::ShortcutHint;
use crate::key_hint::is_plain_text_key_event; use crate::key_hint::is_plain_text_key_event;
use crate::keymap::AgentsKeymap;
use crate::keymap::KeymapContext; use crate::keymap::KeymapContext;
use crate::keymap::KeymapContextSet; use crate::keymap::KeymapContextSet;
use crate::keymap::ListAction; use crate::keymap::ListAction;
@@ -18,7 +21,6 @@ use codex_app_server_protocol::ThreadStatus;
use codex_protocol::ThreadId; use codex_protocol::ThreadId;
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Constraint; use ratatui::layout::Constraint;
use ratatui::layout::Layout; use ratatui::layout::Layout;
@@ -98,6 +100,7 @@ pub(super) struct AgentsOverviewView {
completion: Option<ViewCompletion>, completion: Option<ViewCompletion>,
app_event_tx: AppEventSender, app_event_tx: AppEventSender,
keymap: ListKeymap, keymap: ListKeymap,
agents_keymap: AgentsKeymap,
} }
impl Drop for AgentsOverviewView { impl Drop for AgentsOverviewView {
@@ -135,6 +138,7 @@ impl AgentsOverviewView {
completion: None, completion: None,
app_event_tx, app_event_tx,
keymap: keymap.list, keymap: keymap.list,
agents_keymap: keymap.agents,
}; };
let visible = view.visible_indices(); let visible = view.visible_indices();
if !visible.contains(&view.selected) { if !visible.contains(&view.selected) {
@@ -419,7 +423,7 @@ impl BottomPaneView for AgentsOverviewView {
} }
fn keymap_contexts(&self) -> KeymapContextSet { fn keymap_contexts(&self) -> KeymapContextSet {
KeymapContextSet::new(KeymapContext::List) KeymapContextSet::new(KeymapContext::List).with(KeymapContext::Agents)
} }
fn completion(&self) -> Option<ViewCompletion> { fn completion(&self) -> Option<ViewCompletion> {
@@ -441,58 +445,69 @@ impl BottomPaneView for AgentsOverviewView {
} }
fn handle_key_event(&mut self, key: KeyEvent) { fn handle_key_event(&mut self, key: KeyEvent) {
if key.modifiers == KeyModifiers::CONTROL if key.code == KeyCode::Backspace
&& matches!(key.code, KeyCode::Char('f' | 'n' | 'r' | 's' | 'x')) && key.modifiers.is_empty()
&& self.keymap.action_for(key).is_none()
{ {
match key.code { self.edit_input(|input| {
KeyCode::Char('s') => { input.pop();
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner); });
state.status_grouping = !state.status_grouping; return;
}
if is_plain_text_key_event(key)
&& let KeyCode::Char(character) = key.code
{
self.edit_input(|input| input.push(character));
return;
}
if self.agents_keymap.toggle_grouping.is_pressed(key) {
let mut state = self.state();
state.status_grouping = !state.status_grouping;
return;
}
if self.agents_keymap.search.is_pressed(key) {
let mut state = self.state();
if !state.renaming {
state.searching = !state.searching;
if !state.searching {
state.search.clear();
} }
KeyCode::Char('f') => { }
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner); return;
if !state.renaming { }
state.searching = !state.searching; if self.agents_keymap.new_task.is_pressed(key) {
if !state.searching { let mut state = self.state();
state.search.clear(); state.search.clear();
} state.searching = false;
} state.renaming = false;
} state.input.clear();
KeyCode::Char('n') => { return;
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner); }
if self.agents_keymap.rename.is_pressed(key) {
if let Some(row) = self.selected_row() {
let mut state = self.state();
if state.input.is_empty() {
state.input = row.thread.name.clone().unwrap_or_default();
state.search.clear(); state.search.clear();
state.searching = false; state.searching = false;
state.renaming = false; state.renaming = true;
state.input.clear();
} }
KeyCode::Char('r') => { }
if let Some(row) = self.selected_row() { return;
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner); }
if state.input.is_empty() { if self.agents_keymap.stop.is_pressed(key) {
state.input = row.thread.name.clone().unwrap_or_default(); if let Some(row) = self.selected_row()
state.search.clear(); && matches!(row.thread.status, ThreadStatus::Active { .. })
state.searching = false; {
state.renaming = true; self.app_event_tx.send(AppEvent::StopAgentsOverviewThread {
} thread_id: row.thread_id,
} });
}
KeyCode::Char('x') => {
if let Some(row) = self.selected_row()
&& matches!(row.thread.status, ThreadStatus::Active { .. })
{
self.app_event_tx.send(AppEvent::StopAgentsOverviewThread {
thread_id: row.thread_id,
});
}
}
_ => {}
} }
return; return;
} }
if !is_plain_text_key_event(key) if let Some(action) = self.keymap.action_for(key) {
&& let Some(action) = self.keymap.action_for(key)
{
if self.state().renaming if self.state().renaming
&& matches!(action, ListAction::JumpTop | ListAction::JumpBottom) && matches!(action, ListAction::JumpTop | ListAction::JumpBottom)
{ {
@@ -532,21 +547,10 @@ impl BottomPaneView for AgentsOverviewView {
} }
ListAction::MoveLeft | ListAction::MoveRight => {} ListAction::MoveLeft | ListAction::MoveRight => {}
} }
return; } else if key.code == KeyCode::Backspace {
} self.edit_input(|input| {
input.pop();
match key.code { });
KeyCode::Backspace => {
self.edit_input(|input| {
input.pop();
});
}
KeyCode::Char(character)
if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT =>
{
self.edit_input(|input| input.push(character));
}
_ => {}
} }
} }
} }
@@ -650,30 +654,85 @@ impl Renderable for AgentsOverviewView {
let input = &input[visible_start..]; let input = &input[visible_start..];
Line::from(vec![label.cyan().bold(), input.into(), placeholder.dim()]) Line::from(vec![label.cyan().bold(), input.into(), placeholder.dim()])
.render(inset(prompt), buf); .render(inset(prompt), buf);
let stop_hint = if self let list_hint = |action| {
.selected_row() self.keymap.primary_hint(action).filter(|hint| {
.is_some_and(|row| matches!(row.thread.status, ThreadStatus::Active { .. })) !matches!(hint, ShortcutHint::Single(binding)
{ if is_plain_text_key_event(KeyEvent::new(
"ctrl+x".bold() binding.parts().0,
} else { binding.parts().1,
"ctrl+x".dim() )) || [
&self.agents_keymap.search,
&self.agents_keymap.new_task,
&self.agents_keymap.rename,
&self.agents_keymap.stop,
&self.agents_keymap.toggle_grouping,
]
.into_iter()
.any(|bindings| bindings.contains(binding)))
})
}; };
Line::from(vec![ let navigation_hints = [ListAction::MoveUp, ListAction::MoveDown]
"↑↓".bold(), .into_iter()
" navigate ".dim(), .filter_map(list_hint)
"enter".bold(), .map(|hint| hint.display_label().replace(" + ", "+"))
" open ".dim(), .collect::<Vec<_>>();
"ctrl+f".bold(), let navigation_hint = navigation_hints.join(
" search ".dim(), if navigation_hints
"ctrl+s".bold(), .iter()
" group ".dim(), .all(|hint| hint.chars().count() == 1)
"ctrl+r".bold(), {
" rename ".dim(), ""
stop_hint, } else {
" stop ".dim(), " "
"esc".bold(), },
" back".dim(), );
]) let mut footer_spans = Vec::new();
.render(inset(footer), buf); if !navigation_hint.is_empty() {
footer_spans.extend([navigation_hint.bold(), " navigate ".dim()]);
}
let mut add_hint = |hint: Option<ShortcutHint>, label: &'static str, enabled: bool| {
if let Some(hint) = hint {
let key = hint.display_label().replace(" + ", "+");
footer_spans.push(if enabled { key.bold() } else { key.dim() });
footer_spans.push(format!(" {label} ").dim());
}
};
add_hint(list_hint(ListAction::Accept), "open", true);
add_hint(
self.agents_keymap
.primary_hint("search", &self.agents_keymap.search),
"search",
true,
);
add_hint(
self.agents_keymap
.primary_hint("toggle_grouping", &self.agents_keymap.toggle_grouping),
"group",
true,
);
add_hint(
self.agents_keymap
.primary_hint("rename", &self.agents_keymap.rename),
"rename",
true,
);
add_hint(
self.agents_keymap
.primary_hint("stop", &self.agents_keymap.stop),
"stop",
self.selected_row()
.is_some_and(|row| matches!(row.thread.status, ThreadStatus::Active { .. })),
);
add_hint(list_hint(ListAction::Cancel), "back", true);
let footer_area = inset(footer);
let mut footer_line: Line = footer_spans.into();
if footer_line.width() > usize::from(footer_area.width) {
for span in &mut footer_line.spans {
if span.content.ends_with(" ") {
span.content.to_mut().pop();
}
}
}
footer_line.render(footer_area, buf);
} }
} }
+5
View File
@@ -342,6 +342,11 @@ impl App {
return; return;
} }
if app_keymap_shortcuts_available && self.keymap.app.open_agents.is_pressed(key_event) {
self.open_agents_overview(app_server);
return;
}
if app_keymap_shortcuts_available && self.keymap.app.open_transcript.is_pressed(key_event) { if app_keymap_shortcuts_available && self.keymap.app.open_transcript.is_pressed(key_event) {
self.scrollback_has_older_history = self self.scrollback_has_older_history = self
.chat_widget .chat_widget
+287
View File
@@ -69,11 +69,14 @@ pub(crate) struct RuntimeKeymap {
pub(crate) vim_text_object: VimTextObjectKeymap, pub(crate) vim_text_object: VimTextObjectKeymap,
pub(crate) pager: PagerKeymap, pub(crate) pager: PagerKeymap,
pub(crate) list: ListKeymap, pub(crate) list: ListKeymap,
pub(crate) agents: AgentsKeymap,
pub(crate) approval: ApprovalKeymap, pub(crate) approval: ApprovalKeymap,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct AppKeymap { pub(crate) struct AppKeymap {
/// Open the daemon-wide agent-session overview.
pub(crate) open_agents: Vec<KeyBinding>,
/// Open transcript overlay. /// Open transcript overlay.
pub(crate) open_transcript: Vec<KeyBinding>, pub(crate) open_transcript: Vec<KeyBinding>,
/// Open external editor for the current draft. /// Open external editor for the current draft.
@@ -352,6 +355,28 @@ impl ListKeymap {
} }
} }
/// Task-management shortcuts specific to the shared agents dashboard.
#[derive(Clone, Debug)]
pub(crate) struct AgentsKeymap {
pub(crate) search: Vec<KeyBinding>,
pub(crate) new_task: Vec<KeyBinding>,
pub(crate) rename: Vec<KeyBinding>,
pub(crate) stop: Vec<KeyBinding>,
pub(crate) toggle_grouping: Vec<KeyBinding>,
chord_hints: Arc<RuntimeChordKeymap>,
}
impl AgentsKeymap {
pub(crate) fn primary_hint(
&self,
action: &'static str,
bindings: &[KeyBinding],
) -> Option<ShortcutHint> {
let action_id = keymap_action_id(KeymapContext::Agents.config_name(), action)?;
self.chord_hints.primary_hint(action_id, bindings)
}
}
/// Approval modal keybindings. /// Approval modal keybindings.
/// ///
/// This covers both selection actions and the "open details fullscreen" escape /// This covers both selection actions and the "open details fullscreen" escape
@@ -541,8 +566,25 @@ impl RuntimeKeymap {
|| configured_context_alias_is_used(&keymap.list, alias) || configured_context_alias_is_used(&keymap.list, alias)
|| configured_context_alias_is_used(&keymap.approval, 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 { 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_transcript: resolve_bindings( open_transcript: resolve_bindings(
keymap.global.open_transcript.as_ref(), keymap.global.open_transcript.as_ref(),
&defaults.app.open_transcript, &defaults.app.open_transcript,
@@ -934,6 +976,15 @@ impl RuntimeKeymap {
chord_hints: Arc::clone(&chords), chord_hints: Arc::clone(&chords),
}; };
let mut agents = AgentsKeymap {
search: resolve_local!(keymap, defaults, agents, search),
new_task: resolve_local!(keymap, defaults, agents, new_task),
rename: resolve_local!(keymap, defaults, agents, rename),
stop: resolve_local!(keymap, defaults, agents, stop),
toggle_grouping: resolve_local!(keymap, defaults, agents, toggle_grouping),
chord_hints: Arc::clone(&chords),
};
let approval = ApprovalKeymap { let approval = ApprovalKeymap {
open_fullscreen: resolve_local!(keymap, defaults, approval, open_fullscreen), open_fullscreen: resolve_local!(keymap, defaults, approval, open_fullscreen),
open_thread: resolve_local!(keymap, defaults, approval, open_thread), open_thread: resolve_local!(keymap, defaults, approval, open_thread),
@@ -951,6 +1002,10 @@ impl RuntimeKeymap {
let list_accept = resolve_local!(keymap, defaults, list, accept); let list_accept = resolve_local!(keymap, defaults, list, accept);
let list_cancel = resolve_local!(keymap, defaults, list, cancel); let list_cancel = resolve_local!(keymap, defaults, list, cancel);
let configured_bindings_to_preserve = configured_bindings_to_preserve([ let configured_bindings_to_preserve = configured_bindings_to_preserve([
(
keymap.global.open_agents.as_ref(),
app.open_agents.as_slice(),
),
( (
keymap.global.open_transcript.as_ref(), keymap.global.open_transcript.as_ref(),
app.open_transcript.as_slice(), app.open_transcript.as_slice(),
@@ -1056,6 +1111,26 @@ impl RuntimeKeymap {
chord_hints: Arc::clone(&chords), chord_hints: Arc::clone(&chords),
}; };
for (configured, bindings) in [
(keymap.agents.search.as_ref(), &mut agents.search),
(keymap.agents.new_task.as_ref(), &mut agents.new_task),
(keymap.agents.rename.as_ref(), &mut agents.rename),
(keymap.agents.stop.as_ref(), &mut agents.stop),
(
keymap.agents.toggle_grouping.as_ref(),
&mut agents.toggle_grouping,
),
] {
if configured.is_none() {
bindings.retain(|binding| {
!chords.bindings.iter().any(|chord| {
chord.action.context == KeymapContext::List
&& chord.chord.prefix == *binding
})
});
}
}
let mut resolved = Self { let mut resolved = Self {
app, app,
chords, chords,
@@ -1067,6 +1142,7 @@ impl RuntimeKeymap {
vim_text_object, vim_text_object,
pager, pager,
list, list,
agents,
approval, approval,
}; };
@@ -1095,6 +1171,7 @@ impl RuntimeKeymap {
fn built_in_defaults() -> Self { fn built_in_defaults() -> Self {
Self { Self {
app: AppKeymap { app: AppKeymap {
open_agents: default_bindings![alt(KeyCode::Char('a'))],
open_transcript: default_bindings![ctrl(KeyCode::Char('t'))], open_transcript: default_bindings![ctrl(KeyCode::Char('t'))],
open_external_editor: default_bindings![ctrl(KeyCode::Char('g'))], open_external_editor: default_bindings![ctrl(KeyCode::Char('g'))],
copy: default_bindings![ctrl(KeyCode::Char('o'))], copy: default_bindings![ctrl(KeyCode::Char('o'))],
@@ -1323,6 +1400,14 @@ impl RuntimeKeymap {
cancel: default_bindings![plain(KeyCode::Esc)], cancel: default_bindings![plain(KeyCode::Esc)],
chord_hints: Arc::default(), chord_hints: Arc::default(),
}, },
agents: AgentsKeymap {
search: default_bindings![ctrl(KeyCode::Char('f'))],
new_task: default_bindings![ctrl(KeyCode::Char('n'))],
rename: default_bindings![ctrl(KeyCode::Char('r'))],
stop: default_bindings![ctrl(KeyCode::Char('x'))],
toggle_grouping: default_bindings![ctrl(KeyCode::Char('s'))],
chord_hints: Arc::default(),
},
approval: ApprovalKeymap { approval: ApprovalKeymap {
open_fullscreen: default_bindings![ open_fullscreen: default_bindings![
ctrl(KeyCode::Char('a')), ctrl(KeyCode::Char('a')),
@@ -1353,6 +1438,25 @@ impl RuntimeKeymap {
/// 2. Contexts with hard-coded sequence behavior, such as edit-previous /// 2. Contexts with hard-coded sequence behavior, such as edit-previous
/// backtracking, intentionally stay outside this configurable keymap. /// backtracking, intentionally stay outside this configurable keymap.
fn validate_conflicts(&self) -> Result<(), String> { fn validate_conflicts(&self) -> Result<(), String> {
#[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(),
);
}
if self.app.open_agents.iter().any(|binding| {
matches!(binding.parts(), (KeyCode::Char(_), modifiers)
if crate::key_hint::is_altgr(modifiers))
}) {
return Err(
"tui.keymap.global.open_agents: AltGr characters are reserved for text input"
.to_string(),
);
}
let mut side_toggle_bindings = self.app.toggle_side_conversation.clone(); let mut side_toggle_bindings = self.app.toggle_side_conversation.clone();
let slash_binding = key_hint::ctrl(KeyCode::Char('/')); let slash_binding = key_hint::ctrl(KeyCode::Char('/'));
let legacy_slash_binding = key_hint::ctrl(KeyCode::Char('7')); let legacy_slash_binding = key_hint::ctrl(KeyCode::Char('7'));
@@ -1365,6 +1469,7 @@ impl RuntimeKeymap {
validate_unique( validate_unique(
"app", "app",
[ [
("open_agents", self.app.open_agents.as_slice()),
("open_transcript", self.app.open_transcript.as_slice()), ("open_transcript", self.app.open_transcript.as_slice()),
( (
"open_external_editor", "open_external_editor",
@@ -1409,6 +1514,7 @@ impl RuntimeKeymap {
validate_no_reserved( validate_no_reserved(
"main", "main",
[ [
("open_agents", self.app.open_agents.as_slice()),
("open_transcript", self.app.open_transcript.as_slice()), ("open_transcript", self.app.open_transcript.as_slice()),
( (
"open_external_editor", "open_external_editor",
@@ -1459,6 +1565,7 @@ impl RuntimeKeymap {
validate_no_shadow_with_allowed_overlaps( validate_no_shadow_with_allowed_overlaps(
"app", "app",
[ [
("open_agents", self.app.open_agents.as_slice()),
("open_transcript", self.app.open_transcript.as_slice()), ("open_transcript", self.app.open_transcript.as_slice()),
( (
"open_external_editor", "open_external_editor",
@@ -1524,6 +1631,7 @@ impl RuntimeKeymap {
validate_no_shadow_with_allowed_overlaps( validate_no_shadow_with_allowed_overlaps(
"main", "main",
[ [
("open_agents", self.app.open_agents.as_slice()),
("open_transcript", self.app.open_transcript.as_slice()), ("open_transcript", self.app.open_transcript.as_slice()),
( (
"open_external_editor", "open_external_editor",
@@ -1817,6 +1925,35 @@ impl RuntimeKeymap {
], ],
)?; )?;
let agents_bindings = [
("search", self.agents.search.as_slice()),
("new_task", self.agents.new_task.as_slice()),
("rename", self.agents.rename.as_slice()),
("stop", self.agents.stop.as_slice()),
("toggle_grouping", self.agents.toggle_grouping.as_slice()),
];
validate_unique("agents", agents_bindings)?;
validate_no_reserved("agents", agents_bindings, MAIN_RESERVED_BINDINGS, [])?;
for (action, bindings) in agents_bindings {
#[cfg(unix)]
if bindings.contains(&key_hint::ctrl(KeyCode::Char('z'))) {
return Err(format!(
"tui.keymap.agents.{action}: ctrl-z is reserved for suspend"
));
}
if bindings.iter().any(|binding| {
matches!(binding.parts(), (KeyCode::Char(_), modifiers)
if modifiers.is_empty()
|| modifiers == KeyModifiers::SHIFT
|| crate::key_hint::is_altgr(modifiers))
|| binding.parts() == (KeyCode::Backspace, KeyModifiers::NONE)
}) {
return Err(format!(
"tui.keymap.agents.{action}: printable keys and backspace are reserved for task input"
));
}
}
validate_unique( validate_unique(
"approval", "approval",
[ [
@@ -2891,6 +3028,156 @@ mod tests {
expect_conflict(&keymap, "clear_terminal", "toggle_fast_mode"); expect_conflict(&keymap, "clear_terminal", "toggle_fast_mode");
} }
#[test]
fn agents_overview_can_be_remapped_and_rejects_conflicts() {
let mut keymap = TuiKeymap::default();
keymap.global.open_agents = Some(one("f12"));
keymap.agents.search = Some(one("f6"));
keymap.agents.new_task = Some(one("f7"));
keymap.agents.rename = Some(one("f9"));
keymap.agents.stop = Some(one("f10"));
keymap.agents.toggle_grouping = Some(one("f8"));
let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");
assert_eq!(
(
runtime.app.open_agents,
runtime.agents.search,
runtime.agents.new_task,
runtime.agents.rename,
runtime.agents.stop,
runtime.agents.toggle_grouping,
),
(
vec![key_hint::plain(KeyCode::F(12))],
vec![key_hint::plain(KeyCode::F(6))],
vec![key_hint::plain(KeyCode::F(7))],
vec![key_hint::plain(KeyCode::F(9))],
vec![key_hint::plain(KeyCode::F(10))],
vec![key_hint::plain(KeyCode::F(8))],
)
);
keymap.agents.toggle_grouping = Some(one("right"));
let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");
assert!(
runtime
.list
.move_right
.contains(&key_hint::plain(KeyCode::Right))
);
keymap.agents.toggle_grouping = Some(one("f8"));
keymap.agents.stop = Some(one("f9"));
expect_conflict(&keymap, "rename", "stop");
keymap.agents.stop = Some(one("f10"));
keymap.global.open_agents = Some(one("ctrl-t"));
expect_conflict(&keymap, "open_agents", "open_transcript");
keymap.global.open_agents = Some(one("f12"));
keymap.agents.stop = Some(one("ctrl-c"));
expect_conflict(&keymap, "stop", "fixed.interrupt_or_quit");
#[cfg(unix)]
{
keymap.agents.stop = Some(one("ctrl-z"));
assert!(
RuntimeKeymap::from_config(&keymap)
.unwrap_err()
.contains("suspend")
);
keymap.agents.stop = Some(one("f10"));
keymap.global.open_agents = Some(one("ctrl-z"));
assert!(
RuntimeKeymap::from_config(&keymap)
.unwrap_err()
.contains("suspend")
);
keymap.global.open_agents = Some(one("f12"));
}
keymap.agents.stop = Some(one("f10"));
keymap.agents.search = Some(one("right"));
let runtime =
RuntimeKeymap::from_config(&keymap).expect("dashboard shortcuts take priority");
assert_eq!(runtime.agents.search, vec![key_hint::plain(KeyCode::Right)]);
assert_eq!(
runtime.list.move_right,
RuntimeKeymap::defaults().list.move_right
);
keymap.agents.search = None;
keymap.agents.rename = None;
keymap.list.move_right = Some(one("ctrl-r"));
let runtime = RuntimeKeymap::from_config(&keymap).expect("list shortcut remains usable");
assert_eq!(
runtime.agents.rename,
vec![key_hint::ctrl(KeyCode::Char('r'))]
);
keymap.agents.search = Some(one("s"));
assert!(
RuntimeKeymap::from_config(&keymap)
.expect_err("printable shortcut is reserved for input")
.contains("printable keys")
);
keymap.agents.search = None;
keymap.agents.stop = Some(one("backspace"));
assert!(
RuntimeKeymap::from_config(&keymap)
.expect_err("backspace is reserved for task input")
.contains("backspace")
);
#[cfg(windows)]
{
keymap.agents.stop = Some(one("ctrl-alt-@"));
assert!(RuntimeKeymap::from_config(&keymap).is_err());
keymap.agents.stop = Some(one("f10"));
keymap.global.open_agents = Some(one("ctrl-alt-@"));
assert!(
RuntimeKeymap::from_config(&keymap)
.unwrap_err()
.contains("AltGr")
);
keymap.global.open_agents = Some(one("f12"));
}
keymap.agents.stop = Some(one("backspace f10"));
assert!(
RuntimeKeymap::from_config(&keymap)
.expect_err("backspace is reserved for task input")
.contains("backspace")
);
}
#[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] #[test]
fn toggle_side_conversation_can_be_remapped_and_rejects_conflicts() { fn toggle_side_conversation_can_be_remapped_and_rejects_conflicts() {
let mut keymap = TuiKeymap::default(); let mut keymap = TuiKeymap::default();
+12
View File
@@ -22,6 +22,7 @@ pub(crate) enum KeymapContext {
VimTextObject, VimTextObject,
Pager, Pager,
List, List,
Agents,
Approval, Approval,
} }
@@ -37,6 +38,7 @@ impl KeymapContext {
Self::VimTextObject => "vim_text_object", Self::VimTextObject => "vim_text_object",
Self::Pager => "pager", Self::Pager => "pager",
Self::List => "list", Self::List => "list",
Self::Agents => "agents",
Self::Approval => "approval", Self::Approval => "approval",
} }
} }
@@ -57,6 +59,8 @@ impl KeymapContext {
(self, other), (self, other),
(Self::List, Self::Approval) (Self::List, Self::Approval)
| (Self::Approval, Self::List) | (Self::Approval, Self::List)
| (Self::List, Self::Agents)
| (Self::Agents, Self::List)
| (Self::Chat, Self::List) | (Self::Chat, Self::List)
| (Self::List, Self::Chat) | (Self::List, Self::Chat)
) || self.is_shared_main() && other.is_main_editor() ) || self.is_shared_main() && other.is_main_editor()
@@ -221,6 +225,7 @@ macro_rules! define_runtime_action_bindings {
define_runtime_action_bindings! { define_runtime_action_bindings! {
"global" => Global, app, global [ "global" => Global, app, global [
open_agents,
open_transcript, open_transcript,
open_external_editor, open_external_editor,
copy, copy,
@@ -340,6 +345,13 @@ define_runtime_action_bindings! {
accept, accept,
cancel, cancel,
], ],
"agents" => Agents, agents, agents [
search,
new_task,
rename,
stop,
toggle_grouping,
],
"approval" => Approval, approval, approval [ "approval" => Approval, approval, approval [
open_fullscreen, open_fullscreen,
open_thread, open_thread,
+11 -1
View File
@@ -67,6 +67,7 @@ const fn context_bit(context: KeymapContext) -> u16 {
KeymapContext::Pager => 7, KeymapContext::Pager => 7,
KeymapContext::List => 8, KeymapContext::List => 8,
KeymapContext::Approval => 9, KeymapContext::Approval => 9,
KeymapContext::Agents => 10,
} }
} }
@@ -441,6 +442,15 @@ fn validate_reserved_strokes(binding: &RuntimeChordBinding) -> Result<(), String
)); ));
} }
if binding.action.context == KeymapContext::Agents
&& binding.chord.prefix.parts() == (KeyCode::Backspace, KeyModifiers::NONE)
{
return Err(format!(
"Invalid `{path}` = `{}`: `backspace` is reserved for editing task input.",
binding.spec
));
}
#[cfg(unix)] #[cfg(unix)]
if strokes.contains(&crate::key_hint::ctrl(KeyCode::Char('z')).parts()) { if strokes.contains(&crate::key_hint::ctrl(KeyCode::Char('z')).parts()) {
return Err(format!( return Err(format!(
@@ -460,7 +470,7 @@ Choose a different chord and retry.",
| KeymapContext::VimOperator | KeymapContext::VimOperator
| KeymapContext::VimTextObject => MAIN_RESERVED_BINDINGS, | KeymapContext::VimTextObject => MAIN_RESERVED_BINDINGS,
KeymapContext::List => LIST_RESERVED_BINDINGS, KeymapContext::List => LIST_RESERVED_BINDINGS,
KeymapContext::Approval => &LIST_RESERVED_BINDINGS[..1], KeymapContext::Agents | KeymapContext::Approval => &LIST_RESERVED_BINDINGS[..1],
}; };
if let Some((reserved_action, _)) = reserved.iter().find(|(_, reserved)| { if let Some((reserved_action, _)) = reserved.iter().find(|(_, reserved)| {
binding.chord.prefix.parts() == reserved.parts() binding.chord.prefix.parts() == reserved.parts()
-1
View File
@@ -986,7 +986,6 @@ mod tests {
.join(".") .join(".")
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!( assert_eq!(
actions, actions,
vec![ vec![
+12
View File
@@ -84,6 +84,7 @@ impl KeymapActionDescriptor {
#[rustfmt::skip] #[rustfmt::skip]
pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
action("global", "Global", "open_agents", "Open the shared agent-session overview."),
action("global", "Global", "open_transcript", "Open the transcript overlay."), action("global", "Global", "open_transcript", "Open the transcript overlay."),
action("global", "Global", "open_external_editor", "Open the current draft in an external editor."), action("global", "Global", "open_external_editor", "Open the current draft in an external editor."),
action("global", "Global", "copy", "Copy the last agent response to the clipboard."), action("global", "Global", "copy", "Copy the last agent response to the clipboard."),
@@ -186,6 +187,11 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
action("list", "List", "jump_bottom", "Jump to the last list item."), action("list", "List", "jump_bottom", "Jump to the last list item."),
action("list", "List", "accept", "Accept the current list selection."), action("list", "List", "accept", "Accept the current list selection."),
action("list", "List", "cancel", "Cancel and close selection views."), action("list", "List", "cancel", "Cancel and close selection views."),
action("agents", "Agents", "search", "Search the available agent tasks."),
action("agents", "Agents", "new_task", "Start composing a new agent task."),
action("agents", "Agents", "rename", "Rename the selected task."),
action("agents", "Agents", "stop", "Stop the selected running task."),
action("agents", "Agents", "toggle_grouping", "Group tasks by status or project."),
action("approval", "Approval", "open_fullscreen", "Open approval details fullscreen."), action("approval", "Approval", "open_fullscreen", "Open approval details fullscreen."),
action("approval", "Approval", "open_thread", "Open the approval source thread when available."), action("approval", "Approval", "open_thread", "Open the approval source thread when available."),
action("approval", "Approval", "approve", "Approve the primary option."), action("approval", "Approval", "approve", "Approve the primary option."),
@@ -228,6 +234,7 @@ pub(super) fn binding_slot<'a>(
action: &str, action: &str,
) -> Option<&'a mut Option<KeybindingsSpec>> { ) -> Option<&'a mut Option<KeybindingsSpec>> {
match (context, action) { match (context, action) {
("global", "open_agents") => Some(&mut keymap.global.open_agents),
("global", "open_transcript") => Some(&mut keymap.global.open_transcript), ("global", "open_transcript") => Some(&mut keymap.global.open_transcript),
("global", "open_external_editor") => Some(&mut keymap.global.open_external_editor), ("global", "open_external_editor") => Some(&mut keymap.global.open_external_editor),
("global", "copy") => Some(&mut keymap.global.copy), ("global", "copy") => Some(&mut keymap.global.copy),
@@ -330,6 +337,11 @@ pub(super) fn binding_slot<'a>(
("list", "jump_bottom") => Some(&mut keymap.list.jump_bottom), ("list", "jump_bottom") => Some(&mut keymap.list.jump_bottom),
("list", "accept") => Some(&mut keymap.list.accept), ("list", "accept") => Some(&mut keymap.list.accept),
("list", "cancel") => Some(&mut keymap.list.cancel), ("list", "cancel") => Some(&mut keymap.list.cancel),
("agents", "search") => Some(&mut keymap.agents.search),
("agents", "new_task") => Some(&mut keymap.agents.new_task),
("agents", "rename") => Some(&mut keymap.agents.rename),
("agents", "stop") => Some(&mut keymap.agents.stop),
("agents", "toggle_grouping") => Some(&mut keymap.agents.toggle_grouping),
("approval", "open_fullscreen") => Some(&mut keymap.approval.open_fullscreen), ("approval", "open_fullscreen") => Some(&mut keymap.approval.open_fullscreen),
("approval", "open_thread") => Some(&mut keymap.approval.open_thread), ("approval", "open_thread") => Some(&mut keymap.approval.open_thread),
("approval", "approve") => Some(&mut keymap.approval.approve), ("approval", "approve") => Some(&mut keymap.approval.approve),
+6
View File
@@ -112,6 +112,12 @@ const KEYMAP_CONTEXT_TABS: &[KeymapContextTab] = &[
description: "Pager and selection-list navigation shortcuts.", description: "Pager and selection-list navigation shortcuts.",
contexts: &["pager", "list"], contexts: &["pager", "list"],
}, },
KeymapContextTab {
id: "agents-shortcuts",
label: "Agents",
description: "Shared agents dashboard shortcuts.",
contexts: &["agents"],
},
KeymapContextTab { KeymapContextTab {
id: "approval-shortcuts", id: "approval-shortcuts",
label: "Approval", label: "Approval",
@@ -2,6 +2,7 @@
source: tui/src/keymap_setup.rs source: tui/src/keymap_setup.rs
expression: snapshot expression: snapshot
--- ---
Open Agents | alt-a | Global open_agents Open Agents Open the shared agent-session overview. alt-a Default
Open Transcript | ctrl-t | Global open_transcript Open Transcript Open the transcript overlay. ctrl-t 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 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 Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard. ctrl-o Default
@@ -13,4 +14,3 @@ Interrupt Turn | esc | Chat interrupt_turn Interrupt Turn Interrupt the active t
Decrease Reasoning Effort | alt-,, shift-down | Chat decrease_reasoning_effort Decrease Reasoning Effort Decrease reasoning effort. alt-,, shift-down Default Decrease Reasoning Effort | alt-,, shift-down | Chat decrease_reasoning_effort Decrease Reasoning Effort Decrease reasoning effort. alt-,, shift-down Default
Increase Reasoning Effort | alt-., shift-up | Chat increase_reasoning_effort Increase Reasoning Effort Increase reasoning effort. alt-., shift-up Default Increase Reasoning Effort | alt-., shift-up | Chat increase_reasoning_effort Increase Reasoning Effort Increase reasoning effort. alt-., shift-up Default
Edit Queued Message | alt-up, shift-left | Chat edit_queued_message Edit Queued Message Edit the most recently queued message. alt-up, shift-left Default Edit Queued Message | alt-up, shift-left | Chat edit_queued_message Edit Queued Message Edit the most recently queued message. alt-up, shift-left Default
Submit | enter | Composer submit Submit Submit the current composer draft. enter Default
@@ -5,18 +5,18 @@ expression: "render_picker(params, 120)"
Keymap Keymap
All configurable shortcuts. All configurable shortcuts.
109 actions, 1 customized, 2 unbound. 115 actions, 1 customized, 2 unbound.
[All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Approval Debug [All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Agents Approval Debug
Type to search shortcuts Type to search shortcuts
› Global Open Transcript ctrl-t › Global Open Agents alt-a
Global Open Transcript ctrl-t
Global Open External Editor ctrl-g Global Open External Editor ctrl-g
Global Copy ctrl-o Global Copy ctrl-o
Global Clear Terminal ctrl-l Global Clear Terminal ctrl-l
Global - Toggle Vim Mode unbound Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r Global Toggle Raw Output alt-r
Global Toggle Side Conversation ctrl-/ Global Toggle Side Conversation ctrl-/
Chat Interrupt Turn esc
left/right group · enter edit shortcut · * custom · - unbound · esc close left/right group · enter edit shortcut · * custom · - unbound · esc close
@@ -5,18 +5,18 @@ expression: "render_picker(params, 120)"
Keymap Keymap
All configurable shortcuts. All configurable shortcuts.
110 actions, 0 customized, 3 unbound. 116 actions, 0 customized, 3 unbound.
[All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Approval Debug [All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Agents Approval Debug
Type to search shortcuts Type to search shortcuts
› Global Open Transcript ctrl-t › Global Open Agents alt-a
Global Open Transcript ctrl-t
Global Open External Editor ctrl-g Global Open External Editor ctrl-g
Global Copy ctrl-o Global Copy ctrl-o
Global Clear Terminal ctrl-l Global Clear Terminal ctrl-l
Global - Toggle Vim Mode unbound Global - Toggle Vim Mode unbound
Global - Toggle Fast Mode unbound Global - Toggle Fast Mode unbound
Global Toggle Raw Output alt-r Global Toggle Raw Output alt-r
Global Toggle Side Conversation ctrl-/
left/right group · enter edit shortcut · * custom · - unbound · esc close left/right group · enter edit shortcut · * custom · - unbound · esc close
@@ -2,17 +2,19 @@
source: tui/src/keymap_setup.rs source: tui/src/keymap_setup.rs
expression: snapshot expression: snapshot
--- ---
tab: All (109 selectable) tab: All (115 selectable)
tab: Common (20 selectable) tab: Common (20 selectable)
tab: Customized (0) (0 selectable) tab: Customized (0) (0 selectable)
tab: Unbound (2) (2 selectable) tab: Unbound (2) (2 selectable)
tab: App (11 selectable) tab: App (12 selectable)
tab: Composer (5 selectable) tab: Composer (5 selectable)
tab: Editor (17 selectable) tab: Editor (17 selectable)
tab: Vim (48 selectable) tab: Vim (48 selectable)
tab: Navigation (20 selectable) tab: Navigation (20 selectable)
tab: Agents (5 selectable)
tab: Approval (8 selectable) tab: Approval (8 selectable)
tab: Debug (1 selectable) tab: Debug (1 selectable)
Open Agents | alt-a | Global open_agents Open Agents Open the shared agent-session overview. alt-a Default
Open Transcript | ctrl-t | Global open_transcript Open Transcript Open the transcript overlay. ctrl-t 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 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 Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard. ctrl-o Default
@@ -24,4 +26,3 @@ Interrupt Turn | esc | Chat interrupt_turn Interrupt Turn Interrupt the active t
Decrease Reasoning Effort | alt-,, shift-down | Chat decrease_reasoning_effort Decrease Reasoning Effort Decrease reasoning effort. alt-,, shift-down Default Decrease Reasoning Effort | alt-,, shift-down | Chat decrease_reasoning_effort Decrease Reasoning Effort Decrease reasoning effort. alt-,, shift-down Default
Increase Reasoning Effort | alt-., shift-up | Chat increase_reasoning_effort Increase Reasoning Effort Increase reasoning effort. alt-., shift-up Default Increase Reasoning Effort | alt-., shift-up | Chat increase_reasoning_effort Increase Reasoning Effort Increase reasoning effort. alt-., shift-up Default
Edit Queued Message | alt-up, shift-left | Chat edit_queued_message Edit Queued Message Edit the most recently queued message. alt-up, shift-left Default Edit Queued Message | alt-up, shift-left | Chat edit_queued_message Edit Queued Message Edit the most recently queued message. alt-up, shift-left Default
Submit | enter | Composer submit Submit Submit the current composer draft. enter Default
@@ -5,18 +5,18 @@ expression: "render_picker(params, 120)"
Keymap Keymap
All configurable shortcuts. All configurable shortcuts.
109 actions, 1 customized, 2 unbound. 115 actions, 1 customized, 2 unbound.
[All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Approval Debug [All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Agents Approval Debug
Type to search shortcuts Type to search shortcuts
› Global * Open Transcript ctrl-t, ctrl-x ctrl-t › Global Open Agents alt-a
Global * Open Transcript ctrl-t, ctrl-x ctrl-t
Global Open External Editor ctrl-g Global Open External Editor ctrl-g
Global Copy ctrl-o Global Copy ctrl-o
Global Clear Terminal ctrl-l Global Clear Terminal ctrl-l
Global - Toggle Vim Mode unbound Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r Global Toggle Raw Output alt-r
Global Toggle Side Conversation ctrl-/ Global Toggle Side Conversation ctrl-/
Chat Interrupt Turn esc
left/right group · enter edit shortcut · * custom · - unbound · esc close left/right group · enter edit shortcut · * custom · - unbound · esc close
@@ -5,19 +5,19 @@ expression: "render_picker(params, 78)"
Keymap Keymap
All configurable shortcuts. All configurable shortcuts.
109 actions, 0 customized, 2 unbound. 115 actions, 0 customized, 2 unbound.
[All] Common Customized (0) Unbound (2) App Composer Editor Vim [All] Common Customized (0) Unbound (2) App Composer Editor Vim
Navigation Approval Debug Navigation Agents Approval Debug
Type to search shortcuts Type to search shortcuts
› Global Open Transcript ctrl-t › Global Open Agents alt-a
Global Open Transcript ctrl-t
Global Open External Editor ctrl-g Global Open External Editor ctrl-g
Global Copy ctrl-o Global Copy ctrl-o
Global Clear Terminal ctrl-l Global Clear Terminal ctrl-l
Global - Toggle Vim Mode unbound Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r Global Toggle Raw Output alt-r
Global Toggle Side Conversation ctrl-/ Global Toggle Side Conversation ctrl-/
Chat Interrupt Turn esc
left/right group · enter edit shortcut · * custom · - unbound · esc close left/right group · enter edit shortcut · * custom · - unbound · esc close
@@ -5,18 +5,18 @@ expression: "render_picker(params, 120)"
Keymap Keymap
All configurable shortcuts. All configurable shortcuts.
109 actions, 0 customized, 2 unbound. 115 actions, 0 customized, 2 unbound.
[All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug [All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Agents Approval Debug
Type to search shortcuts Type to search shortcuts
› Global Open Transcript ctrl-t › Global Open Agents alt-a
Global Open Transcript ctrl-t
Global Open External Editor ctrl-g Global Open External Editor ctrl-g
Global Copy ctrl-o Global Copy ctrl-o
Global Clear Terminal ctrl-l Global Clear Terminal ctrl-l
Global - Toggle Vim Mode unbound Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r Global Toggle Raw Output alt-r
Global Toggle Side Conversation ctrl-/ Global Toggle Side Conversation ctrl-/
Chat Interrupt Turn esc
left/right group · enter edit shortcut · * custom · - unbound · esc close left/right group · enter edit shortcut · * custom · - unbound · esc close