Align user history message prefix width (#3467)

<img width="798" height="340" alt="image"
src="https://github.com/user-attachments/assets/fdd63f40-9c94-4e3a-bce5-2d2f333a384f"
/>
This commit is contained in:
Ahmed Ibrahim
2025-09-14 16:51:08 -04:00
committed by GitHub
parent d701eb32d7
commit 7e1543f5d8
18 changed files with 56 additions and 39 deletions

View File

@@ -38,6 +38,7 @@ use crate::bottom_pane::textarea::TextAreaState;
use crate::clipboard_paste::normalize_pasted_path; use crate::clipboard_paste::normalize_pasted_path;
use crate::clipboard_paste::pasted_image_format; use crate::clipboard_paste::pasted_image_format;
use crate::key_hint; use crate::key_hint;
use crate::ui_consts::LIVE_PREFIX_COLS;
use codex_file_search::FileMatch; use codex_file_search::FileMatch;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
@@ -136,7 +137,9 @@ impl ChatComposer {
} }
pub fn desired_height(&self, width: u16) -> u16 { pub fn desired_height(&self, width: u16) -> u16 {
self.textarea.desired_height(width - 1) // Leave 1 column for the left border and 1 column for left padding
self.textarea
.desired_height(width.saturating_sub(LIVE_PREFIX_COLS))
+ match &self.active_popup { + match &self.active_popup {
ActivePopup::None => FOOTER_HEIGHT_WITH_HINT, ActivePopup::None => FOOTER_HEIGHT_WITH_HINT,
ActivePopup::Command(c) => c.calculate_required_height(), ActivePopup::Command(c) => c.calculate_required_height(),
@@ -153,8 +156,9 @@ impl ChatComposer {
let [textarea_rect, _] = let [textarea_rect, _] =
Layout::vertical([Constraint::Min(1), popup_constraint]).areas(area); Layout::vertical([Constraint::Min(1), popup_constraint]).areas(area);
let mut textarea_rect = textarea_rect; let mut textarea_rect = textarea_rect;
textarea_rect.width = textarea_rect.width.saturating_sub(1); // Leave 1 for border and 1 for padding
textarea_rect.x += 1; textarea_rect.width = textarea_rect.width.saturating_sub(LIVE_PREFIX_COLS);
textarea_rect.x = textarea_rect.x.saturating_add(LIVE_PREFIX_COLS);
let state = self.textarea_state.borrow(); let state = self.textarea_state.borrow();
self.textarea.cursor_pos_with_state(textarea_rect, &state) self.textarea.cursor_pos_with_state(textarea_rect, &state)
} }
@@ -1274,7 +1278,6 @@ impl WidgetRef for ChatComposer {
key_hint::ctrl('J') key_hint::ctrl('J')
}; };
vec![ vec![
" ".into(),
key_hint::plain('⏎'), key_hint::plain('⏎'),
" send ".into(), " send ".into(),
newline_hint_key, newline_hint_key,
@@ -1342,15 +1345,16 @@ impl WidgetRef for ChatComposer {
buf, buf,
); );
let mut textarea_rect = textarea_rect; let mut textarea_rect = textarea_rect;
textarea_rect.width = textarea_rect.width.saturating_sub(1); // Leave 1 for border and 1 for padding
textarea_rect.x += 1; textarea_rect.width = textarea_rect.width.saturating_sub(LIVE_PREFIX_COLS);
textarea_rect.x = textarea_rect.x.saturating_add(LIVE_PREFIX_COLS);
let mut state = self.textarea_state.borrow_mut(); let mut state = self.textarea_state.borrow_mut();
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state); StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
if self.textarea.text().is_empty() { if self.textarea.text().is_empty() {
Line::from(self.placeholder_text.as_str()) Line::from(self.placeholder_text.as_str())
.style(Style::default().dim()) .style(Style::default().dim())
.render_ref(textarea_rect.inner(Margin::new(1, 0)), buf); .render_ref(textarea_rect.inner(Margin::new(0, 0)), buf);
} }
} }
} }

View File

@@ -2,7 +2,7 @@
source: tui/src/bottom_pane/chat_composer.rs source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend() expression: terminal.backend()
--- ---
"▌[Pasted Content 1002 chars][Pasted Content 1004 chars] " "▌ [Pasted Content 1002 chars][Pasted Content 1004 chars] "
"▌ " "▌ "
"▌ " "▌ "
"▌ " "▌ "
@@ -11,4 +11,4 @@ expression: terminal.backend()
"▌ " "▌ "
"▌ " "▌ "
" " " "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " "⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

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

View File

@@ -2,7 +2,7 @@
source: tui/src/bottom_pane/chat_composer.rs source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend() expression: terminal.backend()
--- ---
"▌[Pasted Content 1005 chars] " "▌ [Pasted Content 1005 chars] "
"▌ " "▌ "
"▌ " "▌ "
"▌ " "▌ "
@@ -11,4 +11,4 @@ expression: terminal.backend()
"▌ " "▌ "
"▌ " "▌ "
" " " "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " "⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -2,7 +2,7 @@
source: tui/src/bottom_pane/chat_composer.rs source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend() expression: terminal.backend()
--- ---
"▌[Pasted Content 1003 chars][Pasted Content 1007 chars] another short paste " "▌ [Pasted Content 1003 chars][Pasted Content 1007 chars] another short paste "
"▌ " "▌ "
"▌ " "▌ "
"▌ " "▌ "
@@ -11,4 +11,4 @@ expression: terminal.backend()
"▌ " "▌ "
"▌ " "▌ "
" " " "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " "⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -2,7 +2,7 @@
source: tui/src/bottom_pane/chat_composer.rs source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend() expression: terminal.backend()
--- ---
"▌/mo " "▌ /mo "
"▌ " "▌ "
"▌/model choose what model and reasoning effort to use " "▌/model choose what model and reasoning effort to use "
"▌/mention mention a file " "▌/mention mention a file "

View File

@@ -2,7 +2,7 @@
source: tui/src/bottom_pane/chat_composer.rs source: tui/src/bottom_pane/chat_composer.rs
expression: terminal.backend() expression: terminal.backend()
--- ---
"▌short " "▌ short "
"▌ " "▌ "
"▌ " "▌ "
"▌ " "▌ "
@@ -11,4 +11,4 @@ expression: terminal.backend()
"▌ " "▌ "
"▌ " "▌ "
" " " "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " "⏎ send ⌃J newline ⌃T transcript ⌃C quit "

View File

@@ -2,5 +2,5 @@
source: tui/src/chatwidget/tests.rs source: tui/src/chatwidget/tests.rs
expression: terminal.backend() expression: terminal.backend()
--- ---
" Thinking (0s • Esc to interrupt) " " Thinking (0s • Esc to interrupt) "
"▌ Ask Codex to do anything " "▌ Ask Codex to do anything "

View File

@@ -9,8 +9,8 @@ expression: visual
└ Search Change Approved └ Search Change Approved
Read diff_render.rs Read diff_render.rs
Investigating rendering code (0s • Esc to interrupt) Investigating rendering code (0s • Esc to interrupt)
▌Summarize recent commits Summarize recent commits
⏎ send ⌃J newline ⌃T transcript ⌃C quit ⏎ send ⌃J newline ⌃T transcript ⌃C quit

View File

@@ -3,9 +3,9 @@ source: tui/src/chatwidget/tests.rs
expression: terminal.backend() expression: terminal.backend()
--- ---
" " " "
" Analyzing (0s • Esc to interrupt) " " Analyzing (0s • Esc to interrupt) "
" " " "
"▌ Ask Codex to do anything " "▌ Ask Codex to do anything "
" " " "
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " "⏎ send ⌃J newline ⌃T transcript ⌃C quit "
" " " "

View File

@@ -7,6 +7,7 @@ use crate::render::line_utils::prefix_lines;
use crate::render::line_utils::push_owned_lines; use crate::render::line_utils::push_owned_lines;
use crate::slash_command::SlashCommand; use crate::slash_command::SlashCommand;
use crate::text_formatting::format_and_truncate_tool_result; use crate::text_formatting::format_and_truncate_tool_result;
use crate::ui_consts::LIVE_PREFIX_COLS;
use crate::wrapping::RtOptions; use crate::wrapping::RtOptions;
use crate::wrapping::word_wrap_line; use crate::wrapping::word_wrap_line;
use crate::wrapping::word_wrap_lines; use crate::wrapping::word_wrap_lines;
@@ -106,7 +107,7 @@ impl HistoryCell for UserHistoryCell {
let mut lines: Vec<Line<'static>> = Vec::new(); let mut lines: Vec<Line<'static>> = Vec::new();
// Wrap the content first, then prefix each wrapped line with the marker. // Wrap the content first, then prefix each wrapped line with the marker.
let wrap_width = width.saturating_sub(1); // account for the ▌ prefix let wrap_width = width.saturating_sub(LIVE_PREFIX_COLS); // account for the ▌ prefix and trailing space
let wrapped = textwrap::wrap( let wrapped = textwrap::wrap(
&self.message, &self.message,
textwrap::Options::new(wrap_width as usize) textwrap::Options::new(wrap_width as usize)
@@ -114,7 +115,7 @@ impl HistoryCell for UserHistoryCell {
); );
for line in wrapped { for line in wrapped {
lines.push(vec!["".cyan().dim(), line.to_string().dim()].into()); lines.push(vec![" ".cyan().dim(), line.to_string().dim()].into());
} }
lines lines
} }
@@ -644,25 +645,25 @@ pub(crate) fn new_session_info(
])); ]));
lines.push(Line::from("".dim())); lines.push(Line::from("".dim()));
lines.push(Line::from( lines.push(Line::from(
" To get started, describe a task or try one of these commands:".dim(), " To get started, describe a task or try one of these commands:".dim(),
)); ));
lines.push(Line::from("".dim())); lines.push(Line::from("".dim()));
if !has_agents_md { if !has_agents_md {
lines.push(Line::from(vec![ lines.push(Line::from(vec![
" /init".bold(), " /init".bold(),
format!(" - {}", SlashCommand::Init.description()).dim(), format!(" - {}", SlashCommand::Init.description()).dim(),
])); ]));
} }
lines.push(Line::from(vec![ lines.push(Line::from(vec![
" /status".bold(), " /status".bold(),
format!(" - {}", SlashCommand::Status.description()).dim(), format!(" - {}", SlashCommand::Status.description()).dim(),
])); ]));
lines.push(Line::from(vec![ lines.push(Line::from(vec![
" /approvals".bold(), " /approvals".bold(),
format!(" - {}", SlashCommand::Approvals.description()).dim(), format!(" - {}", SlashCommand::Approvals.description()).dim(),
])); ]));
lines.push(Line::from(vec![ lines.push(Line::from(vec![
" /model".bold(), " /model".bold(),
format!(" - {}", SlashCommand::Model.description()).dim(), format!(" - {}", SlashCommand::Model.description()).dim(),
])); ]));
PlainHistoryCell { lines } PlainHistoryCell { lines }
@@ -1785,7 +1786,7 @@ mod tests {
message: msg.to_string(), message: msg.to_string(),
}; };
// Small width to force wrapping more clearly. Effective wrap width is width-1 due to the ▌ prefix. // Small width to force wrapping more clearly. Effective wrap width is width-2 due to the ▌ prefix and trailing space.
let width: u16 = 12; let width: u16 = 12;
let lines = cell.display_lines(width); let lines = cell.display_lines(width);
let rendered = render_lines(&lines).join("\n"); let rendered = render_lines(&lines).join("\n");

View File

@@ -60,6 +60,7 @@ mod status_indicator_widget;
mod streaming; mod streaming;
mod text_formatting; mod text_formatting;
mod tui; mod tui;
mod ui_consts;
mod user_approval_widget; mod user_approval_widget;
mod version; mod version;
mod wrapping; mod wrapping;

View File

@@ -2,7 +2,7 @@
source: tui/src/history_cell.rs source: tui/src/history_cell.rs
expression: rendered expression: rendered
--- ---
▌one two one two
▌three four three four
▌five six five six
▌seven seven

View File

@@ -2,5 +2,5 @@
source: tui/src/status_indicator_widget.rs source: tui/src/status_indicator_widget.rs
expression: terminal.backend() expression: terminal.backend()
--- ---
" Working (0s • Esc t" " Working (0s • Esc "
" " " "

View File

@@ -2,7 +2,7 @@
source: tui/src/status_indicator_widget.rs source: tui/src/status_indicator_widget.rs
expression: terminal.backend() expression: terminal.backend()
--- ---
" Working (0s • Esc to interrupt) " " Working (0s • Esc to interrupt) "
" " " "
" ↳ first " " ↳ first "
" ↳ second " " ↳ second "

View File

@@ -2,5 +2,5 @@
source: tui/src/status_indicator_widget.rs source: tui/src/status_indicator_widget.rs
expression: terminal.backend() expression: terminal.backend()
--- ---
" Working (0s • Esc to interrupt) " " Working (0s • Esc to interrupt) "
" " " "

View File

@@ -17,6 +17,7 @@ use crate::app_event_sender::AppEventSender;
use crate::key_hint; use crate::key_hint;
use crate::shimmer::shimmer_spans; use crate::shimmer::shimmer_spans;
use crate::tui::FrameRequester; use crate::tui::FrameRequester;
use crate::ui_consts::LIVE_PREFIX_COLS;
pub(crate) struct StatusIndicatorWidget { pub(crate) struct StatusIndicatorWidget {
/// Animated header text (defaults to "Working"). /// Animated header text (defaults to "Working").
@@ -159,7 +160,7 @@ impl WidgetRef for StatusIndicatorWidget {
let pretty_elapsed = fmt_elapsed_compact(elapsed); let pretty_elapsed = fmt_elapsed_compact(elapsed);
// Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback. // Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback.
let mut spans = vec![" ".into()]; let mut spans = vec![" ".repeat(LIVE_PREFIX_COLS as usize).into()];
spans.extend(shimmer_spans(&self.header)); spans.extend(shimmer_spans(&self.header));
spans.extend(vec![ spans.extend(vec![
" ".into(), " ".into(),

View File

@@ -0,0 +1,10 @@
//! Shared UI constants for layout and alignment within the TUI.
/// Width (in terminal columns) reserved for the left gutter/prefix used by
/// live cells and aligned widgets.
///
/// Semantics:
/// - Chat composer reserves this many columns for the left border + padding.
/// - Status indicator lines begin with this many spaces for alignment.
/// - User history lines account for this many columns (e.g., "▌ ") when wrapping.
pub(crate) const LIVE_PREFIX_COLS: u16 = 2;