Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ struct SessionSummary {

#[derive(Debug, Default)]
struct InitialHistoryReplayBuffer {
retained_lines: VecDeque<Line<'static>>,
retained_lines: VecDeque<crate::terminal_hyperlinks::HyperlinkLine>,
render_from_transcript_tail: bool,
}

Expand All @@ -498,7 +498,7 @@ pub(crate) struct App {

// Pager overlay state (Transcript or Static like Diff)
pub(crate) overlay: Option<Overlay>,
pub(crate) deferred_history_lines: Vec<Line<'static>>,
pub(crate) deferred_history_lines: Vec<crate::terminal_hyperlinks::HyperlinkLine>,
has_emitted_history_lines: bool,
transcript_reflow: TranscriptReflowState,
initial_history_replay_buffer: Option<InitialHistoryReplayBuffer>,
Expand Down
45 changes: 31 additions & 14 deletions codex-rs/tui/src/app/resize_reflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ use super::InitialHistoryReplayBuffer;
use crate::history_cell;
use crate::history_cell::HistoryCell;
use crate::insert_history::HistoryLineWrapPolicy;
use crate::terminal_hyperlinks::HyperlinkLine;
use crate::transcript_reflow::TRANSCRIPT_REFLOW_DEBOUNCE;
use crate::tui;

struct ReflowCellDisplay {
lines: Vec<Line<'static>>,
lines: Vec<HyperlinkLine>,
is_stream_continuation: bool,
}

Expand All @@ -41,7 +42,7 @@ struct ReflowCellDisplay {
/// already-wrapped rows. Callers should keep treating `transcript_cells` as the source of truth; the
/// rows here are a transient render product for a single terminal width.
pub(super) struct ReflowRenderResult {
pub(super) lines: Vec<Line<'static>>,
pub(super) lines: Vec<HyperlinkLine>,
}

pub(super) fn trailing_run_start<T: 'static>(transcript_cells: &[Arc<dyn HistoryCell>]) -> usize {
Expand Down Expand Up @@ -75,12 +76,12 @@ impl App {
&mut self,
cell: &dyn HistoryCell,
width: u16,
) -> Vec<Line<'static>> {
) -> Vec<HyperlinkLine> {
let mut display =
cell.display_lines_for_mode(width, self.chat_widget.history_render_mode());
cell.display_hyperlink_lines_for_mode(width, self.chat_widget.history_render_mode());
if !display.is_empty() && !cell.is_stream_continuation() {
if self.has_emitted_history_lines {
display.insert(0, Line::from(""));
display.insert(/*index*/ 0, HyperlinkLine::new(Line::from("")));
} else {
self.has_emitted_history_lines = true;
}
Expand All @@ -101,7 +102,10 @@ impl App {
if self.overlay.is_some() {
self.deferred_history_lines.extend(display);
} else {
tui.insert_history_lines_with_wrap_policy(display, self.history_line_wrap_policy());
tui.insert_history_hyperlink_lines_with_wrap_policy(
display,
self.history_line_wrap_policy(),
);
}
}

Expand Down Expand Up @@ -153,14 +157,20 @@ impl App {
let width = tui.terminal.last_known_screen_size.width;
let reflowed_lines = self.render_transcript_lines_for_reflow(width).lines;
if !reflowed_lines.is_empty() {
tui.insert_history_lines(reflowed_lines);
tui.insert_history_hyperlink_lines_with_wrap_policy(
reflowed_lines,
self.history_line_wrap_policy(),
);
}
}
return;
}

let retained_lines = buffer.retained_lines.into_iter().collect::<Vec<_>>();
tui.insert_history_lines_with_wrap_policy(retained_lines, self.history_line_wrap_policy());
tui.insert_history_hyperlink_lines_with_wrap_policy(
retained_lines,
self.history_line_wrap_policy(),
);
}

pub(super) fn insert_history_cell_lines_with_initial_replay_buffer(
Expand Down Expand Up @@ -190,7 +200,10 @@ impl App {
} else if self.overlay.is_some() {
self.deferred_history_lines.extend(display);
} else {
tui.insert_history_lines_with_wrap_policy(display, self.history_line_wrap_policy());
tui.insert_history_hyperlink_lines_with_wrap_policy(
display,
self.history_line_wrap_policy(),
);
}
}
}
Expand All @@ -210,7 +223,7 @@ impl App {
/// here would make copy, transcript overlay, and future replay paths disagree about history.
pub(super) fn buffer_initial_history_replay_display_lines(
buffer: &mut InitialHistoryReplayBuffer,
display: Vec<Line<'static>>,
display: Vec<HyperlinkLine>,
max_rows: usize,
) {
buffer.retained_lines.extend(display);
Expand Down Expand Up @@ -437,7 +450,7 @@ impl App {

self.deferred_history_lines.clear();
if !reflowed_lines.is_empty() {
tui.insert_history_lines_with_wrap_policy(
tui.insert_history_hyperlink_lines_with_wrap_policy(
reflowed_lines,
self.history_line_wrap_policy(),
);
Expand All @@ -462,7 +475,8 @@ impl App {
while start > 0 {
start -= 1;
let cell = self.transcript_cells[start].clone();
let lines = cell.display_lines_for_mode(width, self.chat_widget.history_render_mode());
let lines = cell
.display_hyperlink_lines_for_mode(width, self.chat_widget.history_render_mode());
rendered_rows += lines.len();
cell_displays.push_front(ReflowCellDisplay {
lines,
Expand All @@ -482,7 +496,10 @@ impl App {
start -= 1;
let cell = self.transcript_cells[start].clone();
cell_displays.push_front(ReflowCellDisplay {
lines: cell.display_lines_for_mode(width, self.chat_widget.history_render_mode()),
lines: cell.display_hyperlink_lines_for_mode(
width,
self.chat_widget.history_render_mode(),
),
is_stream_continuation: cell.is_stream_continuation(),
});
}
Expand All @@ -492,7 +509,7 @@ impl App {
for display in cell_displays {
if !display.lines.is_empty() && !display.is_stream_continuation {
if has_emitted_history_lines {
reflowed_lines.push(Line::from(""));
reflowed_lines.push(HyperlinkLine::new(Line::from("")));
} else {
has_emitted_history_lines = true;
}
Expand Down
11 changes: 6 additions & 5 deletions codex-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3906,8 +3906,9 @@ fn plain_line_cell(text: impl Into<String>) -> Arc<dyn HistoryCell> {
Arc::new(PlainHistoryCell::new(vec![Line::from(text.into())])) as Arc<dyn HistoryCell>
}

fn rendered_line_text(line: &Line<'static>) -> String {
line.spans
fn rendered_line_text(line: &crate::terminal_hyperlinks::HyperlinkLine) -> String {
line.line
.spans
.iter()
.map(|span| span.content.as_ref())
.collect()
Expand Down Expand Up @@ -4019,7 +4020,7 @@ async fn initial_replay_buffer_keeps_recent_rows_when_row_cap_present() {
app.initial_history_replay_buffer
.as_mut()
.expect("initial replay buffer active"),
vec![Line::from(format!("line {index}"))],
vec![Line::from(format!("line {index}")).into()],
/*max_rows*/ 3,
);
}
Expand Down Expand Up @@ -4925,7 +4926,7 @@ async fn queued_rollback_syncs_overlay_and_clears_deferred_history() {
app.transcript_cells.clone(),
app.keymap.pager.clone(),
));
app.deferred_history_lines = vec![Line::from("stale buffered line")];
app.deferred_history_lines = vec![Line::from("stale buffered line").into()];
app.backtrack.overlay_preview_active = true;
app.backtrack.nth_user_message = 1;

Expand Down Expand Up @@ -5459,7 +5460,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() {
app.transcript_cells.clone(),
crate::keymap::RuntimeKeymap::defaults().pager,
));
app.deferred_history_lines = vec![Line::from("stale buffered line")];
app.deferred_history_lines = vec![Line::from("stale buffered line").into()];
app.has_emitted_history_lines = true;
app.backtrack.primed = true;
app.backtrack.overlay_preview_active = true;
Expand Down
14 changes: 10 additions & 4 deletions codex-rs/tui/src/app_backtrack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,10 @@ impl App {
let was_backtrack = self.backtrack.overlay_preview_active;
if !self.deferred_history_lines.is_empty() {
let lines = std::mem::take(&mut self.deferred_history_lines);
tui.insert_history_lines_with_wrap_policy(lines, self.history_line_wrap_policy());
tui.insert_history_hyperlink_lines_with_wrap_policy(
lines,
self.history_line_wrap_policy(),
);
}
self.overlay = None;
self.backtrack.overlay_preview_active = false;
Expand All @@ -263,8 +266,11 @@ impl App {
.chat_widget
.history_wrap_width(tui.terminal.last_known_screen_size.width);
for cell in &self.transcript_cells {
tui.insert_history_lines_with_wrap_policy(
cell.display_lines_for_mode(width, self.chat_widget.history_render_mode()),
tui.insert_history_hyperlink_lines_with_wrap_policy(
cell.display_hyperlink_lines_for_mode(
width,
self.chat_widget.history_render_mode(),
),
self.history_line_wrap_policy(),
);
}
Expand Down Expand Up @@ -399,7 +405,7 @@ impl App {
tui.draw(u16::MAX, |frame| {
let width = frame.area().width.max(1);
t.sync_live_tail(width, active_key, |w| {
chat_widget.active_cell_transcript_lines(w)
chat_widget.active_cell_transcript_hyperlink_lines(w)
});
t.render(frame.area(), frame.buffer);
})?;
Expand Down
11 changes: 9 additions & 2 deletions codex-rs/tui/src/bottom_pane/app_link_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,7 @@ impl crate::render::renderable::Renderable for AppLinkView {
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.render(inner, buf);
crate::terminal_hyperlinks::mark_url_hyperlink(buf, inner, &self.url);

if actions_area.height > 0 {
let actions_area = Rect {
Expand Down Expand Up @@ -1005,7 +1006,10 @@ mod tests {
if symbol.is_empty() {
' '
} else {
symbol.chars().next().unwrap_or(' ')
crate::terminal_hyperlinks::strip_osc8(symbol)
.chars()
.next()
.unwrap_or(' ')
}
})
.collect::<String>()
Expand Down Expand Up @@ -1325,7 +1329,10 @@ mod tests {
if symbol.is_empty() {
' '
} else {
symbol.chars().next().unwrap_or(' ')
crate::terminal_hyperlinks::strip_osc8(symbol)
.chars()
.next()
.unwrap_or(' ')
}
})
.collect::<String>()
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/tui/src/bottom_pane/feedback_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ pub(crate) fn feedback_success_cell(
include_logs: bool,
thread_id: &str,
feedback_audience: FeedbackAudience,
) -> history_cell::PlainHistoryCell {
) -> history_cell::WebHyperlinkHistoryCell {
let prefix = if include_logs {
"• Feedback uploaded."
} else {
Expand Down Expand Up @@ -359,7 +359,7 @@ pub(crate) fn feedback_success_cell(
]);
}
}
history_cell::PlainHistoryCell::new(lines)
history_cell::WebHyperlinkHistoryCell::new(lines)
}

fn issue_url_for_category(
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/bottom_pane/memories_settings_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ impl Renderable for MemoriesSettingsView {
}
if self.reset_confirmation.is_none() {
self.docs_link.clone().render(docs_area, buf);
crate::terminal_hyperlinks::mark_url_hyperlink(buf, docs_area, MEMORIES_DOC_URL);
}

let hint_area = Rect {
Expand Down
23 changes: 17 additions & 6 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
//! visible immediately.
//!
//! The transcript overlay is kept in sync by `App::overlay_forward_event`, which syncs a live tail
//! during draws using `active_cell_transcript_key()` and `active_cell_transcript_lines()`. The
//! during draws using `active_cell_transcript_key()` and
//! `active_cell_transcript_hyperlink_lines()`. The
//! cache key is designed to change when the active cell mutates in place or when its transcript
//! output is time-dependent so the overlay can refresh its cached tail without rebuilding it on
//! every draw.
Expand Down Expand Up @@ -77,6 +78,7 @@ use crate::status::StatusHistoryHandle;
use crate::status::format_directory_display;
use crate::status::format_tokens_compact;
use crate::status::rate_limit_snapshot_display_for_limit;
use crate::terminal_hyperlinks::HyperlinkLine;
use crate::terminal_title::SetTerminalTitleResult;
use crate::terminal_title::clear_terminal_title;
use crate::terminal_title::set_terminal_title;
Expand Down Expand Up @@ -1824,28 +1826,37 @@ impl ChatWidget {
})
}

/// Returns the active cell's transcript lines for a given terminal width.
/// Returns the active cell's annotated transcript lines for a given terminal width.
///
/// This is a convenience for the transcript overlay live-tail path, and it intentionally
/// filters out empty results so the overlay can treat "nothing to render" as "no tail". Callers
/// should pass the same width the overlay uses; using a different width will cause wrapping
/// mismatches between the main viewport and the transcript overlay.
pub(crate) fn active_cell_transcript_lines(&self, width: u16) -> Option<Vec<Line<'static>>> {
pub(crate) fn active_cell_transcript_hyperlink_lines(
&self,
width: u16,
) -> Option<Vec<HyperlinkLine>> {
let mut lines = Vec::new();
if let Some(cell) = self.transcript.active_cell.as_ref() {
lines.extend(cell.transcript_lines(width));
lines.extend(cell.transcript_hyperlink_lines(width));
}
if let Some(hook_cell) = self.active_hook_cell.as_ref() {
// Compute hook lines first so hidden hooks do not add a separator.
let hook_lines = hook_cell.transcript_lines(width);
let hook_lines = hook_cell.transcript_hyperlink_lines(width);
if !hook_lines.is_empty() && !lines.is_empty() {
lines.push("".into());
lines.push(HyperlinkLine::from(""));
}
lines.extend(hook_lines);
}
(!lines.is_empty()).then_some(lines)
}

#[cfg(test)]
pub(crate) fn active_cell_transcript_lines(&self, width: u16) -> Option<Vec<Line<'static>>> {
self.active_cell_transcript_hyperlink_lines(width)
.map(crate::terminal_hyperlinks::visible_lines)
}

/// Return a reference to the widget's current config (includes any
/// runtime overrides applied via TUI, e.g., model or approval policy).
pub(crate) fn config_ref(&self) -> &Config {
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2200,7 +2200,7 @@ async fn memories_settings_popup_snapshot() {

chat.open_memories_popup();

let popup = render_bottom_popup(&chat, /*width*/ 80);
let popup = strip_osc8_for_snapshot(&render_bottom_popup(&chat, /*width*/ 80));
assert_chatwidget_snapshot!("memories_settings_popup", popup);
}

Expand Down
Loading
Loading