Skip to content

Commit d4848e5

Browse files
authored
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" />
1 parent 1a6a95f commit d4848e5

9 files changed

+92
-19
lines changed

codex-rs/tui/src/bottom_pane/chat_composer.rs

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ enum ActivePopup {
9595
File(FileSearchPopup),
9696
}
9797

98+
const FOOTER_HINT_HEIGHT: u16 = 1;
99+
const FOOTER_SPACING_HEIGHT: u16 = 1;
100+
const FOOTER_HEIGHT_WITH_HINT: u16 = FOOTER_HINT_HEIGHT + FOOTER_SPACING_HEIGHT;
101+
98102
impl ChatComposer {
99103
pub fn new(
100104
has_input_focus: bool,
@@ -134,20 +138,20 @@ impl ChatComposer {
134138
pub fn desired_height(&self, width: u16) -> u16 {
135139
self.textarea.desired_height(width - 1)
136140
+ match &self.active_popup {
137-
ActivePopup::None => 1u16,
141+
ActivePopup::None => FOOTER_HEIGHT_WITH_HINT,
138142
ActivePopup::Command(c) => c.calculate_required_height(),
139143
ActivePopup::File(c) => c.calculate_required_height(),
140144
}
141145
}
142146

143147
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
144-
let popup_height = match &self.active_popup {
145-
ActivePopup::Command(popup) => popup.calculate_required_height(),
146-
ActivePopup::File(popup) => popup.calculate_required_height(),
147-
ActivePopup::None => 1,
148+
let popup_constraint = match &self.active_popup {
149+
ActivePopup::Command(popup) => Constraint::Max(popup.calculate_required_height()),
150+
ActivePopup::File(popup) => Constraint::Max(popup.calculate_required_height()),
151+
ActivePopup::None => Constraint::Max(FOOTER_HEIGHT_WITH_HINT),
148152
};
149153
let [textarea_rect, _] =
150-
Layout::vertical([Constraint::Min(1), Constraint::Max(popup_height)]).areas(area);
154+
Layout::vertical([Constraint::Min(1), popup_constraint]).areas(area);
151155
let mut textarea_rect = textarea_rect;
152156
textarea_rect.width = textarea_rect.width.saturating_sub(1);
153157
textarea_rect.x += 1;
@@ -1223,13 +1227,16 @@ impl ChatComposer {
12231227

12241228
impl WidgetRef for ChatComposer {
12251229
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
1226-
let popup_height = match &self.active_popup {
1227-
ActivePopup::Command(popup) => popup.calculate_required_height(),
1228-
ActivePopup::File(popup) => popup.calculate_required_height(),
1229-
ActivePopup::None => 1,
1230+
let (popup_constraint, hint_spacing) = match &self.active_popup {
1231+
ActivePopup::Command(popup) => (Constraint::Max(popup.calculate_required_height()), 0),
1232+
ActivePopup::File(popup) => (Constraint::Max(popup.calculate_required_height()), 0),
1233+
ActivePopup::None => (
1234+
Constraint::Length(FOOTER_HEIGHT_WITH_HINT),
1235+
FOOTER_SPACING_HEIGHT,
1236+
),
12301237
};
12311238
let [textarea_rect, popup_rect] =
1232-
Layout::vertical([Constraint::Min(1), Constraint::Max(popup_height)]).areas(area);
1239+
Layout::vertical([Constraint::Min(1), popup_constraint]).areas(area);
12331240
match &self.active_popup {
12341241
ActivePopup::Command(popup) => {
12351242
popup.render_ref(popup_rect, buf);
@@ -1238,7 +1245,16 @@ impl WidgetRef for ChatComposer {
12381245
popup.render_ref(popup_rect, buf);
12391246
}
12401247
ActivePopup::None => {
1241-
let bottom_line_rect = popup_rect;
1248+
let hint_rect = if hint_spacing > 0 {
1249+
let [_, hint_rect] = Layout::vertical([
1250+
Constraint::Length(hint_spacing),
1251+
Constraint::Length(FOOTER_HINT_HEIGHT),
1252+
])
1253+
.areas(popup_rect);
1254+
hint_rect
1255+
} else {
1256+
popup_rect
1257+
};
12421258
let mut hint: Vec<Span<'static>> = if self.ctrl_c_quit_hint {
12431259
let ctrl_c_followup = if self.is_task_running {
12441260
" to interrupt"
@@ -1309,7 +1325,7 @@ impl WidgetRef for ChatComposer {
13091325

13101326
Line::from(hint)
13111327
.style(Style::default().dim())
1312-
.render_ref(bottom_line_rect, buf);
1328+
.render_ref(hint_rect, buf);
13131329
}
13141330
}
13151331
let border_style = if self.has_focus {
@@ -1344,6 +1360,7 @@ mod tests {
13441360
use super::*;
13451361
use image::ImageBuffer;
13461362
use image::Rgba;
1363+
use pretty_assertions::assert_eq;
13471364
use std::path::PathBuf;
13481365
use tempfile::tempdir;
13491366

@@ -1356,6 +1373,60 @@ mod tests {
13561373
use crate::bottom_pane::textarea::TextArea;
13571374
use tokio::sync::mpsc::unbounded_channel;
13581375

1376+
#[test]
1377+
fn footer_hint_row_is_separated_from_composer() {
1378+
let (tx, _rx) = unbounded_channel::<AppEvent>();
1379+
let sender = AppEventSender::new(tx);
1380+
let composer = ChatComposer::new(
1381+
true,
1382+
sender,
1383+
false,
1384+
"Ask Codex to do anything".to_string(),
1385+
false,
1386+
);
1387+
1388+
let area = Rect::new(0, 0, 40, 6);
1389+
let mut buf = Buffer::empty(area);
1390+
composer.render_ref(area, &mut buf);
1391+
1392+
let row_to_string = |y: u16| {
1393+
let mut row = String::new();
1394+
for x in 0..area.width {
1395+
row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));
1396+
}
1397+
row
1398+
};
1399+
1400+
let mut hint_row: Option<(u16, String)> = None;
1401+
for y in 0..area.height {
1402+
let row = row_to_string(y);
1403+
if row.contains(" send") {
1404+
hint_row = Some((y, row));
1405+
break;
1406+
}
1407+
}
1408+
1409+
let (hint_row_idx, hint_row_contents) =
1410+
hint_row.expect("expected footer hint row to be rendered");
1411+
assert_eq!(
1412+
hint_row_idx,
1413+
area.height - 1,
1414+
"hint row should occupy the bottom line: {hint_row_contents:?}",
1415+
);
1416+
1417+
assert!(
1418+
hint_row_idx > 0,
1419+
"expected a spacing row above the footer hints",
1420+
);
1421+
1422+
let spacing_row = row_to_string(hint_row_idx - 1);
1423+
assert_eq!(
1424+
spacing_row.trim(),
1425+
"",
1426+
"expected blank spacing row above hints but saw: {spacing_row:?}",
1427+
);
1428+
}
1429+
13591430
#[test]
13601431
fn test_current_at_token_basic_cases() {
13611432
let test_cases = vec![

codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ expression: terminal.backend()
1010
""
1111
""
1212
""
13-
" "
13+
" "
1414
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "

codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ expression: terminal.backend()
1010
""
1111
""
1212
""
13-
" "
13+
" "
1414
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "

codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ expression: terminal.backend()
1010
""
1111
""
1212
""
13-
" "
13+
" "
1414
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "

codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ expression: terminal.backend()
1010
""
1111
""
1212
""
13-
" "
13+
" "
1414
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "

codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ expression: terminal.backend()
1010
""
1111
""
1212
""
13-
" "
13+
" "
1414
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "

codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_idle_h2.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ source: tui/src/chatwidget/tests.rs
33
expression: terminal.backend()
44
---
55
"▌ Ask Codex to do anything "
6-
" ⏎ send ⌃J newline ⌃T transcript "
6+
" "

codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ expression: visual
1212
Investigating rendering code (0sEsc to interrupt)
1313

1414
Summarize recent commits
15+
1516
sendJ newlineT transcriptC quit

codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ expression: terminal.backend()
66
" Analyzing (0s • Esc to interrupt) "
77
" "
88
"▌ Ask Codex to do anything "
9+
" "
910
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
1011
" "

0 commit comments

Comments
 (0)