Add spacing before composer footer hints (#3469)

<img width="647" height="82" alt="image"
src="https://github.com/user-attachments/assets/867eb5d9-3076-4018-846e-260a50408185"
/>
This commit is contained in:
Ahmed Ibrahim
2025-09-12 15:31:24 -04:00
committed by GitHub
parent 1a6a95fb2a
commit d4848e558b
9 changed files with 92 additions and 19 deletions

View File

@@ -95,6 +95,10 @@ enum ActivePopup {
File(FileSearchPopup),
}
const FOOTER_HINT_HEIGHT: u16 = 1;
const FOOTER_SPACING_HEIGHT: u16 = 1;
const FOOTER_HEIGHT_WITH_HINT: u16 = FOOTER_HINT_HEIGHT + FOOTER_SPACING_HEIGHT;
impl ChatComposer {
pub fn new(
has_input_focus: bool,
@@ -134,20 +138,20 @@ impl ChatComposer {
pub fn desired_height(&self, width: u16) -> u16 {
self.textarea.desired_height(width - 1)
+ match &self.active_popup {
ActivePopup::None => 1u16,
ActivePopup::None => FOOTER_HEIGHT_WITH_HINT,
ActivePopup::Command(c) => c.calculate_required_height(),
ActivePopup::File(c) => c.calculate_required_height(),
}
}
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
let popup_height = match &self.active_popup {
ActivePopup::Command(popup) => popup.calculate_required_height(),
ActivePopup::File(popup) => popup.calculate_required_height(),
ActivePopup::None => 1,
let popup_constraint = match &self.active_popup {
ActivePopup::Command(popup) => Constraint::Max(popup.calculate_required_height()),
ActivePopup::File(popup) => Constraint::Max(popup.calculate_required_height()),
ActivePopup::None => Constraint::Max(FOOTER_HEIGHT_WITH_HINT),
};
let [textarea_rect, _] =
Layout::vertical([Constraint::Min(1), Constraint::Max(popup_height)]).areas(area);
Layout::vertical([Constraint::Min(1), popup_constraint]).areas(area);
let mut textarea_rect = textarea_rect;
textarea_rect.width = textarea_rect.width.saturating_sub(1);
textarea_rect.x += 1;
@@ -1223,13 +1227,16 @@ impl ChatComposer {
impl WidgetRef for ChatComposer {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let popup_height = match &self.active_popup {
ActivePopup::Command(popup) => popup.calculate_required_height(),
ActivePopup::File(popup) => popup.calculate_required_height(),
ActivePopup::None => 1,
let (popup_constraint, hint_spacing) = match &self.active_popup {
ActivePopup::Command(popup) => (Constraint::Max(popup.calculate_required_height()), 0),
ActivePopup::File(popup) => (Constraint::Max(popup.calculate_required_height()), 0),
ActivePopup::None => (
Constraint::Length(FOOTER_HEIGHT_WITH_HINT),
FOOTER_SPACING_HEIGHT,
),
};
let [textarea_rect, popup_rect] =
Layout::vertical([Constraint::Min(1), Constraint::Max(popup_height)]).areas(area);
Layout::vertical([Constraint::Min(1), popup_constraint]).areas(area);
match &self.active_popup {
ActivePopup::Command(popup) => {
popup.render_ref(popup_rect, buf);
@@ -1238,7 +1245,16 @@ impl WidgetRef for ChatComposer {
popup.render_ref(popup_rect, buf);
}
ActivePopup::None => {
let bottom_line_rect = popup_rect;
let hint_rect = if hint_spacing > 0 {
let [_, hint_rect] = Layout::vertical([
Constraint::Length(hint_spacing),
Constraint::Length(FOOTER_HINT_HEIGHT),
])
.areas(popup_rect);
hint_rect
} else {
popup_rect
};
let mut hint: Vec<Span<'static>> = if self.ctrl_c_quit_hint {
let ctrl_c_followup = if self.is_task_running {
" to interrupt"
@@ -1309,7 +1325,7 @@ impl WidgetRef for ChatComposer {
Line::from(hint)
.style(Style::default().dim())
.render_ref(bottom_line_rect, buf);
.render_ref(hint_rect, buf);
}
}
let border_style = if self.has_focus {
@@ -1344,6 +1360,7 @@ mod tests {
use super::*;
use image::ImageBuffer;
use image::Rgba;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use tempfile::tempdir;
@@ -1356,6 +1373,60 @@ mod tests {
use crate::bottom_pane::textarea::TextArea;
use tokio::sync::mpsc::unbounded_channel;
#[test]
fn footer_hint_row_is_separated_from_composer() {
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
let area = Rect::new(0, 0, 40, 6);
let mut buf = Buffer::empty(area);
composer.render_ref(area, &mut buf);
let row_to_string = |y: u16| {
let mut row = String::new();
for x in 0..area.width {
row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));
}
row
};
let mut hint_row: Option<(u16, String)> = None;
for y in 0..area.height {
let row = row_to_string(y);
if row.contains(" send") {
hint_row = Some((y, row));
break;
}
}
let (hint_row_idx, hint_row_contents) =
hint_row.expect("expected footer hint row to be rendered");
assert_eq!(
hint_row_idx,
area.height - 1,
"hint row should occupy the bottom line: {hint_row_contents:?}",
);
assert!(
hint_row_idx > 0,
"expected a spacing row above the footer hints",
);
let spacing_row = row_to_string(hint_row_idx - 1);
assert_eq!(
spacing_row.trim(),
"",
"expected blank spacing row above hints but saw: {spacing_row:?}",
);
}
#[test]
fn test_current_at_token_basic_cases() {
let test_cases = vec![

View File

@@ -10,5 +10,5 @@ expression: terminal.backend()
"▌ "
"▌ "
"▌ "
" "
" "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -10,5 +10,5 @@ expression: terminal.backend()
"▌ "
"▌ "
"▌ "
" "
" "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -10,5 +10,5 @@ expression: terminal.backend()
"▌ "
"▌ "
"▌ "
" "
" "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -10,5 +10,5 @@ expression: terminal.backend()
"▌ "
"▌ "
"▌ "
" "
" "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -10,5 +10,5 @@ expression: terminal.backend()
"▌ "
"▌ "
"▌ "
" "
" "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -3,4 +3,4 @@ source: tui/src/chatwidget/tests.rs
expression: terminal.backend()
---
"▌ Ask Codex to do anything "
" ⏎ send ⌃J newline ⌃T transcript ⌃"
" "

View File

@@ -12,4 +12,5 @@ expression: visual
Investigating rendering code (0s • Esc to interrupt)
▌Summarize recent commits
⏎ send ⌃J newline ⌃T transcript ⌃C quit

View File

@@ -6,5 +6,6 @@ expression: terminal.backend()
" Analyzing (0s • Esc to interrupt) "
" "
"▌ Ask Codex to do anything "
" "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
" "