Add shortcuts to focus the main composer and side conversation (#59208)

## What changed

- Add configurable `focus_main_composer` and `focus_sidebar_content` actions, with default `leader c` and `leader s` bindings in workspace mode.
- Let users return to the main composer while keeping the side pane open, or focus the side conversation and hide it when already focused.
- Clear transcript search, selection, and activity focus when returning to the main composer, including when hiding the side pane.

## Testing

Extend tests for focus switching, blocked thread switches, offline composer focus, key capture, and binding conflicts.

GitOrigin-RevId: da624ce7e00a63c4f13870e55c4bc95f09b32cc4
This commit is contained in:
Felipe Coury
2026-11-03 18:55:44 +00:00
committed by copyberry
parent 781d89b91e
commit 6b2a2757de
15 changed files with 334 additions and 40 deletions
+4
View File
@@ -137,6 +137,10 @@ pub struct TuiGlobalKeymap {
pub leader: Option<KeybindingsSpec>,
/// Change where a new conversation runs.
pub change_host: Option<KeybindingsSpec>,
/// Focus the main conversation composer.
pub focus_main_composer: Option<KeybindingsSpec>,
/// Focus the side conversation, or hide it when already focused.
pub focus_sidebar_content: Option<KeybindingsSpec>,
/// Hide the focused sidebar pane.
pub hide_pane: Option<KeybindingsSpec>,
/// Open the shared agent-session overview.
+20
View File
@@ -5279,6 +5279,8 @@
"copy": null,
"find_transcript": null,
"focus_activity": null,
"focus_main_composer": null,
"focus_sidebar_content": null,
"hide_pane": null,
"leader": null,
"open_agents": null,
@@ -6109,6 +6111,22 @@
],
"description": "Focus activity groups in the owned transcript to inspect their details."
},
"focus_main_composer": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Focus the main conversation composer."
},
"focus_sidebar_content": {
"allOf": [
{
"$ref": "#/definitions/KeybindingsSpec"
}
],
"description": "Focus the side conversation, or hide it when already focused."
},
"hide_pane": {
"allOf": [
{
@@ -6330,6 +6348,8 @@
"copy": null,
"find_transcript": null,
"focus_activity": null,
"focus_main_composer": null,
"focus_sidebar_content": null,
"hide_pane": null,
"leader": null,
"open_agents": null,
+3 -14
View File
@@ -270,6 +270,7 @@ mod server_version_notice;
mod session_lifecycle;
mod session_picker;
mod side;
mod side_focus;
mod side_pane;
mod startup;
pub(crate) mod startup_bootstrap;
@@ -1011,24 +1012,12 @@ impl App {
event
};
self.cancel_primed_browsing_for_event(&event);
if let TuiEvent::Key(key) = &event
&& self.keymap.app.hide_pane.is_pressed(*key)
&& self.side_pane_shortcuts_available(tui)
&& self.handle_side_focus_key(tui, app_server, *key).await
{
if key.kind == KeyEventKind::Press {
let activation = self
.routed_key_activation
.take()
.unwrap_or_else(|| KeyBinding::from_event(*key));
if let Err(err) = self.hide_focused_side_pane(tui, app_server).await {
self.chat_widget.add_error_message(err.to_string());
}
self.chat_widget
.suppress_current_view_key_until_release(activation, Instant::now());
}
return Ok(AppRunControl::Continue);
}
self.cancel_primed_browsing_for_event(&event);
let voice_toggle = |app: &Self, key: KeyEvent| {
key.kind == KeyEventKind::Press
&& app
+5 -1
View File
@@ -239,7 +239,11 @@ impl App {
fn key_chord_contexts(&self, tui: &tui::Tui) -> crate::keymap::KeymapContextSet {
let contexts = self.active_keymap_contexts();
if self.side_pane_shortcuts_available(tui) {
if !self.main_composer_shortcut_available(tui) {
return contexts;
}
let contexts = contexts.with_main_composer_focus();
if self.side_pane_switch_available() {
contexts.with_side_pane()
} else {
contexts
@@ -1664,6 +1664,22 @@ async fn find_refreshes_live_details_before_searching_the_first_query() -> Resul
))
.contains("needle")
);
crate::app::test_support::enable_side_retention(&mut app)?;
app.handle_tui_event(
&mut tui,
&mut app_server,
TuiEvent::Key(KeyCode::F(4).into()),
)
.await?;
assert!(app.transcript_view.is_activity_focused());
for key in [
KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL),
KeyCode::Char('c').into(),
] {
app.handle_tui_event(&mut tui, &mut app_server, TuiEvent::Key(key))
.await?;
}
assert!(!app.transcript_view.is_activity_focused());
for event in [
TuiEvent::Key(KeyEvent::new(KeyCode::F(3), KeyModifiers::NONE)),
TuiEvent::Paste("needle".into()),
+7 -4
View File
@@ -322,11 +322,15 @@ impl App {
self.local_settings.workspace_enabled(&self.config) && tui.is_owned_screen()
}
pub(super) fn side_pane_shortcuts_available(&self, tui: &tui::Tui) -> bool {
pub(super) fn main_composer_shortcut_available(&self, tui: &tui::Tui) -> bool {
self.side_retention_enabled(tui)
&& !self.reconnect.offline
&& self.overlay.is_none()
&& self.chat_widget.no_modal_or_popup_active()
&& (!self.reconnect.offline || self.active_side_parent_thread_id().is_none())
}
pub(super) fn side_pane_switch_available(&self) -> bool {
!self.reconnect.offline
&& self
.current_displayed_thread_id()
.and_then(|id| self.side_pair_key(id))
@@ -483,8 +487,7 @@ impl App {
{
side.pane_open = false;
tui.frame_requester().schedule_frame();
self.surface_pending_inactive_thread_interactive_requests()
.await?;
self.focus_returned_parent_composer(parent).await?;
}
Ok(())
}
+93
View File
@@ -0,0 +1,93 @@
//! Composer and side-conversation focus shortcuts. Thread selection replaces the widget
//! immediately, but Park and Finish transfer transcript views through the app event queue.
use super::*;
use crate::transcript_view::TranscriptView;
impl App {
pub(super) async fn handle_side_focus_key(
&mut self,
tui: &mut tui::Tui,
app_server: &mut AppServerSession,
key: KeyEvent,
) -> bool {
let bindings = &self.keymap.app;
let main = bindings.focus_main_composer.is_pressed(key);
let sidebar = bindings.focus_sidebar_content.is_pressed(key);
if !(main || sidebar || bindings.hide_pane.is_pressed(key))
|| !self.main_composer_shortcut_available(tui)
|| (!main && !self.side_pane_switch_available())
{
return false;
}
if key.kind != KeyEventKind::Press {
return true;
}
let activation = self
.routed_key_activation
.take()
.unwrap_or_else(|| KeyBinding::from_event(key));
let result = if main {
self.focus_main_composer(tui, app_server).await
} else if sidebar
&& let Some(parent) = self.current_displayed_thread_id()
&& let Some(child) = self.side_pair_key(parent)
&& child != parent
{
let result = self.toggle_side_conversation(tui, app_server).await;
if result.is_ok() && self.current_displayed_thread_id() == Some(child) {
// The outgoing parent's view is still here until Park runs.
self.focus_current_parent_view(tui);
}
result
} else {
self.hide_focused_side_pane(tui, app_server).await
};
if let Err(err) = result {
self.chat_widget.add_error_message(err.to_string());
}
self.chat_widget
.suppress_current_view_key_until_release(activation, Instant::now());
true
}
async fn focus_main_composer(
&mut self,
tui: &mut tui::Tui,
app_server: &mut AppServerSession,
) -> Result<()> {
if let Some(parent) = self.active_side_parent_thread_id() {
self.select_agent_thread(tui, app_server, parent).await?;
if self.current_displayed_thread_id() == Some(parent) {
self.focus_returned_parent_composer(parent).await?;
}
} else {
self.focus_current_parent_view(tui);
}
Ok(())
}
fn focus_current_parent_view(&mut self, tui: &mut tui::Tui) {
self.cancel_transcript_browsing(tui);
clear_transcript_interaction(&mut self.transcript_view, &self.transcript_cells);
self.cancel_pending_key_chord();
}
pub(super) async fn focus_returned_parent_composer(&mut self, parent: ThreadId) -> Result<()> {
// Finish will restore this view. The live transcript still belongs to the child.
if let Some(companion) = self.side_companions.get_mut(&parent) {
companion.view.set_highlight(/*index*/ None);
clear_transcript_interaction(&mut companion.view, &companion.cells);
}
self.cancel_pending_key_chord();
self.surface_pending_inactive_thread_interactive_requests()
.await
}
}
fn clear_transcript_interaction(view: &mut TranscriptView, cells: &[Arc<dyn HistoryCell>]) {
view.end_select_mode();
view.end_selection(cells);
view.cancel_search();
view.clear_activity_focus();
}
+68 -5
View File
@@ -3497,7 +3497,7 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
parent_thread_id,
"background",
"reply",
"still working\n\n",
&"still working\n\n".repeat(40),
),
)
.await?;
@@ -3543,7 +3543,8 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
KeyEventKind::Release,
]
.map(|kind| KeyEvent::new_with_kind(KeyCode::Char('-'), KeyModifiers::NONE, kind));
assert!(app.side_pane_shortcuts_available(&tui));
let main = KeyEvent::from(KeyCode::Char('c'));
let side = KeyEvent::from(KeyCode::Char('s'));
// Hide is consumed on the parent without closing the visible pane or typing its completion.
keys!(leader, minus, repeat, release);
assert!(!app.chat_widget.is_in_paste_burst());
@@ -3616,7 +3617,7 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
// The pending permission change refuses a parent switch even though selection returns Ok.
app.pending_server_profiles
.insert(side_thread_id, read_only);
.insert(side_thread_id, read_only.clone());
keys!(leader, minus, repeat, release);
assert_eq!(app.current_displayed_thread_id(), Some(side_thread_id));
assert_eq!(app.chat_widget.composer_text_with_pending(), "child draft!");
@@ -3640,10 +3641,33 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
enable_side_retention(&mut app)?;
KeyCode::F(12).into()
};
// Park an interaction on the parent so both hide shortcuts must clear it on return.
keys!(prefix, main);
app.transcript_view.begin_search();
app.select_agent_thread(&mut tui, &mut app_server, side_thread_id)
.await?;
drain_app_events(&mut app, &mut tui, &mut app_server, &mut app_event_rx).await?;
side_frame(&mut app, &mut tui, size)?;
assert!(app.transcript_view.begin_select_mode(&app.transcript_cells));
keys!(prefix, minus);
if width == 100 {
app.pending_server_profiles
.insert(side_thread_id, read_only.clone());
keys!(prefix, main);
assert_eq!(app.current_displayed_thread_id(), Some(side_thread_id));
assert!(app.transcript_view.is_select_mode());
assert!(
app.side_companions[&parent_thread_id]
.view
.is_search_editing()
);
app.pending_server_profiles.remove(&side_thread_id);
}
keys!(prefix, if width == 100 { minus } else { side });
assert_eq!(app.current_displayed_thread_id(), Some(parent_thread_id));
keys!(repeat, release);
assert!(!app.transcript_view.has_active_interaction());
if width == 100 {
keys!(repeat, release);
}
assert!(!app.chat_widget.is_in_paste_burst());
let hidden = side_frame(&mut app, &mut tui, size)?;
assert!(
@@ -3690,6 +3714,45 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
}
assert_app_snapshot!("hide_and_reopen_side_conversation", frames.join("\n\n"));
// The parent is browsing a saved prompt and the child has a selection.
let prefix = KeyCode::F(12).into();
keys!(toggle);
app.apply_backtrack_selection_internal(/*nth_user_message*/ 0);
let highlighted = side_frame(&mut app, &mut tui, size)?;
assert!(
highlighted.contains("Saved user message") && !highlighted.contains("Back to bottom"),
"{highlighted}"
);
keys!(toggle);
side_frame(&mut app, &mut tui, size)?;
assert!(app.transcript_view.begin_select_mode(&app.transcript_cells));
keys!(prefix, main);
assert_eq!(app.current_displayed_thread_id(), Some(parent_thread_id));
let restored = side_frame(&mut app, &mut tui, size)?;
assert!(
restored.contains("Saved user message")
&& restored.contains("Back to bottom")
&& restored.contains('│'),
"{restored}"
);
assert!(app.transcript_view.begin_select_mode(&app.transcript_cells));
app.pending_server_profiles
.insert(parent_thread_id, read_only);
keys!(prefix, side);
assert_eq!(app.current_displayed_thread_id(), Some(parent_thread_id));
assert!(app.transcript_view.is_select_mode());
app.pending_server_profiles.remove(&parent_thread_id);
keys!(prefix, side);
assert_eq!(app.current_displayed_thread_id(), Some(side_thread_id));
assert!(app.transcript_view.is_select_mode());
assert!(
!app.side_companions[&parent_thread_id]
.view
.has_active_interaction()
);
app.transcript_view.end_select_mode();
read_side_history(&mut app, &mut tui, "child")?;
// The upload finishes before the app event loop can park the now-inactive child.
app.app_event_tx.send(AppEvent::FeedbackSubmitted {
+24 -1
View File
@@ -1173,12 +1173,15 @@ async fn key_capture_receives_voice_shortcut_and_chord_prefix() -> Result<()> {
tui.set_owned_screen(/*owned*/ true)?;
let parent = ThreadId::new();
app.active_thread_id = Some(parent);
let child = ThreadId::new();
app.side_threads
.insert(ThreadId::new(), super::SideThreadState::new(parent));
.insert(child, super::SideThreadState::new(parent));
for (config, key) in [
("", 8),
("[chat]\ntoggle_voice = 'f8 v'", 8),
("[global]\nhide_pane = 'f12'", 12),
("[global]\nfocus_main_composer = 'f12'", 12),
("[global]\nfocus_sidebar_content = 'f12 s'", 12),
] {
app.keymap =
RuntimeKeymap::for_session(&toml::from_str(config)?, /*workspace*/ true).unwrap();
@@ -1219,5 +1222,25 @@ async fn key_capture_receives_voice_shortcut_and_chord_prefix() -> Result<()> {
}
assert_eq!(app.chat_widget.composer_text_with_pending(), draft);
}
// Offline, the same printable binding focuses the local composer but cannot switch threads.
let mut stopped = app_server.request_view();
app_server.shutdown().await?;
app.local_settings.tui.keymap = toml::from_str("[global]\nfocus_main_composer = '%'")?;
super::enable_side_retention(&mut app)?;
app.active_thread_id = Some(ThreadId::new());
app.reconnect.offline = true;
app.chat_widget.apply_external_edit("draft".into());
app.transcript_view.begin_search();
for key in [KeyCode::Char('%'), KeyCode::Char('!'), KeyCode::Right] {
press(&mut app, &mut tui, &mut stopped, key.into()).await?;
}
assert!(!app.transcript_view.has_active_interaction());
assert_eq!(app.chat_widget.composer_text_with_pending(), "draft!");
app.active_thread_id = Some(child);
for key in [KeyCode::Char('%'), KeyCode::Right] {
press(&mut app, &mut tui, &mut stopped, key.into()).await?;
}
assert_eq!(app.chat_widget.composer_text_with_pending(), "draft!%");
Ok(())
}
+70 -11
View File
@@ -104,6 +104,8 @@ pub(crate) struct RuntimeKeymap {
pub(crate) struct AppKeymap {
/// Change where a new conversation runs.
pub(crate) change_host: Vec<KeyBinding>,
pub(crate) focus_main_composer: Vec<KeyBinding>,
pub(crate) focus_sidebar_content: Vec<KeyBinding>,
pub(crate) hide_pane: Vec<KeyBinding>,
/// Open the agent command center.
pub(crate) open_agents: Vec<KeyBinding>,
@@ -739,6 +741,16 @@ impl RuntimeKeymap {
&defaults.app.change_host,
"tui.keymap.global.change_host",
)?,
focus_main_composer: resolve_bindings(
keymap.global.focus_main_composer.as_ref(),
&[],
"tui.keymap.global.focus_main_composer",
)?,
focus_sidebar_content: resolve_bindings(
keymap.global.focus_sidebar_content.as_ref(),
&[],
"tui.keymap.global.focus_sidebar_content",
)?,
hide_pane: resolve_bindings(
keymap.global.hide_pane.as_ref(),
&[],
@@ -1719,6 +1731,8 @@ impl RuntimeKeymap {
Self {
app: AppKeymap {
change_host: Vec::new(),
focus_main_composer: Vec::new(),
focus_sidebar_content: Vec::new(),
hide_pane: Vec::new(),
open_agents: default_bindings![],
open_transcript: default_bindings![ctrl(KeyCode::Char('t'))],
@@ -2071,6 +2085,11 @@ impl RuntimeKeymap {
for (action, bindings) in [
("global.change_host", &self.app.change_host),
("global.open_agents", &self.app.open_agents),
("global.focus_main_composer", &self.app.focus_main_composer),
(
"global.focus_sidebar_content",
&self.app.focus_sidebar_content,
),
("global.hide_pane", &self.app.hide_pane),
("chat.open_presets", &self.chat.open_presets),
("chat.toggle_voice", &self.chat.toggle_voice),
@@ -2101,6 +2120,14 @@ impl RuntimeKeymap {
let main_bindings = [
("change_host", self.app.change_host.as_slice()),
(
"focus_main_composer",
self.app.focus_main_composer.as_slice(),
),
(
"focus_sidebar_content",
self.app.focus_sidebar_content.as_slice(),
),
("hide_pane", self.app.hide_pane.as_slice()),
("open_agents", self.app.open_agents.as_slice()),
("open_transcript", self.app.open_transcript.as_slice()),
@@ -2270,6 +2297,14 @@ impl RuntimeKeymap {
validate_no_shadow_with_allowed_overlaps(
"main",
[
(
"focus_main_composer",
self.app.focus_main_composer.as_slice(),
),
(
"focus_sidebar_content",
self.app.focus_sidebar_content.as_slice(),
),
("hide_pane", self.app.hide_pane.as_slice()),
("open_agents", self.app.open_agents.as_slice()),
("open_transcript", self.app.open_transcript.as_slice()),
@@ -2396,6 +2431,14 @@ impl RuntimeKeymap {
validate_unique(
"transcript",
[
(
"global.focus_main_composer",
self.app.focus_main_composer.as_slice(),
),
(
"global.focus_sidebar_content",
self.app.focus_sidebar_content.as_slice(),
),
("global.hide_pane", self.app.hide_pane.as_slice()),
(
"global.select_transcript",
@@ -2429,6 +2472,14 @@ impl RuntimeKeymap {
validate_unique(
"activity",
context_bindings(KeymapContext::List).chain([
(
"global.focus_main_composer",
self.app.focus_main_composer.as_slice(),
),
(
"global.focus_sidebar_content",
self.app.focus_sidebar_content.as_slice(),
),
("global.hide_pane", self.app.hide_pane.as_slice()),
("global.focus_activity", self.app.focus_activity.as_slice()),
(
@@ -3891,21 +3942,26 @@ mod tests {
#[test]
fn toggle_side_conversation_can_be_remapped_and_rejects_conflicts() {
for (section, action) in [("pager", "page_up"), ("list", "move_down")] {
for (pane, section, action) in [
("hide_pane", "pager", "page_up"),
("hide_pane", "list", "move_down"),
("focus_main_composer", "pager", "page_up"),
("focus_sidebar_content", "list", "move_down"),
] {
let config: TuiKeymap = toml::from_str(&format!(
"[global]\nhide_pane = 'f8'\n[{section}]\n{action} = 'f8'"
"[global]\n{pane} = 'f8'\n[{section}]\n{action} = 'f8'"
))
.unwrap();
let error = RuntimeKeymap::for_session(&config, /*workspace*/ true).unwrap_err();
assert!(
error.contains("global.hide_pane") && error.contains(action),
error.contains(&format!("global.{pane}")) && error.contains(action),
"{error}"
);
}
#[cfg(unix)]
{
let mut config = TuiKeymap::default();
config.global.hide_pane = Some(one("ctrl-z"));
for action in ["hide_pane", "focus_main_composer", "focus_sidebar_content"] {
let config: TuiKeymap =
toml::from_str(&format!("[global]\n{action} = 'ctrl-z'")).unwrap();
assert!(
RuntimeKeymap::for_session(&config, /*workspace*/ true)
.unwrap_err()
@@ -4105,12 +4161,15 @@ mod tests {
let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse");
assert!(runtime.composer.toggle_shortcuts.is_empty());
keymap.global.hide_pane = Some(KeybindingsSpec::Many(vec![]));
keymap.global.focus_sidebar_content = Some(KeybindingsSpec::Many(vec![]));
let runtime = RuntimeKeymap::for_session(&keymap, /*workspace*/ true).unwrap();
assert!(
runtime
.primary_hint(KeymapContext::Global, "hide_pane")
.is_none()
);
for action in ["hide_pane", "focus_sidebar_content"] {
assert!(
runtime
.primary_hint(KeymapContext::Global, action)
.is_none()
);
}
}
#[test]
+7 -1
View File
@@ -104,7 +104,11 @@ pub(crate) struct KeymapActionId {
impl KeymapActionId {
pub(crate) fn is_side_pane(self) -> bool {
self.context == KeymapContext::Global && self.action == "hide_pane"
self.context == KeymapContext::Global
&& matches!(
self.action,
"focus_main_composer" | "focus_sidebar_content" | "hide_pane"
)
}
/// Whether two actions can be active together, including list-specific exceptions.
@@ -299,6 +303,8 @@ define_runtime_action_bindings! {
find_transcript,
select_transcript,
focus_activity,
focus_main_composer,
focus_sidebar_content,
hide_pane,
open_warnings => runtime_only,
open_external_editor,
+11 -1
View File
@@ -55,6 +55,7 @@ const VOICE_TOGGLE_BLOCKED: u32 = 1 << 19;
const TRANSCRIPT_ACTIONS: u32 = 1 << 20;
const BACKEND_CYCLING: u32 = 1 << 21;
const SIDE_PANE: u32 = 1 << 22;
const MAIN_COMPOSER_FOCUS: u32 = 1 << 23;
const TRANSCRIPT_BROWSING: u32 = 1 << 14;
@@ -157,6 +158,10 @@ impl KeymapContextSet {
}
}
pub(crate) const fn with_main_composer_focus(self) -> Self {
Self(self.0 | MAIN_COMPOSER_FOCUS)
}
pub(crate) const fn with_side_pane(self) -> Self {
Self(self.0 | SIDE_PANE)
}
@@ -167,7 +172,12 @@ impl KeymapContextSet {
return self.0 & BACKEND_CYCLING != 0;
}
if action.is_side_pane() {
return self.0 & SIDE_PANE != 0;
let required = if action.action == "focus_main_composer" {
MAIN_COMPOSER_FOCUS
} else {
SIDE_PANE
};
return self.0 & required != 0;
}
if action.context == KeymapContext::Chat && action.action == "open_presets" {
return self.contains(KeymapContext::Chat)
@@ -32,6 +32,8 @@ pub(in crate::keymap) fn register(
(Global, "change_host", 'b'),
(Global, "select_transcript", '['),
(Chat, "open_presets", 'p'),
(Global, "focus_main_composer", 'c'),
(Global, "focus_sidebar_content", 's'),
(Global, "hide_pane", '-'),
] {
if remaining == 0 {
+2
View File
@@ -118,6 +118,8 @@ macro_rules! define_keymap_actions {
define_keymap_actions! {
global "Global" {
change_host [MultiBackend] => "Change where a new conversation runs.",
focus_main_composer [Workspace] => "Focus the main conversation composer.",
focus_sidebar_content [Workspace] => "Focus the side conversation, or hide it when already focused.",
hide_pane [Workspace] => "Hide the focused sidebar pane.",
open_agents => "Open the agent command center.",
open_transcript => "Open the transcript overlay.",
@@ -2,6 +2,8 @@
source: tui/src/keymap_setup.rs
expression: snapshot
---
Focus Main Composer | leader c (ctrl-x c) | Global focus_main_composer Focus Main Composer Focus the main conversation composer. leader c (ctrl-x c) Default
Focus Sidebar Content | leader s (ctrl-x s) | Global focus_sidebar_content Focus Sidebar Content Focus the side conversation, or hide it when already focused. leader s (ctrl-x s) Default
Hide Pane | leader - (ctrl-x -) | Global hide_pane Hide Pane Hide the focused sidebar pane. leader - (ctrl-x -) Default
Open Agents | unbound | Global open_agents Open Agents Open the agent command center. unbound Default
Open Transcript | ctrl-t | Global open_transcript Open Transcript Open the transcript overlay. ctrl-t Default
@@ -13,5 +15,3 @@ Clear Terminal | ctrl-l | Global clear_terminal Clear Terminal Clear the termina
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
Leader | ctrl-x | Global leader Leader Set the prefix used by shared leader shortcuts. ctrl-x Default
Interrupt Turn | esc | Chat interrupt_turn Interrupt Turn Interrupt the active turn. esc Default