mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Show turn tips while working and after completion in the TUI (#48352)
## What changed - Show a random tip beneath the working status after 30 seconds, using the existing tooltip catalog and current key bindings. - Show completion tips only after successful turns with a final answer, starting with the third turn and spacing displayed tips by at least three turn starts. Limit completion tips to two per app session, and skip them when a working tip was already shown for that turn. - Respect `tui.show_tooltips`, hide tips during composer or transcript interactions, and omit tips when space is insufficient. Count exposure only when a tip is rendered. - Anchor completion tips after queued history updates, keep them outside transcript selection and search, and dismiss them on history replay or transcript clearing. ## Testing Add lifecycle tests for timing, completion cadence, duplicate notifications, failed and interrupted turns, and hidden tips. Add rendering snapshots and assertions for completion ordering, resizing, limited terminal space, and exclusion from transcript selection and search. GitOrigin-RevId: 9e956f5b0377e3be71f9574639c3c6ef11ad1168
This commit is contained in:
@@ -70,6 +70,7 @@ struct CachedLayout {
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) struct CellPresentation {
|
||||
separated: bool,
|
||||
turn_tip_space: bool,
|
||||
expanded: bool,
|
||||
disclosure: bool,
|
||||
}
|
||||
@@ -136,6 +137,7 @@ impl TranscriptView {
|
||||
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,
|
||||
};
|
||||
@@ -203,10 +205,15 @@ impl LayoutCache {
|
||||
return layout;
|
||||
}
|
||||
let layout = render();
|
||||
let layout = Arc::new(if presentation.separated {
|
||||
let layout = if presentation.separated {
|
||||
layout.with_leading_separator()
|
||||
} else {
|
||||
layout
|
||||
};
|
||||
let layout = Arc::new(if presentation.turn_tip_space {
|
||||
layout.with_leading_spacer()
|
||||
} else {
|
||||
layout
|
||||
});
|
||||
self.entries.retain(|entry| !entry.source.ptr_eq(&source));
|
||||
self.entries.push(CachedLayout {
|
||||
|
||||
@@ -47,6 +47,7 @@ fn layouts_refresh_for_width_animation_and_mutable_frames() {
|
||||
width,
|
||||
CellPresentation {
|
||||
separated: false,
|
||||
turn_tip_space: false,
|
||||
expanded: false,
|
||||
disclosure: false,
|
||||
},
|
||||
@@ -101,6 +102,7 @@ fn recent_entries_are_reused_and_old_entries_are_evicted() {
|
||||
/*width*/ 20,
|
||||
CellPresentation {
|
||||
separated: false,
|
||||
turn_tip_space: false,
|
||||
expanded: false,
|
||||
disclosure: false,
|
||||
},
|
||||
@@ -119,6 +121,7 @@ fn recent_entries_are_reused_and_old_entries_are_evicted() {
|
||||
/*width*/ 20,
|
||||
CellPresentation {
|
||||
separated: false,
|
||||
turn_tip_space: false,
|
||||
expanded: false,
|
||||
disclosure: false,
|
||||
},
|
||||
|
||||
@@ -142,7 +142,16 @@ impl TextLayout {
|
||||
|
||||
/// Add visual spacing between entries without changing any source position.
|
||||
pub(super) fn with_leading_separator(mut self) -> Self {
|
||||
if !self.separated && !self.rows.is_empty() {
|
||||
if !self.separated {
|
||||
self = self.with_leading_spacer();
|
||||
self.separated = !self.rows.is_empty();
|
||||
}
|
||||
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() {
|
||||
self.rows.insert(
|
||||
/*index*/ 0,
|
||||
TextRow {
|
||||
@@ -158,7 +167,6 @@ impl TextLayout {
|
||||
if let Some(control) = &mut self.disclosure_control {
|
||||
control.row += 1;
|
||||
}
|
||||
self.separated = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//! Reserve a temporary tip before completion metadata, outside selectable source text.
|
||||
//! Return its final draw area only when at least one response row remains visible.
|
||||
|
||||
use super::*;
|
||||
use crate::turn_tip::TurnTip;
|
||||
|
||||
impl TranscriptView {
|
||||
pub(crate) fn render_with_turn_tip_space(
|
||||
&mut self,
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
cells: &[Arc<dyn HistoryCell>],
|
||||
tip: Option<&TurnTip>,
|
||||
) -> Option<Rect> {
|
||||
let tip = tip.filter(|_| area.height > 1);
|
||||
self.turn_tip_key = tip.and_then(|_| {
|
||||
cells
|
||||
.last()
|
||||
.filter(|cell| {
|
||||
cell.as_any()
|
||||
.is::<crate::history_cell::FinalMessageSeparator>()
|
||||
&& !cell.display_lines(area.width).is_empty()
|
||||
})
|
||||
.map(EntryKey::cell)
|
||||
});
|
||||
self.render(
|
||||
Rect {
|
||||
height: area.height - u16::from(tip.is_some() && self.turn_tip_key.is_none()),
|
||||
..area
|
||||
},
|
||||
buf,
|
||||
cells,
|
||||
);
|
||||
// The final viewport includes prompt-header and footer reservations. Let the tip yield
|
||||
// rather than leave only completion metadata visible on a short terminal.
|
||||
if let Some(key) = self.turn_tip_key
|
||||
&& self.visible.first().is_none_or(|row| row.key == key)
|
||||
{
|
||||
self.turn_tip_key = None;
|
||||
self.render(area, buf, cells);
|
||||
return None;
|
||||
}
|
||||
tip?;
|
||||
let row = match self.turn_tip_key {
|
||||
Some(key) => self
|
||||
.visible
|
||||
.iter()
|
||||
.position(|row| row.key == key && row.row == 0)?,
|
||||
None => self.visible.len(),
|
||||
};
|
||||
let y = self.area.y.saturating_add(row as u16);
|
||||
(self.tail_visible && !self.visible.is_empty() && y < area.bottom()).then_some(Rect::new(
|
||||
self.area.x,
|
||||
y,
|
||||
self.area.width,
|
||||
/*height*/ 1,
|
||||
))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user