mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Add directional conversation focus shortcuts in workspace mode (#59287)
## What changed - Add configurable `focus_pane_left` and `focus_pane_right` actions, defaulting to `leader left` / `leader h` and `leader right` / `leader l`. - Move focus to the parent or open side conversation without reopening a hidden pane. Preserve the current interaction when no neighbor is available, and accept fresh typing after a no-op shortcut while suppressing reported key repeats. - Expose the actions in the keymap picker and configuration schema, and prevent workspace-only pane bindings from claiming keys in classic mode. ## Testing Add regression coverage for pane navigation, preserved transcript selection and search, fresh typing after a no-op, key capture, and binding conflicts and limits. GitOrigin-RevId: 7f7629d15cffcd35d1353b36c5c9208dbb1034e8
This commit is contained in:
@@ -143,6 +143,10 @@ pub struct TuiGlobalKeymap {
|
||||
pub focus_sidebar_content: Option<KeybindingsSpec>,
|
||||
/// Hide the focused sidebar pane.
|
||||
pub hide_pane: Option<KeybindingsSpec>,
|
||||
/// Focus the parent conversation.
|
||||
pub focus_pane_left: Option<KeybindingsSpec>,
|
||||
/// Focus the open side conversation.
|
||||
pub focus_pane_right: Option<KeybindingsSpec>,
|
||||
/// Open the shared agent-session overview.
|
||||
pub open_agents: Option<KeybindingsSpec>,
|
||||
/// Open the transcript overlay.
|
||||
|
||||
@@ -5284,6 +5284,8 @@
|
||||
"find_transcript": null,
|
||||
"focus_activity": null,
|
||||
"focus_main_composer": null,
|
||||
"focus_pane_left": null,
|
||||
"focus_pane_right": null,
|
||||
"focus_sidebar_content": null,
|
||||
"hide_pane": null,
|
||||
"leader": null,
|
||||
@@ -6123,6 +6125,22 @@
|
||||
],
|
||||
"description": "Focus the main conversation composer."
|
||||
},
|
||||
"focus_pane_left": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Focus the parent conversation."
|
||||
},
|
||||
"focus_pane_right": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/KeybindingsSpec"
|
||||
}
|
||||
],
|
||||
"description": "Focus the open side conversation."
|
||||
},
|
||||
"focus_sidebar_content": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -6353,6 +6371,8 @@
|
||||
"find_transcript": null,
|
||||
"focus_activity": null,
|
||||
"focus_main_composer": null,
|
||||
"focus_pane_left": null,
|
||||
"focus_pane_right": null,
|
||||
"focus_sidebar_content": null,
|
||||
"hide_pane": null,
|
||||
"leader": null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Suppress a key held across a view transition from activating the child view.
|
||||
//! Keep a held activation key from triggering another action.
|
||||
//!
|
||||
//! Terminals with enhanced keyboard reporting distinguish a held `Repeat` from a fresh `Press`.
|
||||
//! Press-only terminals use a bounded window that extends while repeat events continue to arrive.
|
||||
//! A view transition on a press-only terminal can use a bounded window to block ambiguous presses.
|
||||
|
||||
use crate::key_hint::KeyBinding;
|
||||
use crossterm::event::KeyEventKind;
|
||||
@@ -11,20 +11,25 @@ use std::time::Instant;
|
||||
const INITIAL_GUARD: Duration = Duration::from_secs(/*secs*/ 1);
|
||||
const LEGACY_REPEAT_GUARD: Duration = Duration::from_secs(/*secs*/ 1);
|
||||
|
||||
pub(crate) enum ActivationKeySuppression {
|
||||
ViewTransition,
|
||||
ReportedRepeats,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ActivationKeyGuard {
|
||||
key: KeyBinding,
|
||||
block_until: Instant,
|
||||
key_releases_reported: bool,
|
||||
legacy_repeat_fallback: bool,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl ActivationKeyGuard {
|
||||
pub(crate) fn new_at(key: KeyBinding, now: Instant, key_releases_reported: bool) -> Self {
|
||||
pub(crate) fn new_at(key: KeyBinding, now: Instant, reported_repeats_only: bool) -> Self {
|
||||
Self {
|
||||
key,
|
||||
block_until: now.checked_add(INITIAL_GUARD).unwrap_or(now),
|
||||
key_releases_reported,
|
||||
legacy_repeat_fallback: !reported_repeats_only,
|
||||
active: true,
|
||||
}
|
||||
}
|
||||
@@ -43,8 +48,8 @@ impl ActivationKeyGuard {
|
||||
self.active = false;
|
||||
return true;
|
||||
}
|
||||
// Enhanced keyboard reporting labels a held key as Repeat, so a new Press is fresh.
|
||||
if self.key_releases_reported && kind == KeyEventKind::Press {
|
||||
// Without the legacy fallback, only a reported Repeat is treated as held.
|
||||
if !self.legacy_repeat_fallback && kind == KeyEventKind::Press {
|
||||
self.active = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ use crossterm::event::KeyCode;
|
||||
fn suppresses_activation_until_release_and_bounds_legacy_presses() {
|
||||
let start = Instant::now();
|
||||
let key = key_hint::plain(KeyCode::Enter);
|
||||
let mut guard = ActivationKeyGuard::new_at(key, start, /*key_releases_reported*/ true);
|
||||
let mut guard = ActivationKeyGuard::new_at(key, start, /*reported_repeats_only*/ true);
|
||||
|
||||
assert!(guard.consume_at(key, KeyEventKind::Repeat, start));
|
||||
assert!(guard.consume_at(key, KeyEventKind::Release, start));
|
||||
assert!(!guard.consume_at(key, KeyEventKind::Press, start));
|
||||
|
||||
let mut guard = ActivationKeyGuard::new_at(key, start, /*key_releases_reported*/ false);
|
||||
let mut guard = ActivationKeyGuard::new_at(key, start, /*reported_repeats_only*/ false);
|
||||
for elapsed in [900, 1_300] {
|
||||
assert!(guard.consume_at(
|
||||
key,
|
||||
@@ -30,7 +30,7 @@ fn suppresses_activation_until_release_and_bounds_legacy_presses() {
|
||||
let mut guard = ActivationKeyGuard::new_at(
|
||||
key_hint::plain(KeyCode::BackTab),
|
||||
start,
|
||||
/*key_releases_reported*/ true,
|
||||
/*reported_repeats_only*/ true,
|
||||
);
|
||||
let tab = key_hint::plain(KeyCode::Tab);
|
||||
assert!(guard.consume_at(tab, KeyEventKind::Repeat, start));
|
||||
|
||||
@@ -239,11 +239,14 @@ impl App {
|
||||
|
||||
fn key_chord_contexts(&self, tui: &tui::Tui) -> crate::keymap::KeymapContextSet {
|
||||
let contexts = self.active_keymap_contexts();
|
||||
if !self.main_composer_shortcut_available(tui) {
|
||||
if !self.conversation_focus_available(tui) {
|
||||
return contexts;
|
||||
}
|
||||
let contexts = contexts.with_main_composer_focus();
|
||||
if self.side_pane_switch_available() {
|
||||
let mut contexts = contexts.with_pane_directions();
|
||||
if self.main_composer_shortcut_available() {
|
||||
contexts = contexts.with_main_composer_focus();
|
||||
}
|
||||
if self.switchable_side_child().is_some() {
|
||||
contexts.with_side_pane()
|
||||
} else {
|
||||
contexts
|
||||
|
||||
@@ -322,19 +322,21 @@ impl App {
|
||||
self.local_settings.workspace_enabled(&self.config) && tui.is_owned_screen()
|
||||
}
|
||||
|
||||
pub(super) fn main_composer_shortcut_available(&self, tui: &tui::Tui) -> bool {
|
||||
pub(super) fn conversation_focus_available(&self, tui: &tui::Tui) -> bool {
|
||||
self.side_retention_enabled(tui)
|
||||
&& 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))
|
||||
.is_some()
|
||||
pub(super) fn main_composer_shortcut_available(&self) -> bool {
|
||||
!self.reconnect.offline || self.active_side_parent_thread_id().is_none()
|
||||
}
|
||||
|
||||
pub(super) fn switchable_side_child(&self) -> Option<ThreadId> {
|
||||
if self.reconnect.offline {
|
||||
return None;
|
||||
}
|
||||
self.side_pair_key(self.current_displayed_thread_id()?)
|
||||
}
|
||||
|
||||
pub(super) fn side_pair_key(&self, thread_id: ThreadId) -> Option<ThreadId> {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! immediately, but Park and Finish transfer transcript views through the app event queue.
|
||||
|
||||
use super::*;
|
||||
use crate::activation_key_guard::ActivationKeySuppression;
|
||||
use crate::transcript_view::TranscriptView;
|
||||
|
||||
impl App {
|
||||
@@ -12,42 +13,68 @@ impl App {
|
||||
key: KeyEvent,
|
||||
) -> bool {
|
||||
let bindings = &self.keymap.app;
|
||||
let main = bindings.focus_main_composer.is_pressed(key);
|
||||
let left = bindings.focus_pane_left.is_pressed(key);
|
||||
let right = bindings.focus_pane_right.is_pressed(key);
|
||||
let main =
|
||||
bindings.focus_main_composer.is_pressed(key) && self.main_composer_shortcut_available();
|
||||
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())
|
||||
{
|
||||
let hide = bindings.hide_pane.is_pressed(key);
|
||||
if !(left || right || main || sidebar || hide) || !self.conversation_focus_available(tui) {
|
||||
return false;
|
||||
}
|
||||
let child = self.switchable_side_child();
|
||||
if (sidebar || hide) && child.is_none() {
|
||||
return false;
|
||||
}
|
||||
if key.kind != KeyEventKind::Press {
|
||||
// A matching repeat still belongs to the shortcut. Keep it out of the focused editor.
|
||||
return true;
|
||||
}
|
||||
let activation = self
|
||||
.routed_key_activation
|
||||
.take()
|
||||
.unwrap_or_else(|| KeyBinding::from_event(key));
|
||||
let result = if main {
|
||||
let previous_thread = self.current_displayed_thread_id();
|
||||
let on_child = child.is_some_and(|id| previous_thread == Some(id));
|
||||
let leaves_transcript = main
|
||||
&& !on_child
|
||||
&& (self.transcript_view.has_active_interaction()
|
||||
|| self.backtrack.overlay_preview_active);
|
||||
let result = if main || (left && on_child) {
|
||||
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
|
||||
} else if (sidebar || right)
|
||||
&& !on_child
|
||||
&& let Some(child) = child
|
||||
&& (sidebar || self.side_threads[&child].pane_open)
|
||||
{
|
||||
let result = self.toggle_side_conversation(tui, app_server).await;
|
||||
let result = self.select_agent_thread(tui, app_server, child).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 {
|
||||
} else if hide || sidebar {
|
||||
self.hide_focused_side_pane(tui, app_server).await
|
||||
} else {
|
||||
// A direction with no reachable neighbor leaves the current interaction intact.
|
||||
Ok(())
|
||||
};
|
||||
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());
|
||||
let view_changed =
|
||||
leaves_transcript || self.current_displayed_thread_id() != previous_thread;
|
||||
let suppression = if view_changed {
|
||||
ActivationKeySuppression::ViewTransition
|
||||
} else {
|
||||
// A no-op leaves the editor in place, so prefer accepting fresh input.
|
||||
ActivationKeySuppression::ReportedRepeats
|
||||
};
|
||||
self.chat_widget.suppress_current_view_key_until_release(
|
||||
activation,
|
||||
Instant::now(),
|
||||
suppression,
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
@@ -3323,6 +3323,11 @@ async fn docked_replay_finishes_while_skills_refresh_is_pending() -> Result<()>
|
||||
#[tokio::test]
|
||||
async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()> {
|
||||
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
// Both pane footers should fit the same way on Unix and Windows.
|
||||
std::fs::write(
|
||||
&app.local_settings.user_config_path,
|
||||
"[tui]\nstatus_line = ['model-with-reasoning', 'thread-name']\n",
|
||||
)?;
|
||||
let config = app.chat_widget.config_ref().clone();
|
||||
let parent_thread_id = ThreadId::from_string(
|
||||
&app_test_support::create_fake_rollout(
|
||||
@@ -3652,8 +3657,10 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
|
||||
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));
|
||||
for completion in [main, KeyCode::Left.into()] {
|
||||
keys!(prefix, completion);
|
||||
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]
|
||||
@@ -3684,6 +3691,17 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
|
||||
app.chat_widget.composer_text_with_pending(),
|
||||
"parent draft-"
|
||||
);
|
||||
// With the pane hidden, right must leave both the Find text and its caret alone.
|
||||
app.transcript_view.begin_search();
|
||||
assert!(app.transcript_view.paste_search(parent_row));
|
||||
let (_, end) = app.transcript_view.search_footer(width).unwrap();
|
||||
keys!(KeyCode::Left.into());
|
||||
let footer = app.transcript_view.search_footer(width).unwrap();
|
||||
assert!(footer.1 < end);
|
||||
keys!(prefix, KeyCode::Right.into());
|
||||
assert_eq!(app.transcript_view.search_footer(width), Some(footer));
|
||||
assert!(!app.side_threads[&side_thread_id].pane_open);
|
||||
app.transcript_view.cancel_search();
|
||||
boxed_future!(app.handle_start_side(
|
||||
&mut tui,
|
||||
&mut app_server,
|
||||
@@ -3739,8 +3757,10 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
|
||||
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));
|
||||
for completion in [side, KeyCode::Right.into()] {
|
||||
keys!(prefix, completion);
|
||||
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);
|
||||
@@ -3753,6 +3773,45 @@ async fn handle_start_side_seeds_navigation_before_thread_started() -> Result<()
|
||||
);
|
||||
app.transcript_view.end_select_mode();
|
||||
|
||||
// At 99 columns only the focused conversation is painted, but both panes are still open.
|
||||
side_frame(&mut app, &mut tui, Size::new(/*width*/ 99, /*height*/ 18))?;
|
||||
assert!(app.transcript_view.begin_select_mode(&app.transcript_cells));
|
||||
keys!(KeyCode::Char('v').into(), KeyCode::Right.into());
|
||||
let selected = app.transcript_view.selected_text(&app.transcript_cells);
|
||||
assert!(selected.as_deref().is_some_and(|text| !text.is_empty()));
|
||||
keys!(prefix, KeyCode::Right.into());
|
||||
assert_eq!(
|
||||
app.transcript_view.selected_text(&app.transcript_cells),
|
||||
selected
|
||||
);
|
||||
|
||||
keys!(prefix, KeyCode::Char('h').into());
|
||||
assert_eq!(app.current_displayed_thread_id(), Some(parent_thread_id));
|
||||
assert!(app.side_threads[&side_thread_id].pane_open);
|
||||
side_frame(&mut app, &mut tui, Size::new(/*width*/ 99, /*height*/ 18))?;
|
||||
assert!(app.transcript_view.begin_select_mode(&app.transcript_cells));
|
||||
keys!(KeyCode::Char('v').into(), KeyCode::Right.into());
|
||||
let parent_text = app.transcript_view.selected_text(&app.transcript_cells);
|
||||
assert!(parent_text.as_deref().is_some_and(|text| !text.is_empty()));
|
||||
keys!(prefix, KeyCode::Left.into());
|
||||
assert_eq!(
|
||||
app.transcript_view.selected_text(&app.transcript_cells),
|
||||
parent_text
|
||||
);
|
||||
|
||||
keys!(prefix, KeyCode::Char('l').into());
|
||||
assert_eq!(app.current_displayed_thread_id(), Some(side_thread_id));
|
||||
assert_eq!(
|
||||
app.transcript_view.selected_text(&app.transcript_cells),
|
||||
selected
|
||||
);
|
||||
app.transcript_view.end_select_mode();
|
||||
// Ctrl-/ preserves interaction, so it exposes whether right actually cleaned up the parent.
|
||||
keys!(toggle);
|
||||
assert_eq!(app.current_displayed_thread_id(), Some(parent_thread_id));
|
||||
assert!(!app.transcript_view.has_active_interaction());
|
||||
keys!(toggle);
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1164,6 +1164,37 @@ async fn selection_after_live_commit_uses_the_refreshed_frame() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pane_direction_without_neighbor_preserves_fresh_typing() -> Result<()> {
|
||||
let (mut app, mut tui, mut app_server) = chord_app().await?;
|
||||
super::enable_side_retention(&mut app)?;
|
||||
tui.set_owned_screen(/*owned*/ true)?;
|
||||
|
||||
press(&mut app, &mut tui, &mut app_server, ctrl('x')).await?;
|
||||
press(
|
||||
&mut app,
|
||||
&mut tui,
|
||||
&mut app_server,
|
||||
KeyCode::Char('h').into(),
|
||||
)
|
||||
.await?;
|
||||
for ch in "hello".chars() {
|
||||
press(
|
||||
&mut app,
|
||||
&mut tui,
|
||||
&mut app_server,
|
||||
KeyCode::Char(ch).into(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
// Flush the burst of typed characters before inspecting the draft.
|
||||
press(&mut app, &mut tui, &mut app_server, KeyCode::End.into()).await?;
|
||||
|
||||
assert_eq!(app.chat_widget.composer_text_with_pending(), "hello");
|
||||
app_server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn key_capture_receives_voice_shortcut_and_chord_prefix() -> Result<()> {
|
||||
let (mut app, mut rx, _op_rx) = super::make_test_app_with_channels().await;
|
||||
@@ -1182,6 +1213,8 @@ async fn key_capture_receives_voice_shortcut_and_chord_prefix() -> Result<()> {
|
||||
("[global]\nhide_pane = 'f12'", 12),
|
||||
("[global]\nfocus_main_composer = 'f12'", 12),
|
||||
("[global]\nfocus_sidebar_content = 'f12 s'", 12),
|
||||
("[global]\nfocus_pane_left = 'f12 h'", 12),
|
||||
("[global]\nfocus_pane_right = 'f12 l'", 12),
|
||||
] {
|
||||
app.keymap =
|
||||
RuntimeKeymap::for_session(&toml::from_str(config)?, /*workspace*/ true).unwrap();
|
||||
@@ -1242,5 +1275,17 @@ async fn key_capture_receives_voice_shortcut_and_chord_prefix() -> Result<()> {
|
||||
press(&mut app, &mut tui, &mut stopped, key.into()).await?;
|
||||
}
|
||||
assert_eq!(app.chat_widget.composer_text_with_pending(), "draft!%");
|
||||
// Bare letters and arrows still edit the draft.
|
||||
for key in [
|
||||
KeyCode::Char('h'),
|
||||
KeyCode::Left,
|
||||
KeyCode::Char('l'),
|
||||
KeyCode::Right,
|
||||
KeyCode::Char('!'),
|
||||
KeyCode::Right,
|
||||
] {
|
||||
press(&mut app, &mut tui, &mut stopped, key.into()).await?;
|
||||
}
|
||||
assert_eq!(app.chat_widget.composer_text_with_pending(), "draft!%lh!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ pub(crate) use footer::inset_footer_hint_area;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::activation_key_guard::ActivationKeyGuard;
|
||||
use crate::activation_key_guard::ActivationKeySuppression;
|
||||
use crate::app::app_server_requests::ResolvedAppServerRequest;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event::ConnectorsSnapshot;
|
||||
@@ -2025,13 +2027,15 @@ impl BottomPane {
|
||||
&mut self,
|
||||
key: KeyBinding,
|
||||
activated_at: Instant,
|
||||
suppression: ActivationKeySuppression,
|
||||
) {
|
||||
self.view_transition_key_guard =
|
||||
Some(crate::activation_key_guard::ActivationKeyGuard::new_at(
|
||||
key,
|
||||
activated_at,
|
||||
self.enhanced_keys_supported,
|
||||
));
|
||||
let reported_repeats_only = self.enhanced_keys_supported
|
||||
|| matches!(suppression, ActivationKeySuppression::ReportedRepeats);
|
||||
self.view_transition_key_guard = Some(ActivationKeyGuard::new_at(
|
||||
key,
|
||||
activated_at,
|
||||
reported_repeats_only,
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn consume_view_transition_key_event(&mut self, key_event: KeyEvent) -> bool {
|
||||
@@ -2969,7 +2973,11 @@ mod tests {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let mut pane = test_pane(AppEventSender::new(tx_raw));
|
||||
let activation_key = crate::key_hint::plain(KeyCode::Enter);
|
||||
pane.suppress_current_view_key_until_release(activation_key, Instant::now());
|
||||
pane.suppress_current_view_key_until_release(
|
||||
activation_key,
|
||||
Instant::now(),
|
||||
ActivationKeySuppression::ViewTransition,
|
||||
);
|
||||
pane.push_view(Box::new(DismissibleView::default()));
|
||||
|
||||
assert!(pane.consume_view_transition_key(activation_key, KeyEventKind::Repeat));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Entry points for the configuration browser and its existing child editors.
|
||||
|
||||
use super::ChatWidget;
|
||||
use crate::activation_key_guard::ActivationKeySuppression;
|
||||
use crate::config_panel::ConfigEditor;
|
||||
use crate::config_panel::ConfigEditorHandoff;
|
||||
use crate::config_panel::ConfigPanel;
|
||||
@@ -69,7 +70,11 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
if let Some(activated_at) = handoff.active_at() {
|
||||
self.suppress_current_view_key_until_release(handoff.activation_key, activated_at);
|
||||
self.suppress_current_view_key_until_release(
|
||||
handoff.activation_key,
|
||||
activated_at,
|
||||
ActivationKeySuppression::ViewTransition,
|
||||
);
|
||||
}
|
||||
self.bottom_pane.config_editor_opened();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use super::clipboard::PendingCopy;
|
||||
use super::*;
|
||||
use crate::activation_key_guard::ActivationKeySuppression;
|
||||
use crate::bottom_pane::BottomPaneView;
|
||||
use crate::clipboard_copy::CopyStatus;
|
||||
use crate::clipboard_copy::worker::CopyResult;
|
||||
@@ -91,9 +92,10 @@ impl ChatWidget {
|
||||
&mut self,
|
||||
key: KeyBinding,
|
||||
activated_at: Instant,
|
||||
suppression: ActivationKeySuppression,
|
||||
) {
|
||||
self.bottom_pane
|
||||
.suppress_current_view_key_until_release(key, activated_at);
|
||||
.suppress_current_view_key_until_release(key, activated_at, suppression);
|
||||
}
|
||||
|
||||
pub(crate) fn consume_view_transition_key_event(&mut self, key_event: KeyEvent) -> bool {
|
||||
|
||||
@@ -587,7 +587,7 @@ impl ChatWidget {
|
||||
Some(crate::activation_key_guard::ActivationKeyGuard::new_at(
|
||||
KeyBinding::from_event(key),
|
||||
Instant::now(),
|
||||
/*key_releases_reported*/ false,
|
||||
/*reported_repeats_only*/ false,
|
||||
));
|
||||
}
|
||||
consumed
|
||||
|
||||
@@ -107,6 +107,8 @@ pub(crate) struct AppKeymap {
|
||||
pub(crate) focus_main_composer: Vec<KeyBinding>,
|
||||
pub(crate) focus_sidebar_content: Vec<KeyBinding>,
|
||||
pub(crate) hide_pane: Vec<KeyBinding>,
|
||||
pub(crate) focus_pane_left: Vec<KeyBinding>,
|
||||
pub(crate) focus_pane_right: Vec<KeyBinding>,
|
||||
/// Open the agent command center.
|
||||
pub(crate) open_agents: Vec<KeyBinding>,
|
||||
/// Open transcript overlay.
|
||||
@@ -690,6 +692,19 @@ impl RuntimeKeymap {
|
||||
}
|
||||
|
||||
fn resolve(keymap: &TuiKeymap, workspace: bool) -> Result<Self, String> {
|
||||
// Inactive pane shortcuts must not claim keys or chord prefixes in classic mode.
|
||||
let mut classic_keymap;
|
||||
let keymap = if workspace {
|
||||
keymap
|
||||
} else {
|
||||
classic_keymap = keymap.clone();
|
||||
classic_keymap.global.focus_main_composer = None;
|
||||
classic_keymap.global.focus_sidebar_content = None;
|
||||
classic_keymap.global.hide_pane = None;
|
||||
classic_keymap.global.focus_pane_left = None;
|
||||
classic_keymap.global.focus_pane_right = None;
|
||||
&classic_keymap
|
||||
};
|
||||
let defaults = Self::built_in_defaults();
|
||||
let chords = Arc::new(RuntimeChordKeymap::from_config(keymap)?);
|
||||
let side_toggle_default_is_shadowed = keymap.global.toggle_side_conversation.is_none()
|
||||
@@ -756,6 +771,16 @@ impl RuntimeKeymap {
|
||||
&[],
|
||||
"tui.keymap.global.hide_pane",
|
||||
)?,
|
||||
focus_pane_left: resolve_bindings(
|
||||
keymap.global.focus_pane_left.as_ref(),
|
||||
&[],
|
||||
"tui.keymap.global.focus_pane_left",
|
||||
)?,
|
||||
focus_pane_right: resolve_bindings(
|
||||
keymap.global.focus_pane_right.as_ref(),
|
||||
&[],
|
||||
"tui.keymap.global.focus_pane_right",
|
||||
)?,
|
||||
open_agents: resolve_bindings(
|
||||
keymap.global.open_agents.as_ref(),
|
||||
&defaults.app.open_agents,
|
||||
@@ -1734,6 +1759,8 @@ impl RuntimeKeymap {
|
||||
focus_main_composer: Vec::new(),
|
||||
focus_sidebar_content: Vec::new(),
|
||||
hide_pane: Vec::new(),
|
||||
focus_pane_left: Vec::new(),
|
||||
focus_pane_right: Vec::new(),
|
||||
open_agents: default_bindings![],
|
||||
open_transcript: default_bindings![ctrl(KeyCode::Char('t'))],
|
||||
find_transcript: default_bindings![plain(KeyCode::F(3))],
|
||||
@@ -2090,6 +2117,8 @@ impl RuntimeKeymap {
|
||||
"global.focus_sidebar_content",
|
||||
&self.app.focus_sidebar_content,
|
||||
),
|
||||
("global.focus_pane_left", &self.app.focus_pane_left),
|
||||
("global.focus_pane_right", &self.app.focus_pane_right),
|
||||
("global.hide_pane", &self.app.hide_pane),
|
||||
("chat.open_presets", &self.chat.open_presets),
|
||||
("chat.toggle_voice", &self.chat.toggle_voice),
|
||||
@@ -2128,6 +2157,8 @@ impl RuntimeKeymap {
|
||||
"focus_sidebar_content",
|
||||
self.app.focus_sidebar_content.as_slice(),
|
||||
),
|
||||
("focus_pane_left", self.app.focus_pane_left.as_slice()),
|
||||
("focus_pane_right", self.app.focus_pane_right.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()),
|
||||
@@ -2305,6 +2336,8 @@ impl RuntimeKeymap {
|
||||
"focus_sidebar_content",
|
||||
self.app.focus_sidebar_content.as_slice(),
|
||||
),
|
||||
("focus_pane_left", self.app.focus_pane_left.as_slice()),
|
||||
("focus_pane_right", self.app.focus_pane_right.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()),
|
||||
@@ -2439,6 +2472,14 @@ impl RuntimeKeymap {
|
||||
"global.focus_sidebar_content",
|
||||
self.app.focus_sidebar_content.as_slice(),
|
||||
),
|
||||
(
|
||||
"global.focus_pane_left",
|
||||
self.app.focus_pane_left.as_slice(),
|
||||
),
|
||||
(
|
||||
"global.focus_pane_right",
|
||||
self.app.focus_pane_right.as_slice(),
|
||||
),
|
||||
("global.hide_pane", self.app.hide_pane.as_slice()),
|
||||
(
|
||||
"global.select_transcript",
|
||||
@@ -2480,6 +2521,14 @@ impl RuntimeKeymap {
|
||||
"global.focus_sidebar_content",
|
||||
self.app.focus_sidebar_content.as_slice(),
|
||||
),
|
||||
(
|
||||
"global.focus_pane_left",
|
||||
self.app.focus_pane_left.as_slice(),
|
||||
),
|
||||
(
|
||||
"global.focus_pane_right",
|
||||
self.app.focus_pane_right.as_slice(),
|
||||
),
|
||||
("global.hide_pane", self.app.hide_pane.as_slice()),
|
||||
("global.focus_activity", self.app.focus_activity.as_slice()),
|
||||
(
|
||||
@@ -3947,6 +3996,8 @@ mod tests {
|
||||
("hide_pane", "list", "move_down"),
|
||||
("focus_main_composer", "pager", "page_up"),
|
||||
("focus_sidebar_content", "list", "move_down"),
|
||||
("focus_pane_left", "list", "move_down"),
|
||||
("focus_pane_right", "pager", "page_up"),
|
||||
] {
|
||||
let config: TuiKeymap = toml::from_str(&format!(
|
||||
"[global]\n{pane} = 'f8'\n[{section}]\n{action} = 'f8'"
|
||||
@@ -3959,7 +4010,13 @@ mod tests {
|
||||
);
|
||||
}
|
||||
#[cfg(unix)]
|
||||
for action in ["hide_pane", "focus_main_composer", "focus_sidebar_content"] {
|
||||
for action in [
|
||||
"hide_pane",
|
||||
"focus_main_composer",
|
||||
"focus_sidebar_content",
|
||||
"focus_pane_left",
|
||||
"focus_pane_right",
|
||||
] {
|
||||
let config: TuiKeymap =
|
||||
toml::from_str(&format!("[global]\n{action} = 'ctrl-z'")).unwrap();
|
||||
assert!(
|
||||
@@ -3997,6 +4054,24 @@ mod tests {
|
||||
assert!(runtime.app.toggle_side_conversation.is_empty());
|
||||
}
|
||||
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.global.open_transcript = Some(one("leader left"));
|
||||
keymap.global.focus_pane_right = Some(one("f12 b"));
|
||||
let runtime = RuntimeKeymap::for_session(&keymap, /*workspace*/ true).unwrap();
|
||||
for (action, expected) in [
|
||||
("focus_pane_left", "leader h"),
|
||||
("focus_pane_right", "f12 b"),
|
||||
] {
|
||||
let installed: Vec<_> = runtime
|
||||
.chords
|
||||
.bindings
|
||||
.iter()
|
||||
.filter(|binding| binding.action.action == action)
|
||||
.map(|binding| binding.spec.as_str())
|
||||
.collect();
|
||||
assert_eq!(installed, [expected], "{action}");
|
||||
}
|
||||
|
||||
let mut keymap = TuiKeymap::default();
|
||||
keymap.global.open_transcript = Some(one("ctrl-/"));
|
||||
keymap.global.copy = Some(one("ctrl-7"));
|
||||
@@ -4006,6 +4081,22 @@ mod tests {
|
||||
assert!(runtime.app.toggle_side_conversation.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pane_binding_only_shadows_side_toggle_in_workspace_sessions() {
|
||||
let keymap = toml::from_str("[global]\nfocus_pane_right = 'ctrl-/'").unwrap();
|
||||
let classic = RuntimeKeymap::for_session(&keymap, /*workspace*/ false).unwrap();
|
||||
assert_eq!(
|
||||
classic.app.toggle_side_conversation,
|
||||
RuntimeKeymap::defaults().app.toggle_side_conversation
|
||||
);
|
||||
let workspace = RuntimeKeymap::for_session(&keymap, /*workspace*/ true).unwrap();
|
||||
assert!(workspace.app.toggle_side_conversation.is_empty());
|
||||
assert_eq!(
|
||||
workspace.app.focus_pane_right,
|
||||
vec![key_hint::ctrl(KeyCode::Char('/'))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_side_toggle_rejects_legacy_ctrl_slash_alias_conflicts() {
|
||||
let mut keymap = TuiKeymap::default();
|
||||
@@ -4162,8 +4253,9 @@ mod tests {
|
||||
assert!(runtime.composer.toggle_shortcuts.is_empty());
|
||||
keymap.global.hide_pane = Some(KeybindingsSpec::Many(vec![]));
|
||||
keymap.global.focus_sidebar_content = Some(KeybindingsSpec::Many(vec![]));
|
||||
keymap.global.focus_pane_right = Some(KeybindingsSpec::Many(vec![]));
|
||||
let runtime = RuntimeKeymap::for_session(&keymap, /*workspace*/ true).unwrap();
|
||||
for action in ["hide_pane", "focus_sidebar_content"] {
|
||||
for action in ["hide_pane", "focus_sidebar_content", "focus_pane_right"] {
|
||||
assert!(
|
||||
runtime
|
||||
.primary_hint(KeymapContext::Global, action)
|
||||
|
||||
@@ -107,7 +107,11 @@ impl KeymapActionId {
|
||||
self.context == KeymapContext::Global
|
||||
&& matches!(
|
||||
self.action,
|
||||
"focus_main_composer" | "focus_sidebar_content" | "hide_pane"
|
||||
"focus_main_composer"
|
||||
| "focus_sidebar_content"
|
||||
| "hide_pane"
|
||||
| "focus_pane_left"
|
||||
| "focus_pane_right"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -306,6 +310,8 @@ define_runtime_action_bindings! {
|
||||
focus_main_composer,
|
||||
focus_sidebar_content,
|
||||
hide_pane,
|
||||
focus_pane_left,
|
||||
focus_pane_right,
|
||||
open_warnings => runtime_only,
|
||||
open_external_editor,
|
||||
copy,
|
||||
|
||||
@@ -56,6 +56,7 @@ 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 PANE_DIRECTION: u32 = 1 << 24;
|
||||
|
||||
const TRANSCRIPT_BROWSING: u32 = 1 << 14;
|
||||
|
||||
@@ -158,6 +159,10 @@ impl KeymapContextSet {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn with_pane_directions(self) -> Self {
|
||||
Self(self.0 | PANE_DIRECTION)
|
||||
}
|
||||
|
||||
pub(crate) const fn with_main_composer_focus(self) -> Self {
|
||||
Self(self.0 | MAIN_COMPOSER_FOCUS)
|
||||
}
|
||||
@@ -172,10 +177,10 @@ impl KeymapContextSet {
|
||||
return self.0 & BACKEND_CYCLING != 0;
|
||||
}
|
||||
if action.is_side_pane() {
|
||||
let required = if action.action == "focus_main_composer" {
|
||||
MAIN_COMPOSER_FOCUS
|
||||
} else {
|
||||
SIDE_PANE
|
||||
let required = match action.action {
|
||||
"focus_pane_left" | "focus_pane_right" => PANE_DIRECTION,
|
||||
"focus_main_composer" => MAIN_COMPOSER_FOCUS,
|
||||
_ => SIDE_PANE,
|
||||
};
|
||||
return self.0 & required != 0;
|
||||
}
|
||||
|
||||
@@ -26,15 +26,20 @@ pub(in crate::keymap) fn register(
|
||||
.filter(|binding| binding.is_leader())
|
||||
.count(),
|
||||
);
|
||||
use KeyCode::Char;
|
||||
use KeymapContext::Chat;
|
||||
use KeymapContext::Global;
|
||||
for (context, name, completion) in [
|
||||
(Global, "change_host", 'b'),
|
||||
(Global, "select_transcript", '['),
|
||||
(Chat, "open_presets", 'p'),
|
||||
(Global, "focus_main_composer", 'c'),
|
||||
(Global, "focus_sidebar_content", 's'),
|
||||
(Global, "hide_pane", '-'),
|
||||
for (context, name, completion, spec) in [
|
||||
(Global, "change_host", Char('b'), "b"),
|
||||
(Global, "select_transcript", Char('['), "["),
|
||||
(Chat, "open_presets", Char('p'), "p"),
|
||||
(Global, "focus_main_composer", Char('c'), "c"),
|
||||
(Global, "focus_sidebar_content", Char('s'), "s"),
|
||||
(Global, "hide_pane", Char('-'), "-"),
|
||||
(Global, "focus_pane_left", KeyCode::Left, "left"),
|
||||
(Global, "focus_pane_left", Char('h'), "h"),
|
||||
(Global, "focus_pane_right", KeyCode::Right, "right"),
|
||||
(Global, "focus_pane_right", Char('l'), "l"),
|
||||
] {
|
||||
if remaining == 0 {
|
||||
break;
|
||||
@@ -56,9 +61,9 @@ pub(in crate::keymap) fn register(
|
||||
action,
|
||||
chord: KeyChord {
|
||||
prefix,
|
||||
completion: crate::key_hint::plain(KeyCode::Char(completion)),
|
||||
completion: crate::key_hint::plain(completion),
|
||||
},
|
||||
spec: format!("leader {completion}"),
|
||||
spec: format!("leader {spec}"),
|
||||
};
|
||||
if !resolved.chords.bindings.iter().any(|existing| {
|
||||
binding.overlaps(existing.action) && existing.chord == binding.chord
|
||||
|
||||
@@ -187,6 +187,42 @@ fn allows_shared_prefixes_with_distinct_completions() {
|
||||
config.global.copy = Some(binding("ctrl-x ctrl-o"));
|
||||
|
||||
RuntimeKeymap::from_config(&config).expect("prefix trie can branch on the second stroke");
|
||||
|
||||
// With 16 leaders and 56 explicit completions, only eight more aliases fit per leader.
|
||||
let mut config = TuiKeymap::default();
|
||||
config.global.leader = Some(KeybindingsSpec::Many(
|
||||
(1..=16)
|
||||
.map(|n| KeybindingSpec(format!("super-f{n}")))
|
||||
.collect(),
|
||||
));
|
||||
config.global.open_transcript = Some(KeybindingsSpec::Many(
|
||||
["", "shift-", "alt-"]
|
||||
.into_iter()
|
||||
.flat_map(|modifier| {
|
||||
(1..=19).map(move |n| KeybindingSpec(format!("leader {modifier}f{n}")))
|
||||
})
|
||||
.take(56)
|
||||
.collect(),
|
||||
));
|
||||
let runtime = RuntimeKeymap::for_session(&config, /*workspace*/ true).unwrap();
|
||||
assert_eq!(
|
||||
runtime
|
||||
.chords
|
||||
.bindings
|
||||
.iter()
|
||||
.filter(|b| b.is_leader())
|
||||
.count(),
|
||||
super::MAX_LEADER_CHORD_BINDINGS
|
||||
);
|
||||
assert_eq!(
|
||||
crate::keymap_setup::active_binding_specs(&runtime, "global", "focus_pane_left").unwrap(),
|
||||
["leader left", "leader h"]
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.primary_hint(KeymapContext::Global, "focus_pane_right")
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -121,6 +121,8 @@ define_keymap_actions! {
|
||||
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.",
|
||||
focus_pane_left [Workspace] => "Focus the parent conversation.",
|
||||
focus_pane_right [Workspace] => "Focus the open side conversation.",
|
||||
open_agents => "Open the agent command center.",
|
||||
open_transcript => "Open the transcript overlay.",
|
||||
find_transcript => "Find text in the full transcript.",
|
||||
|
||||
@@ -441,7 +441,7 @@ impl AuthModeWidget {
|
||||
self.activation_key_guard = Some(ActivationKeyGuard::new_at(
|
||||
KeyBinding::from_event(key_event),
|
||||
Instant::now(),
|
||||
/*key_releases_reported*/ false,
|
||||
/*reported_repeats_only*/ false,
|
||||
));
|
||||
}
|
||||
self.handle_sign_in_option(option);
|
||||
|
||||
+3
-3
@@ -19,7 +19,7 @@ parent-29
|
||||
|
||||
› parent draft
|
||||
|
||||
GPT-6.1-Sol default · /tmp/project · Main [default]
|
||||
GPT-6.1-Sol default · Main [default]
|
||||
New activity · esc latest
|
||||
|
||||
100 columns, reopened
|
||||
@@ -39,7 +39,7 @@ parent-29 │ child-29
|
||||
│
|
||||
› parent draft- │ › child draft!
|
||||
│
|
||||
GPT-6.1-Sol default · /tmp/project │ GPT-6.1-Sol default · /tmp/projec…
|
||||
GPT-6.1-Sol default │ GPT-6.1-Sol default · Agent
|
||||
│ Side from main thread · ⌃/ to swit
|
||||
|
||||
99 columns, reopened
|
||||
@@ -59,5 +59,5 @@ child-29
|
||||
|
||||
› child draft!
|
||||
|
||||
GPT-6.1-Sol default · /tmp/project · Agent
|
||||
GPT-6.1-Sol default · Agent
|
||||
Side from main thread · ⌃/ to switch · ⌃c to close
|
||||
|
||||
@@ -18,4 +18,4 @@ parent-29 │ child-29
|
||||
│
|
||||
› parent draft │ › child draft!
|
||||
│
|
||||
GPT-6.1-Sol default · /tmp/project · Main [default] │ GPT-6.1-Sol default · /tmp/project
|
||||
GPT-6.1-Sol default · Main [default] │ GPT-6.1-Sol default
|
||||
|
||||
+2
-2
@@ -5,6 +5,8 @@ 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
|
||||
Focus Pane Left | leader left (ctrl-x left), leader h (ctrl-x h) | Global focus_pane_left Focus Pane Left Focus the parent conversation. leader left (ctrl-x left), leader h (ctrl-x h) Default
|
||||
Focus Pane Right | leader right (ctrl-x right), leader l (ctrl-x l) | Global focus_pane_right Focus Pane Right Focus the open side conversation. leader right (ctrl-x right), leader l (ctrl-x l) 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
|
||||
Find Transcript | f3 | Global find_transcript Find Transcript Find text in the full transcript. f3 Default
|
||||
@@ -13,5 +15,3 @@ Open External Editor | ctrl-g | Global open_external_editor Open External Editor
|
||||
Copy | ctrl-o | Global copy Copy Copy the last agent response to the clipboard. ctrl-o Default
|
||||
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
|
||||
|
||||
@@ -1314,6 +1314,7 @@ fn user_prompt_has_a_background_without_a_terminal_palette() {
|
||||
#[test]
|
||||
fn spoken_prompt_selection_keeps_its_fallback_contrast() {
|
||||
use crate::history_cell::new_spoken_user_prompt;
|
||||
use crate::terminal_palette::rgb_color;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
|
||||
@@ -1352,8 +1353,8 @@ fn spoken_prompt_selection_keeps_its_fallback_contrast() {
|
||||
(
|
||||
Some("one\n"),
|
||||
Color::Red,
|
||||
Color::Rgb(238, 238, 238),
|
||||
Color::Rgb(68, 68, 68),
|
||||
rgb_color((238, 238, 238)),
|
||||
rgb_color((68, 68, 68)),
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user