From b0786ae9d52d391f9b561b82ed14aeee181c56ca Mon Sep 17 00:00:00 2001 From: Oz Date: Thu, 16 Jul 2026 21:03:12 +0000 Subject: [PATCH 1/2] Add bottom padding below TUI footer/prompt The headless TUI's terminal-session root container had top/horizontal padding but no bottom padding, so the footer/prompt rendered flush against the bottom terminal edge. Commit 28f25535f8 added `.with_padding_top(2)` without a matching bottom pad. Wrap the view root with `.with_padding_bottom(1)` (extracted into a small `wrap_view_root_padding` helper) so one blank row renders below the footer, matching the Figma spec (~1 cell row of whitespace below the footer). Add a render-to-lines unit test asserting the trailing blank padding row. Fixes CODE-1878 CHANGELOG-BUG-FIX: Fixed the headless TUI footer/prompt rendering flush against the bottom terminal edge with no padding. Co-Authored-By: Oz --- crates/warp_tui/src/terminal_session_view.rs | 16 ++++-- .../src/terminal_session_view_tests.rs | 57 ++++++++++++++++++- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/crates/warp_tui/src/terminal_session_view.rs b/crates/warp_tui/src/terminal_session_view.rs index 1162a37650b..88cb70f45cd 100644 --- a/crates/warp_tui/src/terminal_session_view.rs +++ b/crates/warp_tui/src/terminal_session_view.rs @@ -2314,13 +2314,21 @@ impl TuiView for TuiTerminalSessionView { // the PTY's columns match the width block content actually renders at // (the GUI wraps its view root, but its padding is sub-cell; here it is // 4 whole columns). - TuiContainer::new(terminal_content.finish()) - .with_padding_x(2) - .with_padding_top(2) - .finish() + wrap_view_root_padding(terminal_content.finish()).finish() } } +/// Wraps the terminal-content root in the view's outer padding: horizontal +/// padding on both sides, two blank rows above, and one blank row below. The +/// bottom row matches the Figma spec (≈1 cell row of whitespace) so the +/// footer/prompt never renders flush against the bottom terminal edge. +fn wrap_view_root_padding(child: Box) -> TuiContainer { + TuiContainer::new(child) + .with_padding_x(2) + .with_padding_top(2) + .with_padding_bottom(1) +} + impl TypedActionView for TuiTerminalSessionView { type Action = TuiTerminalSessionAction; diff --git a/crates/warp_tui/src/terminal_session_view_tests.rs b/crates/warp_tui/src/terminal_session_view_tests.rs index 6a9c4476da7..afda69e68a5 100644 --- a/crates/warp_tui/src/terminal_session_view_tests.rs +++ b/crates/warp_tui/src/terminal_session_view_tests.rs @@ -5,13 +5,13 @@ use warp::tui_export::{ use warpui::EntityIdMap; use warpui_core::elements::tui::{ TuiBuffer, TuiBufferExt, TuiConstraint, TuiElement, TuiLayoutContext, TuiPaintContext, - TuiPaintSurface, TuiRect, TuiScreenPosition, TuiSize, + TuiPaintSurface, TuiRect, TuiScreenPosition, TuiSize, TuiText, }; use warpui_core::{App, AppContext}; use super::{ export_file_success_message, raw_prompt_if_not_blank, render_left_footer_hint, - TuiTerminalSessionEvent, + wrap_view_root_padding, TuiTerminalSessionEvent, }; use crate::tui_builder::TuiUiBuilder; @@ -158,3 +158,56 @@ fn resize_event_maps_to_pty_resize_intent() { assert_eq!(actual_update.new_size().rows(), 8); assert_eq!(actual_update.new_size().columns(), 42); } + +fn render_lines_sized( + mut element: Box, + ctx: &AppContext, + width: u16, + height: u16, +) -> Vec { + let mut rendered_views = EntityIdMap::default(); + let mut layout_ctx = TuiLayoutContext { + rendered_views: &mut rendered_views, + }; + let size = element.layout( + TuiConstraint::loose(TuiSize::new(width, height)), + &mut layout_ctx, + ctx, + ); + let area = TuiRect::new(0, 0, size.width, size.height); + let mut buffer = TuiBuffer::empty(area); + let mut paint_ctx = TuiPaintContext::new(&mut rendered_views); + { + let mut surface = TuiPaintSurface::new(&mut buffer); + element.render( + TuiScreenPosition::new(i32::from(area.x), i32::from(area.y)), + &mut surface, + &mut paint_ctx, + ); + } + buffer.to_lines() +} + +#[test] +fn view_root_pads_a_blank_row_below_the_footer() { + // The footer is the content column's last child, so a stand-in text line + // stands for the footer/prompt row here. The view-root padding must leave a + // trailing blank row beneath it (matching the Figma spec) rather than + // rendering flush against the bottom terminal edge. + App::test((), |mut app| async move { + app.update(|ctx| { + ctx.add_singleton_model(|_| Appearance::mock()); + let lines = render_lines_sized( + wrap_view_root_padding(TuiText::new("footer").finish()).finish(), + ctx, + 16, + 8, + ); + + assert_eq!( + lines, + vec![" ", " ", " footer ", " ",], + ); + }); + }); +} From d8721b6fc7f6efcc293a62530e5710cbb4bbd8a7 Mon Sep 17 00:00:00 2001 From: Oz Date: Fri, 17 Jul 2026 00:16:13 +0000 Subject: [PATCH 2/2] Address review comments: inline padding into render, remove test Per reviewer feedback on PR #13850: - Inline the padding chain (.with_padding_x(2), .with_padding_top(2), .with_padding_bottom(1)) directly into TuiTerminalSessionView::render instead of extracting a wrap_view_root_padding helper function. - Remove the view_root_pads_a_blank_row_below_the_footer test case and its render_lines_sized helper (reviewer flagged them as unnecessary). The net change is now a single-line addition of .with_padding_bottom(1) to the existing container chain in render. Co-Authored-By: Oz --- crates/warp_tui/src/terminal_session_view.rs | 17 ++---- .../src/terminal_session_view_tests.rs | 57 +------------------ 2 files changed, 7 insertions(+), 67 deletions(-) diff --git a/crates/warp_tui/src/terminal_session_view.rs b/crates/warp_tui/src/terminal_session_view.rs index 88cb70f45cd..beed6ad334f 100644 --- a/crates/warp_tui/src/terminal_session_view.rs +++ b/crates/warp_tui/src/terminal_session_view.rs @@ -2314,21 +2314,14 @@ impl TuiView for TuiTerminalSessionView { // the PTY's columns match the width block content actually renders at // (the GUI wraps its view root, but its padding is sub-cell; here it is // 4 whole columns). - wrap_view_root_padding(terminal_content.finish()).finish() + TuiContainer::new(terminal_content.finish()) + .with_padding_x(2) + .with_padding_top(2) + .with_padding_bottom(1) + .finish() } } -/// Wraps the terminal-content root in the view's outer padding: horizontal -/// padding on both sides, two blank rows above, and one blank row below. The -/// bottom row matches the Figma spec (≈1 cell row of whitespace) so the -/// footer/prompt never renders flush against the bottom terminal edge. -fn wrap_view_root_padding(child: Box) -> TuiContainer { - TuiContainer::new(child) - .with_padding_x(2) - .with_padding_top(2) - .with_padding_bottom(1) -} - impl TypedActionView for TuiTerminalSessionView { type Action = TuiTerminalSessionAction; diff --git a/crates/warp_tui/src/terminal_session_view_tests.rs b/crates/warp_tui/src/terminal_session_view_tests.rs index afda69e68a5..6a9c4476da7 100644 --- a/crates/warp_tui/src/terminal_session_view_tests.rs +++ b/crates/warp_tui/src/terminal_session_view_tests.rs @@ -5,13 +5,13 @@ use warp::tui_export::{ use warpui::EntityIdMap; use warpui_core::elements::tui::{ TuiBuffer, TuiBufferExt, TuiConstraint, TuiElement, TuiLayoutContext, TuiPaintContext, - TuiPaintSurface, TuiRect, TuiScreenPosition, TuiSize, TuiText, + TuiPaintSurface, TuiRect, TuiScreenPosition, TuiSize, }; use warpui_core::{App, AppContext}; use super::{ export_file_success_message, raw_prompt_if_not_blank, render_left_footer_hint, - wrap_view_root_padding, TuiTerminalSessionEvent, + TuiTerminalSessionEvent, }; use crate::tui_builder::TuiUiBuilder; @@ -158,56 +158,3 @@ fn resize_event_maps_to_pty_resize_intent() { assert_eq!(actual_update.new_size().rows(), 8); assert_eq!(actual_update.new_size().columns(), 42); } - -fn render_lines_sized( - mut element: Box, - ctx: &AppContext, - width: u16, - height: u16, -) -> Vec { - let mut rendered_views = EntityIdMap::default(); - let mut layout_ctx = TuiLayoutContext { - rendered_views: &mut rendered_views, - }; - let size = element.layout( - TuiConstraint::loose(TuiSize::new(width, height)), - &mut layout_ctx, - ctx, - ); - let area = TuiRect::new(0, 0, size.width, size.height); - let mut buffer = TuiBuffer::empty(area); - let mut paint_ctx = TuiPaintContext::new(&mut rendered_views); - { - let mut surface = TuiPaintSurface::new(&mut buffer); - element.render( - TuiScreenPosition::new(i32::from(area.x), i32::from(area.y)), - &mut surface, - &mut paint_ctx, - ); - } - buffer.to_lines() -} - -#[test] -fn view_root_pads_a_blank_row_below_the_footer() { - // The footer is the content column's last child, so a stand-in text line - // stands for the footer/prompt row here. The view-root padding must leave a - // trailing blank row beneath it (matching the Figma spec) rather than - // rendering flush against the bottom terminal edge. - App::test((), |mut app| async move { - app.update(|ctx| { - ctx.add_singleton_model(|_| Appearance::mock()); - let lines = render_lines_sized( - wrap_view_root_padding(TuiText::new("footer").finish()).finish(), - ctx, - 16, - 8, - ); - - assert_eq!( - lines, - vec![" ", " ", " footer ", " ",], - ); - }); - }); -}