Keep side conversations open when switching threads (#35011)

## What changed

- Add a configurable `toggle_side_conversation` TUI action, bound to `ctrl-/` by default, to switch between a side conversation and its parent without closing either.
- Show the active binding in the footer and distinguish switching from closing with `ctrl-c`.
- Allow starting a replacement side conversation from the parent, while cleaning up the previous side thread and preserving it if cleanup fails.
- Clean up retained side conversations when shutting down the current thread.

## Testing

- Cover shortcut remapping, conflicts, and compatibility with existing `ctrl-/` and `ctrl-7` bindings.
- Cover side-thread replacement and footer states for both the parent and side conversation.

GitOrigin-RevId: a621c21e1c45ba73197d59750b2e05efa5ee1081
This commit is contained in:
Eric Traut
2026-07-23 21:03:40 +00:00
committed by copyberry
parent 62ba648136
commit 7d4b417cd1
20 changed files with 244 additions and 27 deletions
+2
View File
@@ -111,6 +111,8 @@ pub struct TuiGlobalKeymap {
pub toggle_fast_mode: Option<KeybindingsSpec>,
/// Toggle raw scrollback mode for copy-friendly transcript selection.
pub toggle_raw_output: Option<KeybindingsSpec>,
/// Switch between a side conversation and its parent without closing either.
pub toggle_side_conversation: Option<KeybindingsSpec>,
}
/// Chat context keybindings.
+10
View File
@@ -3240,6 +3240,7 @@
"toggle_fast_mode": null,
"toggle_raw_output": null,
"toggle_shortcuts": null,
"toggle_side_conversation": null,
"toggle_vim_mode": null
},
"list": {
@@ -3818,6 +3819,14 @@
],
"description": "Toggle the composer shortcut overlay."
},
"toggle_side_conversation": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Switch between a side conversation and its parent without closing either."
},
"toggle_vim_mode": {
"allOf": [
{
@@ -3919,6 +3928,7 @@
"toggle_fast_mode": null,
"toggle_raw_output": null,
"toggle_shortcuts": null,
"toggle_side_conversation": null,
"toggle_vim_mode": null
}
},
+2
View File
@@ -2398,6 +2398,7 @@ impl App {
self.keymap = runtime_keymap.clone();
self.chat_widget
.apply_keymap_update(keymap_config, &runtime_keymap);
self.sync_side_thread_ui();
self.chat_widget
.return_to_keymap_picker(&context, &action, &runtime_keymap);
self.chat_widget.add_info_message(message, /*hint*/ None);
@@ -2448,6 +2449,7 @@ impl App {
self.keymap = runtime_keymap.clone();
self.chat_widget
.apply_keymap_update(keymap_config, &runtime_keymap);
self.sync_side_thread_ui();
self.chat_widget
.return_to_keymap_picker(&context, &action, &runtime_keymap);
self.chat_widget.add_info_message(
+13
View File
@@ -142,6 +142,19 @@ impl App {
let app_keymap_shortcuts_available = self.app_keymap_shortcuts_available();
let side_toggle_bindings = &self.keymap.app.toggle_side_conversation;
if app_keymap_shortcuts_available
&& (side_toggle_bindings.is_pressed(key_event)
|| side_toggle_bindings.contains(&crate::key_hint::ctrl(KeyCode::Char('/')))
&& crate::key_hint::ctrl(KeyCode::Char('7')).is_press(key_event))
{
if let Err(err) = self.toggle_side_conversation(tui, app_server).await {
self.chat_widget
.add_error_message(format!("Failed to switch side conversation: {err}"));
}
return;
}
if app_keymap_shortcuts_available && self.keymap.app.toggle_vim_mode.is_pressed(key_event) {
self.chat_widget.toggle_vim_mode_and_notify();
return;
+59 -8
View File
@@ -20,7 +20,7 @@ const SIDE_NO_STARTED_CONVERSATION_MESSAGE: &str = concat!(
"Send a message first, then try /side again."
);
const SIDE_ALREADY_OPEN_MESSAGE: &str =
"A side conversation is already open. Press Ctrl+C to return before starting another.";
"A side conversation is already open. Press ctrl + c to return before starting another.";
const SIDE_BOUNDARY_PROMPT: &str = r#"Side conversation boundary.
Everything before this boundary is inherited history from the parent thread. It is reference context only. It is not your current task.
@@ -236,6 +236,19 @@ impl App {
.map(|state| (state.parent_thread_id, state.parent_status))
else {
clear_side_ui(&mut self.chat_widget);
if self
.side_threads
.values()
.any(|state| state.parent_thread_id == active_thread_id)
&& let Some(binding) =
crate::keymap::primary_binding(&self.keymap.app.toggle_side_conversation)
{
self.chat_widget
.set_side_conversation_context_label(Some(format!(
"{} for side",
binding.display_label()
)));
}
return;
};
@@ -256,7 +269,12 @@ impl App {
if let Some(parent_status) = parent_status {
label_parts.push(parent_status.label(parent_is_main).to_string());
}
label_parts.push("Ctrl+C to return".to_string());
if let Some(binding) =
crate::keymap::primary_binding(&self.keymap.app.toggle_side_conversation)
{
label_parts.push(format!("{} to switch", binding.display_label()));
}
label_parts.push("ctrl + c to close".to_string());
self.chat_widget
.set_side_conversation_context_label(Some(format!("Side {}", label_parts.join(" · "))));
}
@@ -353,12 +371,37 @@ impl App {
&self,
target_thread_id: ThreadId,
) -> Option<ThreadId> {
let side_thread_id = self.current_displayed_thread_id()?;
if target_thread_id == side_thread_id || !self.side_threads.contains_key(&side_thread_id) {
let active_thread_id = self.current_displayed_thread_id()?;
let (&side_thread_id, state) = self.side_threads.iter().next()?;
if target_thread_id == side_thread_id || target_thread_id == active_thread_id {
return None;
}
Some(side_thread_id)
(active_thread_id == side_thread_id || active_thread_id == state.parent_thread_id)
.then_some(side_thread_id)
}
pub(super) async fn toggle_side_conversation(
&mut self,
tui: &mut tui::Tui,
app_server: &mut AppServerSession,
) -> Result<()> {
let Some(active_thread_id) = self.current_displayed_thread_id() else {
return Ok(());
};
let Some((&side_thread_id, state)) = self.side_threads.iter().next() else {
return Ok(());
};
let target_thread_id = if active_thread_id == side_thread_id {
state.parent_thread_id
} else if active_thread_id == state.parent_thread_id {
side_thread_id
} else {
return Ok(());
};
self.select_agent_thread(tui, app_server, target_thread_id)
.await
}
pub(super) async fn discard_side_thread(
@@ -484,7 +527,7 @@ impl App {
pub(super) fn side_start_block_message(&self) -> Option<&'static str> {
if self.primary_thread_id.is_none() {
Some(SIDE_MAIN_THREAD_UNAVAILABLE_MESSAGE)
} else if !self.side_threads.is_empty() {
} else if self.active_side_parent_thread_id().is_some() {
Some(SIDE_ALREADY_OPEN_MESSAGE)
} else {
None
@@ -530,7 +573,6 @@ impl App {
app_server: &mut AppServerSession,
thread_id: ThreadId,
) -> Result<()> {
let active_thread_id_before_switch = self.active_thread_id;
let side_thread_to_discard = self.side_thread_to_discard_after_switch(thread_id);
self.select_agent_thread(tui, app_server, thread_id).await?;
if self.active_thread_id == Some(thread_id)
@@ -539,7 +581,7 @@ impl App {
if self.discard_side_thread(app_server, side_thread_id).await {
self.surface_pending_inactive_thread_interactive_requests()
.await?;
} else if active_thread_id_before_switch == Some(side_thread_id) {
} else {
self.keep_side_thread_visible_after_cleanup_failure(
tui,
app_server,
@@ -565,6 +607,15 @@ impl App {
return Ok(AppRunControl::Continue);
}
if let Some((&side_thread_id, state)) = self.side_threads.iter().next()
&& (parent_thread_id != state.parent_thread_id
|| !self.discard_side_thread(app_server, side_thread_id).await)
{
self.restore_side_user_message(user_message.take());
self.sync_side_thread_ui();
return Ok(AppRunControl::Continue);
}
self.session_telemetry.counter(
"codex.thread.side",
/*inc*/ 1,
+16 -2
View File
@@ -3788,7 +3788,7 @@ async fn side_fork_config_inherits_parent_thread_runtime_settings() {
}
#[tokio::test]
async fn side_start_block_message_tracks_open_side_conversation() {
async fn side_start_block_message_allows_replacing_open_side_conversation() {
let mut app = make_test_app().await;
assert_eq!(
app.side_start_block_message(),
@@ -3803,10 +3803,14 @@ async fn side_start_block_message_tracks_open_side_conversation() {
app.side_threads
.insert(side_thread_id, SideThreadState::new(parent_thread_id));
app.active_thread_id = Some(parent_thread_id);
assert_eq!(app.side_start_block_message(), None);
app.active_thread_id = Some(side_thread_id);
assert_eq!(
app.side_start_block_message(),
Some(
"A side conversation is already open. Press Ctrl+C to return before starting another."
"A side conversation is already open. Press ctrl + c to return before starting another."
)
);
@@ -4269,6 +4273,16 @@ async fn side_discard_selection_keeps_current_side_thread() {
app.side_thread_to_discard_after_switch(parent_thread_id),
Some(side_thread_id)
);
app.active_thread_id = Some(parent_thread_id);
assert_eq!(
app.side_thread_to_discard_after_switch(ThreadId::new()),
Some(side_thread_id)
);
assert_eq!(
app.side_thread_to_discard_after_switch(side_thread_id),
None
);
}
#[tokio::test]
+4
View File
@@ -14,6 +14,10 @@ use codex_app_server_protocol::WarningNotification;
impl App {
pub(super) async fn shutdown_current_thread(&mut self, app_server: &mut AppServerSession) {
let side_thread_ids: Vec<ThreadId> = self.side_threads.keys().copied().collect();
for side_thread_id in side_thread_ids {
self.discard_side_thread(app_server, side_thread_id).await;
}
if let Some(thread_id) = self.chat_widget.thread_id() {
if let Err(err) = app_server.thread_unsubscribe(thread_id).await {
tracing::warn!("failed to unsubscribe thread {thread_id}: {err}");
+1 -1
View File
@@ -593,7 +593,7 @@ pub(crate) fn side_conversation_context_line(label: &str) -> Line<'static> {
if let Some(rest) = label.strip_prefix("Side ") {
Line::from(vec!["Side".magenta().bold(), format!(" {rest}").magenta()])
} else {
Line::from(label.to_string()).magenta()
Line::from(vec![Span::from(label.to_string()).magenta()])
}
}
@@ -6,4 +6,4 @@ expression: terminal.backend()
" "
"› Check recently modified functions for compatibility "
" "
" gpt-5.6-sol Side from main thread · Ctrl+C to return "
" gpt-5.6-sol Side from main thread · ctrl + / to switch · ctrl + c to close "
@@ -0,0 +1,9 @@
---
source: tui/src/chatwidget/tests/side.rs
expression: terminal.backend()
---
" "
" "
"› Ask Codex to do anything "
" "
" gpt-5.6-sol ctrl + / for side "
@@ -6,4 +6,4 @@ expression: terminal.backend()
" "
"› Check recently modified functions for compatibility "
" "
" gpt-5.6-sol def… Side from main thread · main needs input · Ctrl+C to return "
" Side from main thread · main needs input · ctrl + / to switch · ctrl + c to cl"
+27 -2
View File
@@ -389,7 +389,7 @@ async fn side_context_label_preserves_status_line_snapshot() {
chat.refresh_status_line();
chat.set_side_conversation_active(/*active*/ true);
chat.set_side_conversation_context_label(Some(
"Side from main thread · Ctrl+C to return".to_string(),
"Side from main thread · ctrl + / to switch · ctrl + c to close".to_string(),
));
let width = 80;
@@ -410,7 +410,8 @@ async fn side_context_label_shows_parent_status_snapshot() {
chat.show_welcome_banner = false;
chat.set_side_conversation_active(/*active*/ true);
chat.set_side_conversation_context_label(Some(
"Side from main thread · main needs input · Ctrl+C to return".to_string(),
"Side from main thread · main needs input · ctrl + / to switch · ctrl + c to close"
.to_string(),
));
let width = 80;
@@ -421,3 +422,27 @@ async fn side_context_label_shows_parent_status_snapshot() {
.expect("draw side conversation footer");
assert_chatwidget_snapshot!("side_context_label_shows_parent_status", terminal.backend());
}
#[tokio::test]
async fn side_context_label_shows_hidden_side_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.show_welcome_banner = false;
chat.config.tui_status_line = Some(vec!["model-name".to_string()]);
chat.refresh_status_line();
chat.set_side_conversation_context_label(Some("ctrl + / for side".to_string()));
let width = 80;
let height = chat.desired_height(width);
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("create terminal");
terminal
.draw(|f| chat.render(f.area(), f.buffer_mut()))
.expect("draw hidden side conversation footer");
let hint_start = width - "ctrl + / for side".len() as u16 - 2;
assert_eq!(
terminal.backend().buffer()[(hint_start, height - 1)]
.style()
.fg,
Some(ratatui::style::Color::Magenta)
);
assert_chatwidget_snapshot!("side_context_label_shows_hidden_side", terminal.backend());
}
+84
View File
@@ -70,6 +70,8 @@ pub(crate) struct AppKeymap {
pub(crate) toggle_fast_mode: Vec<KeyBinding>,
/// Toggle raw scrollback mode for copy-friendly transcript selection.
pub(crate) toggle_raw_output: Vec<KeyBinding>,
/// Switch between a side conversation and its parent without closing either.
pub(crate) toggle_side_conversation: Vec<KeyBinding>,
}
/// Chat-level keybindings evaluated at the app event layer.
@@ -385,6 +387,12 @@ impl RuntimeKeymap {
/// dispatch, or conflict guarantees from this resolver no longer hold.
pub(crate) fn from_config(keymap: &TuiKeymap) -> Result<Self, String> {
let defaults = Self::built_in_defaults();
let side_toggle_default_is_shadowed = keymap.global.toggle_side_conversation.is_none()
&& ["ctrl-/", "ctrl-7"].into_iter().any(|alias| {
configured_main_surface_alias_is_used(keymap, alias)
|| configured_context_alias_is_used(&keymap.list, alias)
|| configured_context_alias_is_used(&keymap.approval, alias)
});
let app = AppKeymap {
open_transcript: resolve_bindings(
@@ -422,6 +430,15 @@ impl RuntimeKeymap {
&defaults.app.toggle_raw_output,
"tui.keymap.global.toggle_raw_output",
)?,
toggle_side_conversation: if side_toggle_default_is_shadowed {
Vec::new()
} else {
resolve_bindings(
keymap.global.toggle_side_conversation.as_ref(),
&defaults.app.toggle_side_conversation,
"tui.keymap.global.toggle_side_conversation",
)?
},
};
let mut chat = ChatKeymap {
@@ -809,6 +826,10 @@ impl RuntimeKeymap {
keymap.global.toggle_raw_output.as_ref(),
app.toggle_raw_output.as_slice(),
),
(
keymap.global.toggle_side_conversation.as_ref(),
app.toggle_side_conversation.as_slice(),
),
(keymap.list.move_up.as_ref(), list_move_up.as_slice()),
(keymap.list.move_down.as_ref(), list_move_down.as_slice()),
(keymap.list.accept.as_ref(), list_accept.as_slice()),
@@ -916,6 +937,7 @@ impl RuntimeKeymap {
toggle_vim_mode: default_bindings![],
toggle_fast_mode: default_bindings![],
toggle_raw_output: default_bindings![alt(KeyCode::Char('r'))],
toggle_side_conversation: default_bindings![ctrl(KeyCode::Char('/'))],
},
chat: ChatKeymap {
interrupt_turn: default_bindings![plain(KeyCode::Esc)],
@@ -1162,6 +1184,15 @@ impl RuntimeKeymap {
/// 2. Contexts with hard-coded sequence behavior, such as edit-previous
/// backtracking, intentionally stay outside this configurable keymap.
fn validate_conflicts(&self) -> Result<(), String> {
let mut side_toggle_bindings = self.app.toggle_side_conversation.clone();
let slash_binding = key_hint::ctrl(KeyCode::Char('/'));
let legacy_slash_binding = key_hint::ctrl(KeyCode::Char('7'));
if side_toggle_bindings.contains(&slash_binding)
&& !side_toggle_bindings.contains(&legacy_slash_binding)
{
side_toggle_bindings.push(legacy_slash_binding);
}
validate_unique(
"app",
[
@@ -1175,6 +1206,7 @@ impl RuntimeKeymap {
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
("toggle_raw_output", self.app.toggle_raw_output.as_slice()),
("toggle_side_conversation", side_toggle_bindings.as_slice()),
("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()),
(
"chat.decrease_reasoning_effort",
@@ -1218,6 +1250,7 @@ impl RuntimeKeymap {
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
("toggle_raw_output", self.app.toggle_raw_output.as_slice()),
("toggle_side_conversation", side_toggle_bindings.as_slice()),
("chat.interrupt_turn", self.chat.interrupt_turn.as_slice()),
(
"chat.decrease_reasoning_effort",
@@ -1267,6 +1300,7 @@ impl RuntimeKeymap {
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
("toggle_raw_output", self.app.toggle_raw_output.as_slice()),
("toggle_side_conversation", side_toggle_bindings.as_slice()),
],
[
("list.move_up", self.list.move_up.as_slice()),
@@ -1341,6 +1375,7 @@ impl RuntimeKeymap {
("toggle_vim_mode", self.app.toggle_vim_mode.as_slice()),
("toggle_fast_mode", self.app.toggle_fast_mode.as_slice()),
("toggle_raw_output", self.app.toggle_raw_output.as_slice()),
("toggle_side_conversation", side_toggle_bindings.as_slice()),
(
"composer.history_search_previous",
self.composer.history_search_previous.as_slice(),
@@ -2684,6 +2719,55 @@ mod tests {
expect_conflict(&keymap, "clear_terminal", "toggle_fast_mode");
}
#[test]
fn toggle_side_conversation_can_be_remapped_and_rejects_conflicts() {
let mut keymap = TuiKeymap::default();
keymap.global.toggle_side_conversation = Some(one("f12"));
let runtime = RuntimeKeymap::from_config(&keymap).expect("runtime keymap");
assert_eq!(
runtime.app.toggle_side_conversation,
vec![key_hint::plain(KeyCode::F(12))]
);
keymap.global.toggle_side_conversation = Some(one("ctrl-l"));
expect_conflict(&keymap, "clear_terminal", "toggle_side_conversation");
}
#[test]
fn toggle_side_conversation_default_yields_to_existing_configured_bindings() {
for binding in ["ctrl-/", "ctrl-7"] {
let mut keymap = TuiKeymap::default();
keymap.global.open_transcript = Some(one(binding));
let runtime =
RuntimeKeymap::from_config(&keymap).expect("existing keymap remains valid");
assert_eq!(
runtime.app.open_transcript,
vec![parse_keybinding(binding).expect("configured binding")]
);
assert!(runtime.app.toggle_side_conversation.is_empty());
}
let mut keymap = TuiKeymap::default();
keymap.global.open_transcript = Some(one("ctrl-/"));
keymap.global.copy = Some(one("ctrl-7"));
let runtime =
RuntimeKeymap::from_config(&keymap).expect("distinct legacy bindings remain valid");
assert!(runtime.app.toggle_side_conversation.is_empty());
}
#[test]
fn explicit_side_toggle_rejects_legacy_ctrl_slash_alias_conflicts() {
let mut keymap = TuiKeymap::default();
keymap.global.open_transcript = Some(one("ctrl-7"));
keymap.global.toggle_side_conversation = Some(one("ctrl-/"));
expect_conflict(&keymap, "open_transcript", "toggle_side_conversation");
}
#[test]
fn rejects_main_bindings_that_collide_with_remaining_fixed_shortcuts() {
let mut keymap = TuiKeymap::default();
+3
View File
@@ -93,6 +93,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[
action("global", "Global", "toggle_vim_mode", "Turn Vim composer mode on or off."),
gated_action("global", "Global", "toggle_fast_mode", "Turn Fast mode on or off.", KeymapActionFeature::FastMode),
action("global", "Global", "toggle_raw_output", "Toggle raw scrollback mode."),
action("global", "Global", "toggle_side_conversation", "Switch between a side conversation and its parent."),
action("chat", "Chat", "interrupt_turn", "Interrupt the active turn."),
action("chat", "Chat", "decrease_reasoning_effort", "Decrease reasoning effort."),
action("chat", "Chat", "increase_reasoning_effort", "Increase reasoning effort."),
@@ -236,6 +237,7 @@ pub(super) fn binding_slot<'a>(
("global", "toggle_vim_mode") => Some(&mut keymap.global.toggle_vim_mode),
("global", "toggle_fast_mode") => Some(&mut keymap.global.toggle_fast_mode),
("global", "toggle_raw_output") => Some(&mut keymap.global.toggle_raw_output),
("global", "toggle_side_conversation") => Some(&mut keymap.global.toggle_side_conversation),
("chat", "interrupt_turn") => Some(&mut keymap.chat.interrupt_turn),
("chat", "decrease_reasoning_effort") => Some(&mut keymap.chat.decrease_reasoning_effort),
("chat", "increase_reasoning_effort") => Some(&mut keymap.chat.increase_reasoning_effort),
@@ -361,6 +363,7 @@ pub(super) fn bindings_for_action<'a>(
("global", "toggle_vim_mode") => Some(runtime_keymap.app.toggle_vim_mode.as_slice()),
("global", "toggle_fast_mode") => Some(runtime_keymap.app.toggle_fast_mode.as_slice()),
("global", "toggle_raw_output") => Some(runtime_keymap.app.toggle_raw_output.as_slice()),
("global", "toggle_side_conversation") => Some(runtime_keymap.app.toggle_side_conversation.as_slice()),
("chat", "interrupt_turn") => Some(runtime_keymap.chat.interrupt_turn.as_slice()),
("chat", "decrease_reasoning_effort") => Some(runtime_keymap.chat.decrease_reasoning_effort.as_slice()),
("chat", "increase_reasoning_effort") => Some(runtime_keymap.chat.increase_reasoning_effort.as_slice()),
@@ -8,9 +8,9 @@ Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard.
Clear Terminal | ctrl-l | Global clear_terminal Clear Terminal Clear the terminal UI. ctrl-l Default
Toggle Vim Mode | unbound | Global toggle_vim_mode Toggle Vim Mode Turn Vim composer mode on or off. unbound Default
Toggle Raw Output | alt-r | Global toggle_raw_output Toggle Raw Output Toggle raw scrollback mode. alt-r Default
Toggle Side Conversation | ctrl-/ | Global toggle_side_conversation Toggle Side Conversation Switch between a side conversation and its parent. ctrl-/ Default
Interrupt Turn | esc | Chat interrupt_turn Interrupt Turn Interrupt the active turn. esc 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
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
Queue | tab | Composer queue Queue Queue the draft while a task is running. tab Default
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
Keymap
All configurable shortcuts.
108 actions, 1 customized, 2 unbound.
109 actions, 1 customized, 2 unbound.
[All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Approval Debug
@@ -16,7 +16,7 @@ expression: "render_picker(params, 120)"
Global Clear Terminal ctrl-l
Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r
Global Toggle Side Conversation ctrl-/
Chat Interrupt Turn esc
Chat Decrease Reasoning Effort alt-,, shift-down
left/right group · enter edit shortcut · * custom · - unbound · esc close
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
Keymap
All configurable shortcuts.
109 actions, 0 customized, 3 unbound.
110 actions, 0 customized, 3 unbound.
[All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Approval Debug
@@ -17,6 +17,6 @@ expression: "render_picker(params, 120)"
Global - Toggle Vim Mode unbound
Global - Toggle Fast Mode unbound
Global Toggle Raw Output alt-r
Chat Interrupt Turn esc
Global Toggle Side Conversation ctrl-/
left/right group · enter edit shortcut · * custom · - unbound · esc close
@@ -2,11 +2,11 @@
source: tui/src/keymap_setup.rs
expression: snapshot
---
tab: All (108 selectable)
tab: All (109 selectable)
tab: Common (20 selectable)
tab: Customized (0) (0 selectable)
tab: Unbound (2) (2 selectable)
tab: App (10 selectable)
tab: App (11 selectable)
tab: Composer (5 selectable)
tab: Editor (17 selectable)
tab: Vim (48 selectable)
@@ -19,9 +19,9 @@ Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard.
Clear Terminal | ctrl-l | Global clear_terminal Clear Terminal Clear the terminal UI. ctrl-l Default
Toggle Vim Mode | unbound | Global toggle_vim_mode Toggle Vim Mode Turn Vim composer mode on or off. unbound Default
Toggle Raw Output | alt-r | Global toggle_raw_output Toggle Raw Output Toggle raw scrollback mode. alt-r Default
Toggle Side Conversation | ctrl-/ | Global toggle_side_conversation Toggle Side Conversation Switch between a side conversation and its parent. ctrl-/ Default
Interrupt Turn | esc | Chat interrupt_turn Interrupt Turn Interrupt the active turn. esc 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
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
Queue | tab | Composer queue Queue Queue the draft while a task is running. tab Default
@@ -5,7 +5,7 @@ expression: "render_picker(params, 78)"
Keymap
All configurable shortcuts.
108 actions, 0 customized, 2 unbound.
109 actions, 0 customized, 2 unbound.
[All] Common Customized (0) Unbound (2) App Composer Editor Vim
Navigation Approval Debug
@@ -17,7 +17,7 @@ expression: "render_picker(params, 78)"
Global Clear Terminal ctrl-l
Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r
Global Toggle Side Conversation ctrl-/
Chat Interrupt Turn esc
Chat Decrease Reasoning Effort alt-,, shift-down
left/right group · enter edit shortcut · * custom · - unbound · esc close
@@ -5,7 +5,7 @@ expression: "render_picker(params, 120)"
Keymap
All configurable shortcuts.
108 actions, 0 customized, 2 unbound.
109 actions, 0 customized, 2 unbound.
[All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug
@@ -16,7 +16,7 @@ expression: "render_picker(params, 120)"
Global Clear Terminal ctrl-l
Global - Toggle Vim Mode unbound
Global Toggle Raw Output alt-r
Global Toggle Side Conversation ctrl-/
Chat Interrupt Turn esc
Chat Decrease Reasoning Effort alt-,, shift-down
left/right group · enter edit shortcut · * custom · - unbound · esc close