feat(tui/footer): show one-time Shift+Up/Down history hint on first scroll

Add a 6s footer hint "Use Shift+Up/Down to use previous input" when the user scrolls for the first time. Triggered by PageUp/PageDown, mouse wheel, or Up/Down scrolling. Implemented via a new bottom-pane/composer API to display timed footer notices.

Closes #59
This commit is contained in:
James Peter
2025-09-08 10:49:38 +10:00
parent 16b7800501
commit 9a4bddc748
4 changed files with 39 additions and 0 deletions
@@ -212,6 +212,12 @@ impl ChatComposer {
self.footer_notice = Some((text, expiry));
}
/// Show a footer notice for a specific duration.
pub fn flash_footer_notice_for(&mut self, text: String, dur: std::time::Duration) {
let expiry = std::time::Instant::now() + dur;
self.footer_notice = Some((text, expiry));
}
// Control footer hint visibility
pub fn set_show_reasoning_hint(&mut self, show: bool) {
if self.show_reasoning_hint != show {
+9
View File
@@ -297,6 +297,15 @@ impl BottomPane<'_> {
self.request_redraw();
}
/// Show an ephemeral footer notice for a custom duration.
pub(crate) fn flash_footer_notice_for(&mut self, text: String, dur: Duration) {
self.composer.flash_footer_notice_for(text, dur);
// Ask app to clear it slightly after expiry to avoid flicker on boundary
self.app_event_tx
.send(AppEvent::ScheduleFrameIn(dur + Duration::from_millis(100)));
self.request_redraw();
}
pub(crate) fn show_ctrl_c_quit_hint(&mut self) {
self.ctrl_c_quit_hint = true;
self.composer
+19
View File
@@ -266,6 +266,9 @@ pub(crate) struct ChatWidget<'a> {
internal_seq: u64,
// Show order overlay when true (from --order)
show_order_overlay: bool,
// One-time hint to teach input history navigation
scroll_history_hint_shown: bool,
}
struct PendingJumpBack {
@@ -462,6 +465,16 @@ impl ChatWidget<'_> {
OrderKey { req, out: i32::MAX, seq: self.internal_seq }
}
/// Show the "Shift+Up/Down" input history hint the first time the user scrolls.
pub(super) fn maybe_show_history_nav_hint_on_first_scroll(&mut self) {
if self.scroll_history_hint_shown { return; }
self.scroll_history_hint_shown = true;
self.bottom_pane.flash_footer_notice_for(
"Use Shift+Up/Down to use previous input".to_string(),
std::time::Duration::from_secs(6),
);
}
// Synthetic key for internal content that should appear at the TOP of the NEXT request
// (e.g., the user’s prompt preceding the model’s output for that turn).
fn next_req_key_top(&mut self) -> OrderKey {
@@ -1248,6 +1261,7 @@ impl ChatWidget<'_> {
current_request_index: 0,
internal_seq: 0,
show_order_overlay,
scroll_history_hint_shown: false,
};
// Insert the welcome cell as top-of-first-request so future model output
// appears below it.
@@ -1371,6 +1385,7 @@ impl ChatWidget<'_> {
current_request_index: 0,
internal_seq: 0,
show_order_overlay,
scroll_history_hint_shown: false,
};
// Welcome at top of first request for forked session too
@@ -1520,6 +1535,7 @@ impl ChatWidget<'_> {
self.height_manager
.borrow_mut()
.record_event(HeightEvent::UserScroll);
self.maybe_show_history_nav_hint_on_first_scroll();
}
InputResult::ScrollDown => {
// Only allow Down to navigate command history when the top view
@@ -1535,6 +1551,7 @@ impl ChatWidget<'_> {
self.height_manager
.borrow_mut()
.record_event(HeightEvent::UserScroll);
self.maybe_show_history_nav_hint_on_first_scroll();
self.height_manager
.borrow_mut()
.record_event(HeightEvent::ComposerModeChange);
@@ -1546,6 +1563,7 @@ impl ChatWidget<'_> {
self.height_manager
.borrow_mut()
.record_event(HeightEvent::UserScroll);
self.maybe_show_history_nav_hint_on_first_scroll();
} else if self.layout.scroll_offset > 0 {
// Land exactly at bottom without toggling spacer yet; require
// a subsequent Down to re-enable the spacer so the input
@@ -1555,6 +1573,7 @@ impl ChatWidget<'_> {
self.height_manager
.borrow_mut()
.record_event(HeightEvent::UserScroll);
self.maybe_show_history_nav_hint_on_first_scroll();
}
self.flash_scrollbar();
}
@@ -25,6 +25,7 @@ pub(super) fn page_up(chat: &mut ChatWidget<'_>) {
flash_scrollbar(chat);
chat.app_event_tx.send(crate::app_event::AppEvent::RequestRedraw);
chat.height_manager.borrow_mut().record_event(crate::height_manager::HeightEvent::UserScroll);
chat.maybe_show_history_nav_hint_on_first_scroll();
}
pub(super) fn page_down(chat: &mut ChatWidget<'_>) {
@@ -38,6 +39,7 @@ pub(super) fn page_down(chat: &mut ChatWidget<'_>) {
flash_scrollbar(chat);
chat.app_event_tx.send(crate::app_event::AppEvent::RequestRedraw);
chat.height_manager.borrow_mut().record_event(crate::height_manager::HeightEvent::UserScroll);
chat.maybe_show_history_nav_hint_on_first_scroll();
}
pub(super) fn mouse_scroll(chat: &mut ChatWidget<'_>, up: bool) {
@@ -52,13 +54,16 @@ pub(super) fn mouse_scroll(chat: &mut ChatWidget<'_>, up: bool) {
chat.bottom_pane.set_compact_compose(true);
}
chat.app_event_tx.send(crate::app_event::AppEvent::RequestRedraw);
chat.maybe_show_history_nav_hint_on_first_scroll();
} else {
if chat.layout.scroll_offset >= 3 {
chat.layout.scroll_offset = chat.layout.scroll_offset.saturating_sub(3);
chat.app_event_tx.send(crate::app_event::AppEvent::RequestRedraw);
chat.maybe_show_history_nav_hint_on_first_scroll();
} else if chat.layout.scroll_offset > 0 {
chat.layout.scroll_offset = 0;
chat.app_event_tx.send(crate::app_event::AppEvent::RequestRedraw);
chat.maybe_show_history_nav_hint_on_first_scroll();
}
flash_scrollbar(chat);
if chat.layout.scroll_offset == 0 {