mirror of
https://github.com/openai/codex.git
synced 2026-10-21 04:48:12 +00:00
Make pinned transcript prompts clickable (#52778)
## What changed Clicking a pinned prompt header with an unmodified left click now scrolls the transcript to the original prompt. The click also ends text selection, cancels search, and clears activity focus. Track the prompt by entry identity so navigation remains correct when history is prepended. ## Testing Update the prompt-header snapshot test to navigate by clicking the header after prepending history and verify that the original prompt becomes visible. GitOrigin-RevId: dea811693853e3740909ab675b1339f701e01e02
This commit is contained in:
@@ -101,6 +101,7 @@ pub(crate) struct TranscriptView {
|
||||
live_key: Option<(u16, ActiveCellTranscriptKey)>,
|
||||
live_continuation: bool,
|
||||
area: Rect,
|
||||
prompt_header_target: Option<(Rect, EntryKey)>,
|
||||
suppressed_prompt_header: Option<prompt_header::SuppressedHeader>,
|
||||
visible: Vec<VisibleRow>,
|
||||
selection: Option<Selection>,
|
||||
@@ -137,6 +138,7 @@ impl Default for TranscriptView {
|
||||
live_key: None,
|
||||
live_continuation: false,
|
||||
area: Rect::default(),
|
||||
prompt_header_target: None,
|
||||
suppressed_prompt_header: None,
|
||||
visible: Vec::new(),
|
||||
selection: None,
|
||||
@@ -173,6 +175,7 @@ impl TranscriptView {
|
||||
|
||||
pub(crate) fn render(&mut self, area: Rect, buf: &mut Buffer, cells: &[Arc<dyn HistoryCell>]) {
|
||||
self.composer_tip = None;
|
||||
self.prompt_header_target = None;
|
||||
self.cache.begin_frame();
|
||||
self.sync_history_tail(cells);
|
||||
let current_cells = cells;
|
||||
@@ -217,8 +220,10 @@ impl TranscriptView {
|
||||
}
|
||||
// Following may advance into the next turn after reserving the header row.
|
||||
start = self.start(cells);
|
||||
if let Some(header) = prompt_header::line(cells, start.0, area.width) {
|
||||
header.render(Rect::new(area.x, area.y, area.width, /*height*/ 1), buf);
|
||||
if let Some((header, key)) = prompt_header::line(cells, start.0, area.width) {
|
||||
let header_area = Rect::new(area.x, area.y, area.width, /*height*/ 1);
|
||||
header.render(header_area, buf);
|
||||
self.prompt_header_target = Some((header_area, key));
|
||||
} else {
|
||||
body = area;
|
||||
self.area = area;
|
||||
|
||||
@@ -229,13 +229,29 @@ impl TranscriptView {
|
||||
{
|
||||
return Some(action);
|
||||
}
|
||||
let point = ScreenPosition::new(event.column, event.row);
|
||||
if mode == MouseMode::Interactive
|
||||
&& event.kind == MouseEventKind::Down(MouseButton::Left)
|
||||
&& event.modifiers.is_empty()
|
||||
&& let Some((area, key)) = self.prompt_header_target
|
||||
&& area.contains(point)
|
||||
{
|
||||
self.last_click = None;
|
||||
if let Some(index) = cells.iter().position(|cell| EntryKey::cell(cell) == key) {
|
||||
self.end_selection(cells);
|
||||
self.cancel_search();
|
||||
self.clear_activity_focus();
|
||||
self.jump_to_entry(cells, index);
|
||||
}
|
||||
return Some(ViewAction::Changed);
|
||||
}
|
||||
if mode == MouseMode::Interactive
|
||||
&& event.kind == MouseEventKind::Down(MouseButton::Left)
|
||||
&& event
|
||||
.modifiers
|
||||
.intersects(KeyModifiers::CONTROL | KeyModifiers::SUPER)
|
||||
&& let Some((area, tip)) = &self.composer_tip
|
||||
&& area.contains(ScreenPosition::new(event.column, event.row))
|
||||
&& area.contains(point)
|
||||
{
|
||||
let column = usize::from(event.column - area.x);
|
||||
return tip
|
||||
@@ -245,9 +261,7 @@ impl TranscriptView {
|
||||
.terminal_destination()
|
||||
.map(ViewAction::OpenLink);
|
||||
}
|
||||
let inside = self
|
||||
.area
|
||||
.contains(ScreenPosition::new(event.column, event.row));
|
||||
let inside = self.area.contains(point);
|
||||
let dragging = self
|
||||
.selection
|
||||
.as_ref()
|
||||
|
||||
@@ -19,7 +19,7 @@ pub(super) fn line(
|
||||
cells: &[Arc<dyn HistoryCell>],
|
||||
first: usize,
|
||||
width: u16,
|
||||
) -> Option<Line<'static>> {
|
||||
) -> Option<(Line<'static>, EntryKey)> {
|
||||
// A prompt that is still visible supplies its own context. Only pin a preceding prompt.
|
||||
if width < 16
|
||||
|| cells
|
||||
@@ -29,11 +29,16 @@ pub(super) fn line(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let prompt = cells[..first.min(cells.len())]
|
||||
let (index, prompt) = cells[..first.min(cells.len())]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.filter_map(|cell| cell.as_any().downcast_ref::<UserHistoryCell>())
|
||||
.find(|prompt| prompt.has_visible_content())?;
|
||||
.find_map(|(index, cell)| {
|
||||
cell.as_any()
|
||||
.downcast_ref::<UserHistoryCell>()
|
||||
.filter(|prompt| prompt.has_visible_content())
|
||||
.map(|prompt| (index, prompt))
|
||||
})?;
|
||||
// Bound work even for pasted multi-megabyte prompts; sanitize before painting controls.
|
||||
let prefix: String = prompt.message.chars().take(/*n*/ 512).collect();
|
||||
let message = sanitize_user_text(prefix.into())
|
||||
@@ -45,17 +50,20 @@ pub(super) fn line(
|
||||
} else {
|
||||
message
|
||||
};
|
||||
Some(truncate_line_with_ellipsis_if_overflow(
|
||||
Line::from(vec![
|
||||
if prompt.spoken {
|
||||
"› ".red().bold()
|
||||
} else {
|
||||
"› ".bold().dim()
|
||||
},
|
||||
message.into(),
|
||||
])
|
||||
.style(crate::style::history_prompt_style()),
|
||||
usize::from(width.saturating_sub(1)),
|
||||
Some((
|
||||
truncate_line_with_ellipsis_if_overflow(
|
||||
Line::from(vec![
|
||||
if prompt.spoken {
|
||||
"› ".red().bold()
|
||||
} else {
|
||||
"› ".bold().dim()
|
||||
},
|
||||
message.into(),
|
||||
])
|
||||
.style(crate::style::history_prompt_style()),
|
||||
usize::from(width.saturating_sub(1)),
|
||||
),
|
||||
EntryKey::cell(&cells[index]),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,18 @@ fn prompt_header_tracks_reading_and_yields_to_visible_prompts_and_short_screens(
|
||||
&mut view, &cells, /*width*/ 30, /*height*/ 5
|
||||
))
|
||||
));
|
||||
view.jump_to_entry(&cells, /*index*/ 1);
|
||||
assert!(matches!(
|
||||
view.handle_mouse(
|
||||
MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: 0,
|
||||
row: 0,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
},
|
||||
&cells,
|
||||
),
|
||||
Some(ViewAction::Changed)
|
||||
));
|
||||
snapshots.push(format!(
|
||||
"Prompt visible\n{}",
|
||||
text(&render(
|
||||
@@ -80,7 +91,7 @@ fn prompt_header_tracks_reading_and_yields_to_visible_prompts_and_short_screens(
|
||||
#[test]
|
||||
fn header_sanitizes_controls_and_truncates_by_display_width() {
|
||||
let cells = vec![user("\x1b[31m界 café\r\nnext question"), cell("answer")];
|
||||
let line = line(&cells, /*first*/ 1, /*width*/ 18).unwrap();
|
||||
let (line, _) = line(&cells, /*first*/ 1, /*width*/ 18).unwrap();
|
||||
assert!(line.width() <= 18);
|
||||
insta::assert_snapshot!(line.to_string());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user