diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 065232d8..2138d540 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -38,6 +38,7 @@ use crate::bottom_pane::textarea::TextAreaState; use crate::clipboard_paste::normalize_pasted_path; use crate::clipboard_paste::pasted_image_format; use crate::key_hint; +use crate::ui_consts::LIVE_PREFIX_COLS; use codex_file_search::FileMatch; use std::cell::RefCell; use std::collections::HashMap; @@ -136,7 +137,9 @@ impl ChatComposer { } 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 { ActivePopup::None => FOOTER_HEIGHT_WITH_HINT, ActivePopup::Command(c) => c.calculate_required_height(), @@ -153,8 +156,9 @@ impl ChatComposer { let [textarea_rect, _] = 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; + // Leave 1 for border and 1 for padding + 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(); self.textarea.cursor_pos_with_state(textarea_rect, &state) } @@ -1274,7 +1278,6 @@ impl WidgetRef for ChatComposer { key_hint::ctrl('J') }; vec![ - " ".into(), key_hint::plain('⏎'), " send ".into(), newline_hint_key, @@ -1342,15 +1345,16 @@ impl WidgetRef for ChatComposer { buf, ); let mut textarea_rect = textarea_rect; - textarea_rect.width = textarea_rect.width.saturating_sub(1); - textarea_rect.x += 1; + // Leave 1 for border and 1 for padding + 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(); StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state); if self.textarea.text().is_empty() { Line::from(self.placeholder_text.as_str()) .style(Style::default().dim()) - .render_ref(textarea_rect.inner(Margin::new(1, 0)), buf); + .render_ref(textarea_rect.inner(Margin::new(0, 0)), buf); } } } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap index eafcf945..c9f5cb97 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__backspace_after_pastes.snap @@ -2,7 +2,7 @@ source: tui/src/bottom_pane/chat_composer.rs 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 " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap index 8de047f1..823a41cf 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__empty.snap @@ -11,4 +11,4 @@ expression: terminal.backend() "▌ " "▌ " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +"⏎ send ⌃J newline ⌃T transcript ⌃C quit " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap index b9e96aed..d89c4033 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__large.snap @@ -2,7 +2,7 @@ source: tui/src/bottom_pane/chat_composer.rs 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 " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap index cb546595..636d5c22 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__multiple_pastes.snap @@ -2,7 +2,7 @@ source: tui/src/bottom_pane/chat_composer.rs 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 " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap index 63f9b3a9..b626f80c 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__slash_popup_mo.snap @@ -2,7 +2,7 @@ source: tui/src/bottom_pane/chat_composer.rs expression: terminal.backend() --- -"▌/mo " +"▌ /mo " "▌ " "▌/model choose what model and reasoning effort to use " "▌/mention mention a file " diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap index 8843b4a5..111e45b6 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__small.snap @@ -2,7 +2,7 @@ source: tui/src/bottom_pane/chat_composer.rs 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 " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h2.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h2.snap index f5097286..917f0126 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h2.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chat_small_running_h2.snap @@ -2,5 +2,5 @@ source: tui/src/chatwidget/tests.rs expression: terminal.backend() --- -" Thinking (0s • Esc to interrupt) " +" Thinking (0s • Esc to interrupt) " "▌ Ask Codex to do anything " diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap index df4fd4ca..9a285904 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__chatwidget_exec_and_status_layout_vt100_snapshot.snap @@ -9,8 +9,8 @@ expression: visual └ Search Change Approved 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 diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap index 8eb6e72a..e28659e2 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__status_widget_active.snap @@ -3,9 +3,9 @@ source: tui/src/chatwidget/tests.rs expression: terminal.backend() --- " " -" Analyzing (0s • Esc to interrupt) " +" Analyzing (0s • Esc to interrupt) " " " "▌ Ask Codex to do anything " " " -" ⏎ send ⌃J newline ⌃T transcript ⌃C quit " +"⏎ send ⌃J newline ⌃T transcript ⌃C quit " " " diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index b2af5817..c4c019db 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -7,6 +7,7 @@ use crate::render::line_utils::prefix_lines; use crate::render::line_utils::push_owned_lines; use crate::slash_command::SlashCommand; use crate::text_formatting::format_and_truncate_tool_result; +use crate::ui_consts::LIVE_PREFIX_COLS; use crate::wrapping::RtOptions; use crate::wrapping::word_wrap_line; use crate::wrapping::word_wrap_lines; @@ -106,7 +107,7 @@ impl HistoryCell for UserHistoryCell { let mut lines: Vec> = Vec::new(); // 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( &self.message, textwrap::Options::new(wrap_width as usize) @@ -114,7 +115,7 @@ impl HistoryCell for UserHistoryCell { ); 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 } @@ -644,25 +645,25 @@ pub(crate) fn new_session_info( ])); lines.push(Line::from("".dim())); 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())); if !has_agents_md { lines.push(Line::from(vec![ - " /init".bold(), + " /init".bold(), format!(" - {}", SlashCommand::Init.description()).dim(), ])); } lines.push(Line::from(vec![ - " /status".bold(), + " /status".bold(), format!(" - {}", SlashCommand::Status.description()).dim(), ])); lines.push(Line::from(vec![ - " /approvals".bold(), + " /approvals".bold(), format!(" - {}", SlashCommand::Approvals.description()).dim(), ])); lines.push(Line::from(vec![ - " /model".bold(), + " /model".bold(), format!(" - {}", SlashCommand::Model.description()).dim(), ])); PlainHistoryCell { lines } @@ -1785,7 +1786,7 @@ mod tests { 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 lines = cell.display_lines(width); let rendered = render_lines(&lines).join("\n"); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 455a9a21..e050e5b5 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -60,6 +60,7 @@ mod status_indicator_widget; mod streaming; mod text_formatting; mod tui; +mod ui_consts; mod user_approval_widget; mod version; mod wrapping; diff --git a/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__user_history_cell_wraps_and_prefixes_each_line_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__user_history_cell_wraps_and_prefixes_each_line_snapshot.snap index 8b8cc38a..ef0ceabc 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__user_history_cell_wraps_and_prefixes_each_line_snapshot.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__history_cell__tests__user_history_cell_wraps_and_prefixes_each_line_snapshot.snap @@ -2,7 +2,7 @@ source: tui/src/history_cell.rs expression: rendered --- -▌one two -▌three four -▌five six -▌seven +▌ one two +▌ three four +▌ five six +▌ seven diff --git a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap index 19dc5d31..6aa34017 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_truncated.snap @@ -2,5 +2,5 @@ source: tui/src/status_indicator_widget.rs expression: terminal.backend() --- -" Working (0s • Esc t" +" Working (0s • Esc " " " diff --git a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap index 7f2c267f..061f3a13 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_queued_messages.snap @@ -2,7 +2,7 @@ source: tui/src/status_indicator_widget.rs expression: terminal.backend() --- -" Working (0s • Esc to interrupt) " +" Working (0s • Esc to interrupt) " " " " ↳ first " " ↳ second " diff --git a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap index fe9eebed..debf2821 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__status_indicator_widget__tests__renders_with_working_header.snap @@ -2,5 +2,5 @@ source: tui/src/status_indicator_widget.rs expression: terminal.backend() --- -" Working (0s • Esc to interrupt) " +" Working (0s • Esc to interrupt) " " " diff --git a/codex-rs/tui/src/status_indicator_widget.rs b/codex-rs/tui/src/status_indicator_widget.rs index e3a1b854..96f2c49f 100644 --- a/codex-rs/tui/src/status_indicator_widget.rs +++ b/codex-rs/tui/src/status_indicator_widget.rs @@ -17,6 +17,7 @@ use crate::app_event_sender::AppEventSender; use crate::key_hint; use crate::shimmer::shimmer_spans; use crate::tui::FrameRequester; +use crate::ui_consts::LIVE_PREFIX_COLS; pub(crate) struct StatusIndicatorWidget { /// Animated header text (defaults to "Working"). @@ -159,7 +160,7 @@ impl WidgetRef for StatusIndicatorWidget { let pretty_elapsed = fmt_elapsed_compact(elapsed); // 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(vec![ " ".into(), diff --git a/codex-rs/tui/src/ui_consts.rs b/codex-rs/tui/src/ui_consts.rs new file mode 100644 index 00000000..b249cd5f --- /dev/null +++ b/codex-rs/tui/src/ui_consts.rs @@ -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;