mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
Group consecutive transcript prompts into one background block (#56280)
## Why Consecutive user prompts in the rich transcript have extra blank spacing that breaks up their shared background. ## What changed - Join adjacent visible user prompts into one background block, skipping hidden entries when determining adjacency. - Keep prompts separated when intervening content becomes visible through detailed mode or search, and restore grouping when it is hidden again. - Preserve selectable source offsets and joined spacing when text is rewrapped, and reuse cached layouts when checking adjacency. ## Testing Add regression tests for prompt grouping, layout cache reuse, source offsets across rewrapping, detailed reasoning, and search-driven visibility. Update transcript snapshots to reflect the tighter spacing. GitOrigin-RevId: faca8001244b0ac883d9d670864c8f7856ac2bad
This commit is contained in:
committed by
copyberry
parent
d124fe9de1
commit
35e2102808
+4
-4
@@ -4,20 +4,20 @@ expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
80 columns
|
||||
/ T R A N S C R I P T / / / / / / / / / / / / / / / / / / / / / / / / / / / / /
|
||||
|
||||
› first prompt
|
||||
|
||||
|
||||
|
||||
› second prompt
|
||||
|
||||
|
||||
Browsing · ↑↓/jk scroll · ←→/hl prompts · ⌃t details · ↵ rewind · esc back
|
||||
|
||||
32 columns
|
||||
/ T R A N S C R I P T / / / / /
|
||||
|
||||
› first prompt
|
||||
|
||||
|
||||
|
||||
› second prompt
|
||||
|
||||
|
||||
Browsing · ↵ rewind · esc back
|
||||
|
||||
+4
-4
@@ -5,15 +5,15 @@ expression: "normalize_snapshot_paths(buffer_text(crate::custom_terminal::test_s
|
||||
|
||||
› first question
|
||||
|
||||
|
||||
|
||||
› second question
|
||||
|
||||
|
||||
|
||||
› third question
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
gpt-test default · /tmp/project
|
||||
|
||||
+2
-3
@@ -1,8 +1,9 @@
|
||||
---
|
||||
source: tui/src/app/tests.rs
|
||||
assertion_line: 8342
|
||||
expression: transcript
|
||||
---
|
||||
/init - create an AGENTS.md file with instructions for Codex
|
||||
/status - show current session configuration
|
||||
/permissions - choose what Codex is allowed to do
|
||||
/model - choose what model and reasoning effort to use
|
||||
/review - review any changes and find issues
|
||||
@@ -10,8 +11,6 @@ expression: transcript
|
||||
|
||||
› retained prompt
|
||||
|
||||
|
||||
|
||||
[Image #1]
|
||||
[Image #2]
|
||||
|
||||
|
||||
+2
-2
@@ -6,10 +6,10 @@ expression: buffer_text(&buffer)
|
||||
|
||||
› first
|
||||
|
||||
|
||||
|
||||
› second
|
||||
|
||||
|
||||
|
||||
⌃space select
|
||||
↑/↓ to scroll · pgup/pgdn to page · home/end to jump
|
||||
q close · f3 find · esc browse prompts
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: tui/src/transcript_view_tests.rs
|
||||
expression: "format!(\"{buffer:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 32, height: 12 },
|
||||
content: [
|
||||
" ",
|
||||
"› First steering message ",
|
||||
" ",
|
||||
"› Second steering message ",
|
||||
" ",
|
||||
" ",
|
||||
"• Assistant response ",
|
||||
" ",
|
||||
" ",
|
||||
"› Later user message ",
|
||||
" ",
|
||||
" ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Rgb(55, 57, 66), underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Reset, bg: Rgb(55, 57, 66), underline: Reset, modifier: BOLD | DIM,
|
||||
x: 2, y: 1, fg: Reset, bg: Rgb(55, 57, 66), underline: Reset, modifier: NONE,
|
||||
x: 0, y: 3, fg: Reset, bg: Rgb(55, 57, 66), underline: Reset, modifier: BOLD | DIM,
|
||||
x: 2, y: 3, fg: Reset, bg: Rgb(55, 57, 66), underline: Reset, modifier: NONE,
|
||||
x: 0, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 2, y: 6, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 8, fg: Reset, bg: Rgb(55, 57, 66), underline: Reset, modifier: NONE,
|
||||
x: 0, y: 9, fg: Reset, bg: Rgb(55, 57, 66), underline: Reset, modifier: BOLD | DIM,
|
||||
x: 2, y: 9, fg: Reset, bg: Rgb(55, 57, 66), underline: Reset, modifier: NONE,
|
||||
x: 0, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
@@ -58,6 +58,48 @@ pub(super) fn activity_layout(
|
||||
}
|
||||
}
|
||||
|
||||
fn cell_content_layout(
|
||||
cell: &dyn HistoryCell,
|
||||
width: u16,
|
||||
mode: HistoryRenderMode,
|
||||
presentation: CellPresentation,
|
||||
shortcut: Option<ShortcutHint>,
|
||||
) -> TextLayout {
|
||||
let mut layout = if presentation.disclosure {
|
||||
activity_layout(
|
||||
ActivityTranscriptLines {
|
||||
activity: if presentation.expanded {
|
||||
cell.expanded_hyperlink_lines(width)
|
||||
} else {
|
||||
cell.compact_hyperlink_lines(width)
|
||||
},
|
||||
auxiliary: Vec::new(),
|
||||
disclosure: cell.activity_disclosure(width),
|
||||
},
|
||||
width,
|
||||
presentation.expanded,
|
||||
shortcut,
|
||||
)
|
||||
} else {
|
||||
TextLayout::new(
|
||||
crate::history_cell::fullscreen_session_lines(cell, width, presentation.detailed, mode),
|
||||
width,
|
||||
)
|
||||
};
|
||||
if let Some(protocol) = presentation.image_protocol
|
||||
&& let Some(path) = cell.image_path()
|
||||
{
|
||||
layout.image =
|
||||
crate::transcript_image::TranscriptImage::load(path.as_path(), width, protocol)
|
||||
.map(Arc::new);
|
||||
}
|
||||
layout
|
||||
}
|
||||
|
||||
fn is_rich_user_prompt(cell: &dyn HistoryCell, mode: HistoryRenderMode) -> bool {
|
||||
mode == HistoryRenderMode::Rich && cell.as_any().is::<crate::history_cell::UserHistoryCell>()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct LayoutCache {
|
||||
entries: Vec<CachedLayout>,
|
||||
@@ -86,6 +128,7 @@ struct CachedLayout {
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct CellPresentation {
|
||||
separated: bool,
|
||||
shares_prompt_background: bool,
|
||||
turn_tip_space: bool,
|
||||
expanded: bool,
|
||||
disclosure: bool,
|
||||
@@ -94,6 +137,32 @@ pub(super) struct CellPresentation {
|
||||
}
|
||||
|
||||
impl TranscriptView {
|
||||
fn content_presentation(
|
||||
&mut self,
|
||||
cell: &dyn HistoryCell,
|
||||
full_content: bool,
|
||||
mode: HistoryRenderMode,
|
||||
) -> CellPresentation {
|
||||
let ids = cell.activity_ids();
|
||||
let disclosure = !self.detailed && mode == HistoryRenderMode::Rich && !ids.is_empty();
|
||||
let manually_expanded = self.disclosure.is_expanded(&ids);
|
||||
let reveal_image = ids.is_empty() || manually_expanded || self.detailed;
|
||||
if manually_expanded {
|
||||
self.disclosure.expanded.extend(ids);
|
||||
}
|
||||
CellPresentation {
|
||||
separated: false,
|
||||
shares_prompt_background: false,
|
||||
turn_tip_space: false,
|
||||
expanded: disclosure && (full_content || manually_expanded),
|
||||
disclosure,
|
||||
detailed: full_content,
|
||||
image_protocol: self
|
||||
.image_protocol
|
||||
.filter(|_| mode == HistoryRenderMode::Rich && reveal_image),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn layout(
|
||||
&mut self,
|
||||
cells: &[Arc<dyn HistoryCell>],
|
||||
@@ -168,62 +237,47 @@ impl TranscriptView {
|
||||
return Some(Arc::new(TextLayout::new(Vec::new(), width)));
|
||||
}
|
||||
let mode = self.mode;
|
||||
let ids = cell.activity_ids();
|
||||
let disclosure = !self.detailed && mode == HistoryRenderMode::Rich && !ids.is_empty();
|
||||
let manually_expanded = self.disclosure.is_expanded(&ids);
|
||||
let reveal_image = ids.is_empty() || manually_expanded || self.detailed;
|
||||
let expanded = disclosure && (full_content || manually_expanded);
|
||||
if manually_expanded {
|
||||
self.disclosure.expanded.extend(ids);
|
||||
}
|
||||
let separated = index > 0 && !cell.is_stream_continuation();
|
||||
let presentation = CellPresentation {
|
||||
separated,
|
||||
turn_tip_space: self.turn_tip_key == Some(EntryKey::cell(cell)),
|
||||
expanded,
|
||||
disclosure,
|
||||
detailed: full_content,
|
||||
image_protocol: self
|
||||
.image_protocol
|
||||
.filter(|_| mode == HistoryRenderMode::Rich && reveal_image),
|
||||
};
|
||||
let mut presentation = self.content_presentation(cell.as_ref(), full_content, mode);
|
||||
let shortcut = self
|
||||
.disclosure
|
||||
.keymap
|
||||
.primary_hint(KeymapContext::Global, "open_transcript");
|
||||
let shares_prompt_background = is_rich_user_prompt(cell.as_ref(), mode)
|
||||
&& (0..index)
|
||||
.rev()
|
||||
.find(|&previous| {
|
||||
let previous = &cells[previous];
|
||||
let key = EntryKey::cell(previous);
|
||||
if let Some(layout) = self.search.match_layout(key).or_else(|| {
|
||||
self.snapshot()
|
||||
.and_then(|snapshot| snapshot.pinned.get(&key))
|
||||
.cloned()
|
||||
}) {
|
||||
return layout.row_count() > 0;
|
||||
}
|
||||
let previous_presentation =
|
||||
self.content_presentation(previous.as_ref(), self.detailed, mode);
|
||||
self.cache
|
||||
.has_visible_content(previous, width, previous_presentation, || {
|
||||
cell_content_layout(
|
||||
previous.as_ref(),
|
||||
width,
|
||||
mode,
|
||||
previous_presentation,
|
||||
shortcut,
|
||||
)
|
||||
})
|
||||
})
|
||||
.is_some_and(|previous| is_rich_user_prompt(cells[previous].as_ref(), mode));
|
||||
presentation.separated =
|
||||
index > 0 && !cell.is_stream_continuation() && !shares_prompt_background;
|
||||
presentation.shares_prompt_background = shares_prompt_background;
|
||||
presentation.turn_tip_space = self.turn_tip_key == Some(EntryKey::cell(cell));
|
||||
Some(self.cache.get(cell, width, presentation, || {
|
||||
let mut layout = if disclosure {
|
||||
activity_layout(
|
||||
ActivityTranscriptLines {
|
||||
activity: if expanded {
|
||||
cell.expanded_hyperlink_lines(width)
|
||||
} else {
|
||||
cell.compact_hyperlink_lines(width)
|
||||
},
|
||||
auxiliary: Vec::new(),
|
||||
disclosure: cell.activity_disclosure(width),
|
||||
},
|
||||
width,
|
||||
expanded,
|
||||
shortcut,
|
||||
)
|
||||
} else {
|
||||
TextLayout::new(
|
||||
crate::history_cell::fullscreen_session_lines(
|
||||
cell.as_ref(),
|
||||
width,
|
||||
full_content,
|
||||
mode,
|
||||
),
|
||||
width,
|
||||
)
|
||||
};
|
||||
if let Some(protocol) = presentation.image_protocol
|
||||
&& let Some(path) = cell.image_path()
|
||||
{
|
||||
layout.image =
|
||||
crate::transcript_image::TranscriptImage::load(path.as_path(), width, protocol)
|
||||
.map(Arc::new);
|
||||
let mut layout =
|
||||
cell_content_layout(cell.as_ref(), width, mode, presentation, shortcut);
|
||||
if shares_prompt_background {
|
||||
layout = layout.without_leading_blank_row();
|
||||
}
|
||||
layout
|
||||
}))
|
||||
@@ -294,6 +348,36 @@ impl LayoutCache {
|
||||
layout
|
||||
}
|
||||
|
||||
fn has_visible_content(
|
||||
&mut self,
|
||||
cell: &Arc<dyn HistoryCell>,
|
||||
width: u16,
|
||||
presentation: CellPresentation,
|
||||
render: impl FnOnce() -> TextLayout,
|
||||
) -> bool {
|
||||
let source = Arc::downgrade(cell);
|
||||
let animation_tick = cell.transcript_animation_tick();
|
||||
if let Some(index) = self.entries.iter().position(|entry| {
|
||||
entry.width == width
|
||||
&& entry.presentation.expanded == presentation.expanded
|
||||
&& entry.presentation.disclosure == presentation.disclosure
|
||||
&& entry.presentation.detailed == presentation.detailed
|
||||
&& entry.presentation.image_protocol == presentation.image_protocol
|
||||
&& entry.source.ptr_eq(&source)
|
||||
&& (entry.rendered_frame == self.frame
|
||||
|| (cell.has_stable_transcript_height()
|
||||
&& entry.animation_tick == animation_tick))
|
||||
}) {
|
||||
let visible = self.entries[index].layout.row_count() > 0;
|
||||
if index + 1 != self.entries.len() {
|
||||
let entry = self.entries.remove(index);
|
||||
self.entries.push(entry);
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
self.get(cell, width, presentation, render).row_count() > 0
|
||||
}
|
||||
|
||||
pub(super) fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ fn layouts_refresh_for_width_animation_and_mutable_frames() {
|
||||
width,
|
||||
CellPresentation {
|
||||
separated: false,
|
||||
shares_prompt_background: false,
|
||||
turn_tip_space: false,
|
||||
expanded: false,
|
||||
disclosure: false,
|
||||
@@ -104,6 +105,7 @@ fn recent_entries_are_reused_and_old_entries_are_evicted() {
|
||||
/*width*/ 20,
|
||||
CellPresentation {
|
||||
separated: false,
|
||||
shares_prompt_background: false,
|
||||
turn_tip_space: false,
|
||||
expanded: false,
|
||||
disclosure: false,
|
||||
@@ -125,6 +127,7 @@ fn recent_entries_are_reused_and_old_entries_are_evicted() {
|
||||
/*width*/ 20,
|
||||
CellPresentation {
|
||||
separated: false,
|
||||
shares_prompt_background: false,
|
||||
turn_tip_space: false,
|
||||
expanded: false,
|
||||
disclosure: false,
|
||||
|
||||
@@ -103,7 +103,12 @@ impl TranscriptView {
|
||||
} else if self.detailed {
|
||||
self.layout(cells, anchor.index)?
|
||||
} else {
|
||||
self.entry_layout(cells, anchor.index, /*full_content*/ true)?
|
||||
// A candidate replaces the current match when found, so its spacing must not
|
||||
// depend on an expansion that will disappear at that point.
|
||||
let current = self.search.current.take();
|
||||
let layout = self.entry_layout(cells, anchor.index, /*full_content*/ true);
|
||||
self.search.current = current;
|
||||
layout?
|
||||
};
|
||||
self.search.scanning_layout = Some((anchor.key, Arc::clone(&layout)));
|
||||
Some(layout)
|
||||
|
||||
@@ -30,6 +30,87 @@ fn finish_scan(view: &mut TranscriptView, cells: &[Arc<dyn HistoryCell>]) {
|
||||
panic!("search did not yield after bounded scanning frames");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_match_separates_user_prompts_only_while_visible() {
|
||||
use crate::history_cell::ReasoningSummaryCell;
|
||||
use crate::history_cell::new_user_prompt;
|
||||
use std::path::Path;
|
||||
|
||||
crate::terminal_palette::with_test_default_colors(
|
||||
crate::terminal_probe::DefaultColors {
|
||||
fg: (235, 235, 235),
|
||||
bg: (18, 20, 30),
|
||||
},
|
||||
|| {
|
||||
let prompt = |text| {
|
||||
Arc::new(new_user_prompt(text, Vec::new(), Vec::new(), Vec::new()))
|
||||
as Arc<dyn HistoryCell>
|
||||
};
|
||||
let cells: Vec<Arc<dyn HistoryCell>> = vec![
|
||||
prompt("first".into()),
|
||||
Arc::new(ReasoningSummaryCell::new(
|
||||
"Reasoning".into(),
|
||||
"needle detail".into(),
|
||||
Path::new("."),
|
||||
/*transcript_only*/ true,
|
||||
)),
|
||||
prompt("needle second".into()),
|
||||
];
|
||||
let area = Rect::new(
|
||||
/*x*/ 0, /*y*/ 0, /*width*/ 30, /*height*/ 12,
|
||||
);
|
||||
let mut view = TranscriptView::default();
|
||||
view.render(area, &mut Buffer::empty(area), &cells);
|
||||
view.begin_search();
|
||||
view.paste_search("needle");
|
||||
finish_scan(&mut view, &cells);
|
||||
assert_eq!(view.search.current.as_ref().unwrap().anchor.index, 2);
|
||||
|
||||
view.handle_key(
|
||||
KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL),
|
||||
&cells,
|
||||
);
|
||||
finish_scan(&mut view, &cells);
|
||||
assert_eq!(view.search.current.as_ref().unwrap().anchor.index, 1);
|
||||
assert!(view.current_layout(&cells, /*index*/ 2).unwrap().separated);
|
||||
|
||||
view.handle_key(
|
||||
KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL),
|
||||
&cells,
|
||||
);
|
||||
finish_scan(&mut view, &cells);
|
||||
assert_eq!(view.search.current.as_ref().unwrap().anchor.index, 2);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
view.render(area, &mut buffer, &cells);
|
||||
let rendered = crate::transcript_view::tests::text(&buffer);
|
||||
let rows = rendered.lines().collect::<Vec<_>>();
|
||||
let first = rows.iter().position(|row| row.contains("› first")).unwrap();
|
||||
let second = rows
|
||||
.iter()
|
||||
.position(|row| row.contains("› needle second"))
|
||||
.unwrap();
|
||||
let background = buffer[(area.width - 1, first as u16)].bg;
|
||||
assert_ne!(background, ratatui::style::Color::Reset);
|
||||
let joined = (first..=second)
|
||||
.map(|row| {
|
||||
let marker = if buffer[(area.width - 1, row as u16)].bg == background {
|
||||
'#'
|
||||
} else {
|
||||
'.'
|
||||
};
|
||||
format!("{marker}|{}|", rows[row])
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
insta::assert_snapshot!(joined, @r"
|
||||
#|› first|
|
||||
#||
|
||||
#|› needle second|
|
||||
");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_keeps_context_and_allows_reading_without_losing_the_query() {
|
||||
let cells = vec![
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: tui/src/transcript_view/prompt_header_tests.rs
|
||||
assertion_line: 68
|
||||
expression: "snapshots.join(\"\\n\\n\")"
|
||||
---
|
||||
Latest
|
||||
@@ -25,11 +24,11 @@ three
|
||||
four
|
||||
|
||||
Prompt visible
|
||||
|
||||
› First question
|
||||
|
||||
|
||||
one
|
||||
two
|
||||
|
||||
Short screen
|
||||
eight
|
||||
|
||||
@@ -49,6 +49,7 @@ pub(super) struct TextLayout {
|
||||
width: u16,
|
||||
pub(super) separated: bool,
|
||||
pub(super) disclosure: bool,
|
||||
leading_blank_hidden: bool,
|
||||
disclosure_control: Option<DisclosureControl>,
|
||||
}
|
||||
|
||||
@@ -139,6 +140,9 @@ impl TextLayout {
|
||||
layout.with_disclosure_control_at(control.label.clone(), control.source_offset);
|
||||
}
|
||||
layout.image = self.image.clone();
|
||||
if self.leading_blank_hidden {
|
||||
layout = layout.without_leading_blank_row();
|
||||
}
|
||||
if self.separated {
|
||||
layout.with_leading_separator()
|
||||
} else {
|
||||
@@ -207,6 +211,22 @@ impl TextLayout {
|
||||
self
|
||||
}
|
||||
|
||||
/// Hide one leading blank row without changing selectable source offsets.
|
||||
pub(super) fn without_leading_blank_row(mut self) -> Self {
|
||||
if self
|
||||
.rows
|
||||
.first()
|
||||
.is_some_and(|row| line_width(&row.line.line) == 0)
|
||||
{
|
||||
self.rows.remove(/*index*/ 0);
|
||||
self.leading_blank_hidden = true;
|
||||
if let Some(control) = &mut self.disclosure_control {
|
||||
control.row = control.row.saturating_sub(/*rhs*/ 1);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Reserve one presentation-only row before existing spacing and source text.
|
||||
pub(super) fn with_leading_spacer(mut self) -> Self {
|
||||
if !self.rows.is_empty() {
|
||||
@@ -531,6 +551,7 @@ impl TextLayout {
|
||||
width,
|
||||
separated: false,
|
||||
disclosure: false,
|
||||
leading_blank_hidden: false,
|
||||
disclosure_control: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Viewport behavior and cache lifetime across scrolling, selection, and redraws.
|
||||
|
||||
use super::*;
|
||||
use crate::history_cell::new_user_prompt;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
@@ -38,6 +39,15 @@ pub(super) fn cell(text: impl Into<String>) -> Arc<dyn HistoryCell> {
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt(text: impl Into<String>) -> Arc<dyn HistoryCell> {
|
||||
Arc::new(new_user_prompt(
|
||||
text.into(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn render(
|
||||
view: &mut TranscriptView,
|
||||
cells: &[Arc<dyn HistoryCell>],
|
||||
@@ -63,6 +73,115 @@ pub(super) fn text(buffer: &Buffer) -> String {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consecutive_user_prompts_share_one_background_block() {
|
||||
use crate::history_cell::AgentMessageCell;
|
||||
use crate::history_cell::new_spoken_user_prompt;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
|
||||
let cells: Vec<Arc<dyn HistoryCell>> = vec![
|
||||
prompt("First steering message"),
|
||||
prompt("Second steering message"),
|
||||
Arc::new(AgentMessageCell::new(
|
||||
vec!["Assistant response".into()],
|
||||
/*is_first_line*/ true,
|
||||
)),
|
||||
Arc::new(new_spoken_user_prompt(String::new())),
|
||||
Arc::new(new_user_prompt(
|
||||
" ".into(),
|
||||
vec![TextElement::new((0..3).into(), Some(" ".into()))],
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
)),
|
||||
prompt("Later user message"),
|
||||
];
|
||||
crate::terminal_palette::with_test_default_colors(
|
||||
crate::terminal_probe::DefaultColors {
|
||||
fg: (235, 235, 235),
|
||||
bg: (18, 20, 30),
|
||||
},
|
||||
|| {
|
||||
let mut view = TranscriptView::default();
|
||||
let buffer = render(&mut view, &cells, /*width*/ 32, /*height*/ 12);
|
||||
insta::assert_snapshot!(
|
||||
"consecutive_user_prompts_share_one_background_block",
|
||||
format!("{buffer:?}")
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_adjacency_uses_cached_retained_presentations() {
|
||||
let renders = Arc::new(AtomicUsize::new(0));
|
||||
let cells: Vec<Arc<dyn HistoryCell>> = vec![
|
||||
Arc::new(TestCell {
|
||||
text: "response".into(),
|
||||
renders: Arc::clone(&renders),
|
||||
}),
|
||||
prompt("steering"),
|
||||
];
|
||||
let mut view = TranscriptView::default();
|
||||
|
||||
view.current_layout(&cells, /*index*/ 1);
|
||||
view.current_layout(&cells, /*index*/ 1);
|
||||
|
||||
assert_eq!(renders.load(Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn joined_prompt_preserves_source_offsets_across_rewrap() {
|
||||
let cells = vec![prompt("older"), prompt("one\n\ntwo")];
|
||||
let mut view = TranscriptView::default();
|
||||
let unjoined = [Arc::clone(&cells[1])];
|
||||
let source = view
|
||||
.current_layout(&unjoined, /*index*/ 0)
|
||||
.unwrap()
|
||||
.text()
|
||||
.to_owned();
|
||||
let joined = view.current_layout(&cells, /*index*/ 1).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
(joined.text(), joined.rewrap(/*width*/ 8).text()),
|
||||
(source.as_str(), source.as_str())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detailed_only_reasoning_separates_user_prompts() {
|
||||
use crate::history_cell::ReasoningSummaryCell;
|
||||
use std::path::Path;
|
||||
|
||||
let cells: Vec<Arc<dyn HistoryCell>> = vec![
|
||||
prompt("first"),
|
||||
Arc::new(ReasoningSummaryCell::new(
|
||||
"Reasoning".into(),
|
||||
"visible detail".into(),
|
||||
Path::new("."),
|
||||
/*transcript_only*/ true,
|
||||
)),
|
||||
prompt("second"),
|
||||
];
|
||||
let mut detailed = TranscriptView {
|
||||
detailed: true,
|
||||
..TranscriptView::default()
|
||||
};
|
||||
assert!(
|
||||
detailed
|
||||
.current_layout(&cells, /*index*/ 2)
|
||||
.unwrap()
|
||||
.separated
|
||||
);
|
||||
|
||||
let mut compact = TranscriptView::default();
|
||||
assert!(
|
||||
!compact
|
||||
.entry_layout(&cells, /*index*/ 2, /*full_content*/ true)
|
||||
.unwrap()
|
||||
.separated
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_keys_follow_pager_scroll_bindings() {
|
||||
let cells = [cell(
|
||||
|
||||
Reference in New Issue
Block a user