mirror of
https://github.com/openai/codex.git
synced 2026-05-22 12:04:19 +00:00
feat(tui): align transcript footer stats with session picker
This commit is contained in:
@@ -99,6 +99,14 @@ pub(crate) fn render_footer_separator(area: Rect, buf: &mut Buffer, label: Strin
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn first_fitting_right_label(width: u16, labels: &[String]) -> String {
|
||||
labels
|
||||
.iter()
|
||||
.find(|label| UnicodeWidthStr::width(label.as_str()) < width as usize)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn render_footer_line_with_optional_right(
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
@@ -372,4 +380,25 @@ mod tests {
|
||||
|
||||
assert_eq!(buffer_text(&buf, area), "sta…");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_fitting_right_label_picks_first_that_fits() {
|
||||
let labels = vec![
|
||||
" 10 / 120 · 55% ".to_string(),
|
||||
" 10/120 · 55% ".to_string(),
|
||||
" 55% ".to_string(),
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
first_fitting_right_label(/*width*/ 15, &labels),
|
||||
" 10/120 · 55% "
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_fitting_right_label_returns_empty_when_nothing_fits() {
|
||||
let labels = vec![" 100% ".to_string()];
|
||||
|
||||
assert_eq!(first_fitting_right_label(/*width*/ 4, &labels), "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use std::time::Instant;
|
||||
use crate::chatwidget::ActiveCellTranscriptKey;
|
||||
use crate::chatwidget::CopyStatus;
|
||||
use crate::footer_hints::FooterHint;
|
||||
use crate::footer_hints::first_fitting_right_label;
|
||||
use crate::footer_hints::footer_hint_line_for_row;
|
||||
use crate::footer_hints::render_footer_line_with_optional_right;
|
||||
use crate::footer_hints::render_footer_separator;
|
||||
@@ -141,6 +142,8 @@ struct PagerView {
|
||||
renderables: Vec<Box<dyn Renderable>>,
|
||||
scroll_offset: usize,
|
||||
title: String,
|
||||
footer_separator_label: String,
|
||||
show_header_progress: bool,
|
||||
keymap: PagerKeymap,
|
||||
last_content_height: Option<usize>,
|
||||
last_rendered_height: Option<usize>,
|
||||
@@ -168,6 +171,8 @@ impl PagerView {
|
||||
renderables,
|
||||
scroll_offset,
|
||||
title,
|
||||
footer_separator_label: String::new(),
|
||||
show_header_progress: true,
|
||||
keymap,
|
||||
last_content_height: None,
|
||||
last_rendered_height: None,
|
||||
@@ -235,10 +240,13 @@ impl PagerView {
|
||||
fn render_header(&self, area: Rect, content_area: Rect, buf: &mut Buffer, total_len: usize) {
|
||||
let header = Rect::new(area.x, area.y, area.width, 1);
|
||||
render_footer_separator(header, buf, String::new());
|
||||
let percent = self.scroll_percent(content_area.height, total_len);
|
||||
format!(" {} · {percent}% ", self.title)
|
||||
.dim()
|
||||
.render_ref(header, buf);
|
||||
let title = if self.show_header_progress {
|
||||
let percent = self.scroll_percent(content_area.height, total_len);
|
||||
format!(" {} · {percent}% ", self.title)
|
||||
} else {
|
||||
format!(" {} ", self.title)
|
||||
};
|
||||
title.dim().render_ref(header, buf);
|
||||
}
|
||||
|
||||
fn render_content(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
@@ -306,7 +314,7 @@ impl PagerView {
|
||||
let sep_y = content_area.bottom();
|
||||
let sep_rect = Rect::new(full_area.x, sep_y, full_area.width, 1);
|
||||
|
||||
render_footer_separator(sep_rect, buf, String::new());
|
||||
render_footer_separator(sep_rect, buf, self.footer_separator_label.clone());
|
||||
}
|
||||
|
||||
fn scroll_percent(&self, content_height: u16, total_len: usize) -> u8 {
|
||||
@@ -535,12 +543,16 @@ impl TranscriptOverlay {
|
||||
state: TranscriptOverlayState,
|
||||
) -> Self {
|
||||
Self {
|
||||
view: PagerView::new(
|
||||
Self::render_cells(&transcript_cells, state.highlight_cell, state.render_mode),
|
||||
"Transcript".to_string(),
|
||||
state.scroll_offset,
|
||||
keymap,
|
||||
),
|
||||
view: {
|
||||
let mut view = PagerView::new(
|
||||
Self::render_cells(&transcript_cells, state.highlight_cell, state.render_mode),
|
||||
"Transcript".to_string(),
|
||||
state.scroll_offset,
|
||||
keymap,
|
||||
);
|
||||
view.show_header_progress = false;
|
||||
view
|
||||
},
|
||||
cells: transcript_cells,
|
||||
highlight_cell: state.highlight_cell,
|
||||
render_mode: state.render_mode,
|
||||
@@ -832,26 +844,34 @@ impl TranscriptOverlay {
|
||||
}
|
||||
|
||||
fn header_title(&self) -> String {
|
||||
"Transcript".to_string()
|
||||
}
|
||||
|
||||
fn footer_progress_label(&self, content_height: u16, total_len: usize, width: u16) -> String {
|
||||
let total = self
|
||||
.cells
|
||||
.iter()
|
||||
.filter(|cell| cell.is_user_prompt())
|
||||
.count();
|
||||
let Some(highlight_cell) = self.highlight_cell else {
|
||||
let noun = if total == 1 { "prompt" } else { "prompts" };
|
||||
return format!("Transcript · {total} {noun}");
|
||||
};
|
||||
let selected = self
|
||||
.cells
|
||||
.iter()
|
||||
.take(highlight_cell.saturating_add(1))
|
||||
.filter(|cell| cell.is_user_prompt())
|
||||
.count();
|
||||
if selected == 0 || total == 0 {
|
||||
let noun = if total == 1 { "prompt" } else { "prompts" };
|
||||
return format!("Transcript · {total} {noun}");
|
||||
}
|
||||
format!("Transcript · {selected}/{total}")
|
||||
.highlight_cell
|
||||
.and_then(|highlight_cell| {
|
||||
let selected = self
|
||||
.cells
|
||||
.iter()
|
||||
.take(highlight_cell.saturating_add(1))
|
||||
.filter(|cell| cell.is_user_prompt())
|
||||
.count();
|
||||
(selected > 0).then_some(selected)
|
||||
})
|
||||
.unwrap_or(total);
|
||||
let percent = self.view.scroll_percent(content_height, total_len);
|
||||
let labels = [
|
||||
format!(" {selected} / {total} · {percent}% "),
|
||||
format!(" {selected}/{total} · {percent}% "),
|
||||
format!(" {percent}% "),
|
||||
];
|
||||
first_fitting_right_label(width, &labels)
|
||||
}
|
||||
|
||||
fn rebuild_renderables(&mut self) {
|
||||
@@ -1036,6 +1056,10 @@ impl TranscriptOverlay {
|
||||
let top = Rect::new(area.x, area.y, area.width, top_h);
|
||||
let bottom = Rect::new(area.x, area.y + top_h, area.width, 3);
|
||||
self.view.title = self.header_title();
|
||||
let content_area = self.view.content_area(top);
|
||||
let total_len = self.view.content_height(content_area.width);
|
||||
self.view.footer_separator_label =
|
||||
self.footer_progress_label(content_area.height, total_len, top.width);
|
||||
self.view.render(top, buf);
|
||||
self.render_hints(bottom, buf);
|
||||
}
|
||||
@@ -1440,7 +1464,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcript_header_counts_selected_user_prompt() {
|
||||
fn transcript_header_title_is_stable() {
|
||||
let mut overlay = transcript_overlay(vec![
|
||||
user_cell("first"),
|
||||
Arc::new(AgentMessageCell::new(
|
||||
@@ -1450,13 +1474,48 @@ mod tests {
|
||||
user_cell("second"),
|
||||
]);
|
||||
|
||||
assert_eq!(overlay.header_title(), "Transcript · 2 prompts");
|
||||
assert_eq!(overlay.header_title(), "Transcript");
|
||||
|
||||
overlay.move_prompt_selection(PromptSelectionDirection::Previous);
|
||||
assert_eq!(overlay.header_title(), "Transcript · 2/2");
|
||||
assert_eq!(overlay.header_title(), "Transcript");
|
||||
|
||||
overlay.move_prompt_selection(PromptSelectionDirection::Previous);
|
||||
assert_eq!(overlay.header_title(), "Transcript · 1/2");
|
||||
assert_eq!(overlay.header_title(), "Transcript");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcript_footer_progress_label_counts_selected_user_prompt() {
|
||||
let mut overlay = transcript_overlay(vec![
|
||||
user_cell("first"),
|
||||
Arc::new(AgentMessageCell::new(
|
||||
vec![Line::from("assistant")],
|
||||
/*is_first_line*/ true,
|
||||
)),
|
||||
user_cell("second"),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
overlay.footer_progress_label(
|
||||
/*content_height*/ 5, /*total_len*/ 12, /*width*/ 80
|
||||
),
|
||||
" 2 / 2 · 100% "
|
||||
);
|
||||
|
||||
overlay.move_prompt_selection(PromptSelectionDirection::Previous);
|
||||
assert_eq!(
|
||||
overlay.footer_progress_label(
|
||||
/*content_height*/ 5, /*total_len*/ 12, /*width*/ 80
|
||||
),
|
||||
" 2 / 2 · 100% "
|
||||
);
|
||||
|
||||
overlay.move_prompt_selection(PromptSelectionDirection::Previous);
|
||||
assert_eq!(
|
||||
overlay.footer_progress_label(
|
||||
/*content_height*/ 5, /*total_len*/ 12, /*width*/ 80
|
||||
),
|
||||
" 1 / 2 · 100% "
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::app_server_session::AppServerSession;
|
||||
use crate::color::blend;
|
||||
use crate::color::is_light;
|
||||
use crate::footer_hints::FooterHint;
|
||||
use crate::footer_hints::first_fitting_right_label;
|
||||
use crate::footer_hints::footer_hint_line_for_row;
|
||||
use crate::footer_hints::render_footer_separator;
|
||||
use crate::git_action_directives::parse_assistant_markdown;
|
||||
@@ -2048,10 +2049,7 @@ fn picker_footer_progress_label(state: &PickerState, list_height: u16, width: u1
|
||||
format!(" {position}/{total} · {percent}% "),
|
||||
format!(" {percent}% "),
|
||||
];
|
||||
labels
|
||||
.into_iter()
|
||||
.find(|label| UnicodeWidthStr::width(label.as_str()) < width as usize)
|
||||
.unwrap_or_default()
|
||||
first_fitting_right_label(width, &labels)
|
||||
}
|
||||
|
||||
fn picker_footer_percent(state: &PickerState, list_height: u16) -> u8 {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: tui/src/pager_overlay.rs
|
||||
expression: snapshot
|
||||
---
|
||||
Transcript · 0 prompts · 0% ───────────────────────────────────────────────────
|
||||
Transcript ────────────────────────────────────────────────────────────────────
|
||||
• Added foo.txt (+2 -0)
|
||||
1 +hello
|
||||
2 +world
|
||||
@@ -10,6 +10,6 @@ expression: snapshot
|
||||
• Added foo.txt (+2 -0)
|
||||
1 +hello
|
||||
2 +world
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
─────────────────────────────────────────────────────────────────── 0 / 0 · 0% ─
|
||||
↑/↓ scroll ←/→ prompts pgup/pgdn page home/end jump
|
||||
q quit ctrl + o copy ⌥ + r raw esc/← prev
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
source: tui/src/pager_overlay.rs
|
||||
expression: "render_snapshot(&mut overlay, Rect::new(0, 0, 80, 8),)"
|
||||
---
|
||||
Transcript · 1 prompt · 100% ──────────────────────────────────────────────────
|
||||
Transcript ────────────────────────────────────────────────────────────────────
|
||||
|
||||
› prompt
|
||||
|
||||
────────────────────────────────────────────────────────────────────────────────
|
||||
───────────────────────────────────────────────────────────────── 1 / 1 · 100% ─
|
||||
↑/↓ scroll ←/→ prompts pgup/pgdn page … Copied selected turn to clipboard
|
||||
q quit ctrl + o copy ⌥ + r raw esc/← prev
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
source: tui/src/pager_overlay.rs
|
||||
expression: "render_snapshot(&mut overlay, Rect::new(0, 0, 28, 8),)"
|
||||
---
|
||||
Transcript · 1 prompt · 100
|
||||
Transcript ────────────────
|
||||
|
||||
› prompt
|
||||
|
||||
────────────────────────────
|
||||
───────────── 1 / 1 · 100% ─
|
||||
No agent response to copy f…
|
||||
q ctrl + o ⌥ + r
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
source: tui/src/pager_overlay.rs
|
||||
expression: term.backend()
|
||||
---
|
||||
" Transcript · 0 prompts · 100% ─────────"
|
||||
" Transcript ────────────────────────────"
|
||||
"alpha "
|
||||
" "
|
||||
"tail "
|
||||
"~ "
|
||||
"~ "
|
||||
"────────────────────────────────────────"
|
||||
"───────────────────────── 0 / 0 · 100% ─"
|
||||
" ↑/↓ ←/→ pgup/pgdn home/end "
|
||||
" q ctrl + o ⌥ + r esc/← "
|
||||
" "
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
source: tui/src/pager_overlay.rs
|
||||
expression: term.backend()
|
||||
---
|
||||
" Transcript · 0 prompts · 100% ─────────"
|
||||
" Transcript ────────────────────────────"
|
||||
"alpha "
|
||||
" "
|
||||
"beta "
|
||||
" "
|
||||
"gamma "
|
||||
"────────────────────────────────────────"
|
||||
"───────────────────────── 0 / 0 · 100% ─"
|
||||
" ↑/↓ ←/→ pgup/pgdn home/end "
|
||||
" q ctrl + o ⌥ + r esc/← "
|
||||
" "
|
||||
|
||||
Reference in New Issue
Block a user