update composer + user message styling (#4240)
Changes: - the composer and user messages now have a colored background that stretches the entire width of the terminal. - the prompt character was changed from a cyan `▌` to a bold `›`. - the "working" shimmer now follows the "dark gray" color of the terminal, better matching the terminal's color scheme | Terminal + Background | Screenshot | |------------------------------|------------| | iTerm with dark bg | <img width="810" height="641" alt="Screenshot 2025-09-25 at 11 44 52 AM" src="https://github.com/user-attachments/assets/1317e579-64a9-4785-93e6-98b0258f5d92" /> | | iTerm with light bg | <img width="845" height="540" alt="Screenshot 2025-09-25 at 11 46 29 AM" src="https://github.com/user-attachments/assets/e671d490-c747-4460-af0b-3f8d7f7a6b8e" /> | | iTerm with color bg | <img width="825" height="564" alt="Screenshot 2025-09-25 at 11 47 12 AM" src="https://github.com/user-attachments/assets/141cda1b-1164-41d5-87da-3be11e6a3063" /> | | Terminal.app with dark bg | <img width="577" height="367" alt="Screenshot 2025-09-25 at 11 45 22 AM" src="https://github.com/user-attachments/assets/93fc4781-99f7-4ee7-9c8e-3db3cd854fe5" /> | | Terminal.app with light bg | <img width="577" height="367" alt="Screenshot 2025-09-25 at 11 46 04 AM" src="https://github.com/user-attachments/assets/19bf6a3c-91e0-447b-9667-b8033f512219" /> | | Terminal.app with color bg | <img width="577" height="367" alt="Screenshot 2025-09-25 at 11 45 50 AM" src="https://github.com/user-attachments/assets/dd7c4b5b-342e-4028-8140-f4e65752bd0b" /> |
This commit is contained in:
@@ -8,15 +8,10 @@ use ratatui::layout::Constraint;
|
||||
use ratatui::layout::Layout;
|
||||
use ratatui::layout::Margin;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::widgets::BorderType;
|
||||
use ratatui::widgets::Borders;
|
||||
use ratatui::widgets::StatefulWidgetRef;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
|
||||
@@ -30,6 +25,8 @@ use super::paste_burst::CharDecision;
|
||||
use super::paste_burst::PasteBurst;
|
||||
use crate::bottom_pane::paste_burst::FlushResult;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::style::user_message_style;
|
||||
use crate::terminal_palette;
|
||||
use codex_protocol::custom_prompts::CustomPrompt;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
@@ -97,8 +94,6 @@ enum ActivePopup {
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -137,30 +132,40 @@ impl ChatComposer {
|
||||
}
|
||||
|
||||
pub fn desired_height(&self, width: u16) -> u16 {
|
||||
// Leave 1 column for the left border and 1 column for left padding
|
||||
self.textarea
|
||||
.desired_height(width.saturating_sub(LIVE_PREFIX_COLS))
|
||||
+ 2
|
||||
+ match &self.active_popup {
|
||||
ActivePopup::None => FOOTER_HEIGHT_WITH_HINT,
|
||||
ActivePopup::None => FOOTER_HINT_HEIGHT,
|
||||
ActivePopup::Command(c) => c.calculate_required_height(width),
|
||||
ActivePopup::File(c) => c.calculate_required_height(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
fn layout_areas(&self, area: Rect) -> [Rect; 3] {
|
||||
let popup_constraint = match &self.active_popup {
|
||||
ActivePopup::Command(popup) => {
|
||||
Constraint::Max(popup.calculate_required_height(area.width))
|
||||
}
|
||||
ActivePopup::File(popup) => Constraint::Max(popup.calculate_required_height()),
|
||||
ActivePopup::None => Constraint::Max(FOOTER_HEIGHT_WITH_HINT),
|
||||
ActivePopup::None => Constraint::Max(FOOTER_HINT_HEIGHT),
|
||||
};
|
||||
let [textarea_rect, _] =
|
||||
let mut area = area;
|
||||
// Leave an empty row at the top, unless there isn't room.
|
||||
if area.height > 1 {
|
||||
area.height -= 1;
|
||||
area.y += 1;
|
||||
}
|
||||
let [composer_rect, popup_rect] =
|
||||
Layout::vertical([Constraint::Min(1), popup_constraint]).areas(area);
|
||||
let mut textarea_rect = textarea_rect;
|
||||
// Leave 1 for border and 1 for padding
|
||||
let mut textarea_rect = composer_rect;
|
||||
textarea_rect.width = textarea_rect.width.saturating_sub(LIVE_PREFIX_COLS);
|
||||
textarea_rect.x = textarea_rect.x.saturating_add(LIVE_PREFIX_COLS);
|
||||
[composer_rect, textarea_rect, popup_rect]
|
||||
}
|
||||
|
||||
pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
|
||||
let [_, textarea_rect, _] = self.layout_areas(area);
|
||||
let state = *self.textarea_state.borrow();
|
||||
self.textarea.cursor_pos_with_state(textarea_rect, state)
|
||||
}
|
||||
@@ -1232,19 +1237,13 @@ impl ChatComposer {
|
||||
|
||||
impl WidgetRef for ChatComposer {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
let (popup_constraint, hint_spacing) = match &self.active_popup {
|
||||
ActivePopup::Command(popup) => (
|
||||
Constraint::Max(popup.calculate_required_height(area.width)),
|
||||
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), popup_constraint]).areas(area);
|
||||
let [composer_rect, textarea_rect, popup_rect] = self.layout_areas(area);
|
||||
if !matches!(self.active_popup, ActivePopup::None) {
|
||||
buf.set_style(
|
||||
popup_rect,
|
||||
user_message_style(terminal_palette::default_bg()),
|
||||
);
|
||||
}
|
||||
match &self.active_popup {
|
||||
ActivePopup::Command(popup) => {
|
||||
popup.render_ref(popup_rect, buf);
|
||||
@@ -1253,16 +1252,9 @@ impl WidgetRef for ChatComposer {
|
||||
popup.render_ref(popup_rect, buf);
|
||||
}
|
||||
ActivePopup::None => {
|
||||
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_rect = popup_rect;
|
||||
hint_rect.x += 2;
|
||||
hint_rect.width = hint_rect.width.saturating_sub(2);
|
||||
render_footer(
|
||||
hint_rect,
|
||||
buf,
|
||||
@@ -1276,23 +1268,17 @@ impl WidgetRef for ChatComposer {
|
||||
);
|
||||
}
|
||||
}
|
||||
let border_style = if self.has_focus {
|
||||
Style::default().fg(Color::Cyan)
|
||||
} else {
|
||||
Style::default().add_modifier(Modifier::DIM)
|
||||
};
|
||||
Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.border_type(BorderType::QuadrantOutside)
|
||||
.border_style(border_style)
|
||||
.render_ref(
|
||||
Rect::new(textarea_rect.x, textarea_rect.y, 1, textarea_rect.height),
|
||||
buf,
|
||||
);
|
||||
let mut textarea_rect = textarea_rect;
|
||||
// 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 style = user_message_style(terminal_palette::default_bg());
|
||||
let mut block_rect = composer_rect;
|
||||
block_rect.y = composer_rect.y.saturating_sub(1);
|
||||
block_rect.height = composer_rect.height.saturating_add(1);
|
||||
Block::default().style(style).render_ref(block_rect, buf);
|
||||
buf.set_span(
|
||||
composer_rect.x,
|
||||
composer_rect.y,
|
||||
&"›".bold(),
|
||||
composer_rect.width,
|
||||
);
|
||||
|
||||
let mut state = self.textarea_state.borrow_mut();
|
||||
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
|
||||
|
||||
@@ -209,8 +209,8 @@ impl WidgetRef for CommandPopup {
|
||||
&rows,
|
||||
&self.state,
|
||||
MAX_POPUP_ROWS,
|
||||
false,
|
||||
"no matches",
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,8 +144,8 @@ impl WidgetRef for &FileSearchPopup {
|
||||
&rows_all,
|
||||
&self.state,
|
||||
MAX_POPUP_ROWS,
|
||||
false,
|
||||
empty_message,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,8 +454,8 @@ impl BottomPaneView for ListSelectionView {
|
||||
&rows,
|
||||
&self.state,
|
||||
MAX_POPUP_ROWS,
|
||||
true,
|
||||
"no matches",
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,9 @@ impl BottomPane {
|
||||
let status_height = self
|
||||
.status
|
||||
.as_ref()
|
||||
.map_or(0, |status| status.desired_height(area.width));
|
||||
.map_or(0, |status| status.desired_height(area.width))
|
||||
.min(area.height.saturating_sub(1));
|
||||
|
||||
Layout::vertical([Constraint::Max(status_height), Constraint::Min(1)]).areas(area)
|
||||
}
|
||||
}
|
||||
@@ -616,7 +618,7 @@ mod tests {
|
||||
|
||||
// Composer placeholder should be visible somewhere below.
|
||||
let mut found_composer = false;
|
||||
for y in 1..area.height.saturating_sub(2) {
|
||||
for y in 1..area.height {
|
||||
let mut row = String::new();
|
||||
for x in 0..area.width {
|
||||
row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));
|
||||
|
||||
@@ -8,9 +8,6 @@ use ratatui::style::Style;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::Span;
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::widgets::BorderType;
|
||||
use ratatui::widgets::Borders;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::Widget;
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
@@ -119,15 +116,21 @@ pub(crate) fn render_rows(
|
||||
rows_all: &[GenericDisplayRow],
|
||||
state: &ScrollState,
|
||||
max_results: usize,
|
||||
_dim_non_selected: bool,
|
||||
empty_message: &str,
|
||||
include_border: bool,
|
||||
) {
|
||||
// Always draw a dim left border to match other popups.
|
||||
let block = Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.border_type(BorderType::QuadrantOutside)
|
||||
.border_style(Style::default().add_modifier(Modifier::DIM));
|
||||
block.render(area, buf);
|
||||
if include_border {
|
||||
use ratatui::widgets::Block;
|
||||
use ratatui::widgets::BorderType;
|
||||
use ratatui::widgets::Borders;
|
||||
|
||||
// Always draw a dim left border to match other popups.
|
||||
let block = Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.border_type(BorderType::QuadrantOutside)
|
||||
.border_style(Style::default().add_modifier(Modifier::DIM));
|
||||
block.render(area, buf);
|
||||
}
|
||||
|
||||
// Content renders to the right of the border with the same live prefix
|
||||
// padding used by the composer so the popup aligns with the input text.
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"▌ [Pasted Content 1002 chars][Pasted Content 1004 chars] "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
" "
|
||||
"⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||
"› [Pasted Content 1002 chars][Pasted Content 1004 chars] "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"▌ Ask Codex to do anything "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
" "
|
||||
"⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"▌ [Pasted Content 1005 chars] "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
" "
|
||||
"⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||
"› [Pasted Content 1005 chars] "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"▌ [Pasted Content 1003 chars][Pasted Content 1007 chars] another short paste "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
" "
|
||||
"⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||
"› [Pasted Content 1003 chars][Pasted Content 1007 chars] another short paste "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"▌ /mo "
|
||||
"▌ "
|
||||
"▌ /model choose what model and reasoning effort to use "
|
||||
"▌ /mention mention a file "
|
||||
" "
|
||||
"› /mo "
|
||||
" /model choose what model and reasoning effort to use "
|
||||
" /mention mention a file "
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"▌ short "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
"▌ "
|
||||
" "
|
||||
"⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||
"› short "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ⏎ send ⌃J newline ⌃T transcript ⌃C quit "
|
||||
|
||||
Reference in New Issue
Block a user