mirror of
https://github.com/openai/codex.git
synced 2026-11-11 14:32:25 +00:00
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:
1 parent
9a254ba1fa
commit
f47f77ada6
19 files changed
+661
-110
No files matched your search
@@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -986,7 +986,6 @@ mod tests {
|
|||||||
.join(".")
|
.join(".")
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
actions,
|
actions,
|
||||||
vec![
|
vec![
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
+1
-1
@@ -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
|
|
||||||
+4
-4
@@ -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
|
||||||
+4
-4
@@ -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
|
||||||
+4
-3
@@ -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
|
|
||||||
+4
-4
@@ -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
|
||||||
+4
-4
@@ -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
|
||||||
Reference in new issue
Block a user