fix(tui): change transcript 'g' scroll to vim-style 'gg' double-tap
A single bare 'g' with an empty composer was hijacked as a scroll-to-top command, preventing users from typing 'g' as the first character of a message. The transcript would jump to line 0 instead of inserting 'g' into the composer. Change to a vim-style 'gg' double-tap: first 'g' arms transcript_pending_g, second 'g' executes the scroll. Any other character input, Enter, or Escape resets the pending flag so a stray 'g' during composition arms without scrolling. Also adds transcript_pending_g field to App struct (default false).
This commit is contained in:
@@ -878,6 +878,12 @@ pub struct App {
|
||||
/// Esc primes, second Esc opens the live-transcript overlay scoped to
|
||||
/// previous user messages so the user can rewind a turn.
|
||||
pub backtrack: crate::tui::backtrack::BacktrackState,
|
||||
/// Pending 'gg' second keystroke for vim-style jump-to-top of the
|
||||
/// transcript. Set by the first bare 'g' with an empty composer and
|
||||
/// consumed on the second 'g' to scroll to line 0. Any other
|
||||
/// navigation key resets it, so typing 'g' as the first character
|
||||
/// of a message only arms this flag without scrolling.
|
||||
pub transcript_pending_g: bool,
|
||||
/// Current session ID for auto-save updates
|
||||
pub current_session_id: Option<String>,
|
||||
/// Metadata-only registry of large tool outputs produced in this session.
|
||||
@@ -1465,6 +1471,7 @@ impl App {
|
||||
},
|
||||
view_stack: ViewStack::new(),
|
||||
backtrack: crate::tui::backtrack::BacktrackState::new(),
|
||||
transcript_pending_g: false,
|
||||
current_session_id: None,
|
||||
session_artifacts: Vec::new(),
|
||||
trust_mode: initial_mode == AppMode::Yolo,
|
||||
|
||||
@@ -2391,7 +2391,9 @@ async fn run_event_loop(
|
||||
app.mention_menu_hidden = true;
|
||||
app.mention_menu_selected = 0;
|
||||
}
|
||||
KeyCode::Esc => match next_escape_action(app, slash_menu_open) {
|
||||
KeyCode::Esc => {
|
||||
app.transcript_pending_g = false;
|
||||
match next_escape_action(app, slash_menu_open) {
|
||||
EscapeAction::CloseSlashMenu => {
|
||||
// A popup-style action wins over backtrack — clear
|
||||
// any prime so a stale Primed state can't jump us
|
||||
@@ -2460,7 +2462,8 @@ async fn run_event_loop(
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
KeyCode::Up if key.modifiers.contains(KeyModifiers::SUPER) => {
|
||||
app.scroll_up(app.viewport.last_transcript_visible.max(3));
|
||||
}
|
||||
@@ -2572,10 +2575,20 @@ async fn run_event_loop(
|
||||
KeyCode::Char('g')
|
||||
if key.modifiers.is_empty() && app.input.is_empty() && !slash_menu_open =>
|
||||
{
|
||||
if let Some(anchor) =
|
||||
TranscriptScroll::anchor_for(app.viewport.transcript_cache.line_meta(), 0)
|
||||
{
|
||||
app.viewport.transcript_scroll = anchor;
|
||||
// Vim-style 'gg' — double-tap 'g' to jump to top.
|
||||
// First 'g' arms the pending flag; second executes the scroll.
|
||||
// This prevents a single 'g' (the first letter of a message)
|
||||
// from hijacking the transcript scroll.
|
||||
if app.transcript_pending_g {
|
||||
if let Some(anchor) = TranscriptScroll::anchor_for(
|
||||
app.viewport.transcript_cache.line_meta(),
|
||||
0,
|
||||
) {
|
||||
app.viewport.transcript_scroll = anchor;
|
||||
}
|
||||
app.transcript_pending_g = false;
|
||||
} else {
|
||||
app.transcript_pending_g = true;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('G')
|
||||
@@ -2675,6 +2688,7 @@ async fn run_event_loop(
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
app.transcript_pending_g = false;
|
||||
// #573: when the user typed a slash-command prefix that
|
||||
// the popup is matching (e.g. `/mo` → `/model`), Enter
|
||||
// should run the *highlighted match* rather than
|
||||
@@ -2986,6 +3000,11 @@ async fn run_event_loop(
|
||||
// absorb — Visual mode not yet fully implemented
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
// Any typed character after a pending 'g' means the user
|
||||
// is composing a message, not navigating — disarm the
|
||||
// 'gg' double-tap so the next bare 'g' on an empty
|
||||
// composer starts a new sequence.
|
||||
app.transcript_pending_g = false;
|
||||
app.insert_char(c);
|
||||
}
|
||||
_ => {}
|
||||
|
||||
Reference in New Issue
Block a user