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
2 changes: 2 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ pub(crate) struct App {
has_emitted_history_lines: bool,
transcript_reflow: TranscriptReflowState,
initial_history_replay_buffer: Option<InitialHistoryReplayBuffer>,
pub(crate) scrollback_has_older_history: bool,

pub(crate) enhanced_keys_supported: bool,
pub(crate) keymap: RuntimeKeymap,
Expand Down Expand Up @@ -1057,6 +1058,7 @@ See the Codex keymap documentation for supported actions and examples."
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
initial_history_replay_buffer: None,
scrollback_has_older_history: false,
commit_anim_running: Arc::new(AtomicBool::new(false)),
status_line_invalid_items_warned: status_line_invalid_items_warned.clone(),
terminal_title_invalid_items_warned: terminal_title_invalid_items_warned.clone(),
Expand Down
30 changes: 22 additions & 8 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use super::*;
use crate::app_server_session::ForkGoalContinuation;
use crate::config_update::format_config_error;
use crate::external_agent_config_migration::flow::ExternalAgentConfigMigrationFlowOutcome;
use crate::pager_overlay::TranscriptHistoryState;
#[cfg(target_os = "windows")]
use codex_config::types::WindowsSandboxModeToml;

Expand Down Expand Up @@ -43,6 +44,12 @@ impl App {
.await
{
app_server.cancel_older_history_page(thread_id);
if self.chat_widget.thread_id() == Some(thread_id)
&& let Some(Overlay::Transcript(overlay)) = self.overlay.as_mut()
{
overlay.set_history_state(TranscriptHistoryState::Failed);
tui.frame_requester().schedule_frame();
}
tracing::warn!(%thread_id, error = %err, "failed to load older transcript history");
}
}
Expand Down Expand Up @@ -282,12 +289,13 @@ impl App {
self.refresh_in_memory_config_from_disk_best_effort("forking the thread")
.await;
let config = self.fresh_session_config();
let started = match app_server
.thread_read(thread_id, /*include_turns*/ true)
.await
{
Ok(thread) => match crate::app_backtrack::backtrack_fork_before_turn_id(
&thread.turns,
let turns = match self.thread_event_channels.get(&thread_id) {
Some(channel) => Some(channel.store.lock().await.turns.clone()),
None => None,
};
let started = match turns {
Some(turns) => match crate::app_backtrack::backtrack_fork_before_turn_id(
&turns,
nth_user_message,
&mut prompt,
) {
Expand All @@ -296,7 +304,7 @@ impl App {
|| app_server.has_older_history(thread_id) =>
{
let before_turn_id = before_turn_id
.or_else(|| thread.turns.first().map(|turn| turn.id.clone()));
.or_else(|| turns.first().map(|turn| turn.id.clone()));
app_server
.fork_thread_at(
config.clone(),
Expand All @@ -316,7 +324,9 @@ impl App {
}
Err(err) => Err(err),
},
Err(err) => Err(err),
None => Err(color_eyre::eyre::eyre!(
"the selected thread is no longer available for prompt editing"
)),
};
match started {
Ok(forked) => {
Expand Down Expand Up @@ -352,6 +362,10 @@ impl App {
self.insert_history_cell(tui, cell);
}
AppEvent::EndInitialHistoryReplayBuffer => {
self.scrollback_has_older_history = self
.chat_widget
.thread_id()
.is_some_and(|thread_id| app_server.has_older_history(thread_id));
self.finish_initial_history_replay_buffer(tui);
}
AppEvent::ConsolidateAgentMessage {
Expand Down
59 changes: 59 additions & 0 deletions codex-rs/tui/src/app/history_pagination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,56 @@
use std::collections::HashSet;

use super::*;
use crate::app_server_session::HISTORY_ITEM_PAGE_LIMIT;
use crate::app_server_session::thread_items_page_params;
use crate::history_cell::UserHistoryCell;
use crate::pager_overlay::TranscriptHistoryState;
use crate::thread_transcript::RawReasoningVisibility;
use crate::thread_transcript::thread_items_to_transcript_cells;
use codex_app_server_protocol::ClientRequest;
use codex_app_server_protocol::ThreadItemsListResponse;

impl App {
/// Start one bounded page request shared by transcript-history navigation.
pub(crate) fn request_older_history_page(
&self,
app_server: &mut AppServerSession,
thread_id: ThreadId,
) -> bool {
let Some(cursor) = app_server.begin_older_history_page(thread_id) else {
return false;
};
tracing::debug!(
%thread_id,
%cursor,
overlay = self.overlay.is_some(),
"loading older transcript history page"
);
let request_id = app_server.next_request_id();
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let result = request_handle
.request_typed::<ThreadItemsListResponse>(ClientRequest::ThreadItemsList {
request_id,
params: thread_items_page_params(
thread_id,
/*turn_id*/ None,
Some(cursor.clone()),
HISTORY_ITEM_PAGE_LIMIT,
),
})
.await
.map_err(|err| err.to_string());
app_event_tx.send(AppEvent::OlderThreadHistoryLoaded {
thread_id,
cursor,
result,
});
});
true
}

pub(super) async fn handle_older_history_page(
&mut self,
tui: &mut tui::Tui,
Expand Down Expand Up @@ -173,9 +217,24 @@ impl App {
.count(),
);
}
self.scrollback_has_older_history = app_server.has_older_history(thread_id);
let mut continue_to_start = false;
if let Some(Overlay::Transcript(overlay)) = self.overlay.as_mut() {
let index = overlay.prepend(cells.clone(), width);
self.transcript_cells.splice(index..index, cells);
let previous_state = overlay.set_history_state(if self.scrollback_has_older_history {
TranscriptHistoryState::Partial
} else {
TranscriptHistoryState::Complete
});
continue_to_start = previous_state == TranscriptHistoryState::LoadingBeginning
&& self.scrollback_has_older_history;
}
if continue_to_start
&& self.request_older_history_page(app_server, thread_id)
&& let Some(Overlay::Transcript(overlay)) = self.overlay.as_mut()
{
overlay.set_history_state(TranscriptHistoryState::LoadingBeginning);
}
if self.backtrack.overlay_preview_active {
self.apply_backtrack_selection_internal(self.backtrack.nth_user_message);
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/app/history_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ impl App {
self.chat_widget.clear_pending_token_activity_refreshes();
self.chat_widget.clear_pending_rate_limit_reset_hint();
self.initial_history_replay_buffer = None;
self.scrollback_has_older_history = false;
self.backtrack = BacktrackState::default();
self.backtrack_render_pending = false;
self.skill_load_warnings.clear();
Expand Down
12 changes: 5 additions & 7 deletions codex-rs/tui/src/app/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,11 @@ impl App {
}

if app_keymap_shortcuts_available && self.keymap.app.open_transcript.is_pressed(key_event) {
// Enter alternate screen and set viewport to full size.
let _ = tui.enter_alt_screen();
self.overlay = Some(Overlay::new_transcript(
self.transcript_cells.clone(),
self.keymap.pager.clone(),
));
tui.frame_requester().schedule_frame();
self.scrollback_has_older_history = self
.chat_widget
.thread_id()
.is_some_and(|thread_id| app_server.has_older_history(thread_id));
self.open_transcript_overlay(tui);
return;
}

Expand Down
52 changes: 49 additions & 3 deletions codex-rs/tui/src/app/safety_buffering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
use super::session_lifecycle::ThreadAttachPresentation;
use super::*;
use crate::app_server_session::ForkGoalContinuation;
use crate::app_server_session::HISTORY_ITEM_PAGE_LIMIT;
use crate::chatwidget::ThreadInputState;
use crate::chatwidget::ThreadInputStateRestoreMode;
use crate::chatwidget::UserMessage;
use codex_app_server_protocol::ThreadHistoryMode;
use codex_app_server_protocol::TurnItemsView;
use codex_app_server_protocol::UserInput;

pub(super) struct SafetyBufferedRetry {
Expand Down Expand Up @@ -76,9 +79,52 @@ impl App {
return;
}

let thread = match app_server
.thread_read(thread_id, /*include_turns*/ true)
.await
let thread = match async {
let mut thread = app_server
.thread_read(thread_id, /*include_turns*/ false)
.await?;
if thread.history_mode == ThreadHistoryMode::Legacy {
app_server
.hydrate_initial_thread_history(
&mut thread,
/*turn_cursor*/ None,
/*item_cursor*/ None,
/*config*/ None,
crate::app_server_session::HistoryHydrationScope::Initial,
)
.await?;
} else {
let page = app_server
.thread_turns_page(thread_id, /*cursor*/ None)
.await?;
thread.turns = page.data.into_iter().rev().collect();
if let Some(turn_index) = thread.turns.iter().position(|turn| turn.id == turn_id) {
let page = app_server
.thread_items_page(
thread_id,
Some(&turn_id),
/*cursor*/ None,
HISTORY_ITEM_PAGE_LIMIT,
)
.await?;
if page.next_cursor.is_some() {
color_eyre::eyre::bail!(
"Cannot safely retry a turn whose input exceeds the bounded history page."
);
}
let turn = &mut thread.turns[turn_index];
turn.items = page
.data
.into_iter()
.rev()
.map(|entry| entry.item)
.collect();
turn.items_view = TurnItemsView::Full;
}
}
Ok::<_, color_eyre::Report>(thread)
}
.await
{
Ok(thread) => thread,
Err(err) => {
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/tui/src/app/safety_buffering_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,14 @@ fn retry_rejects_a_stale_or_in_progress_turn() {
assert!(safety_retry_fork_point(&in_progress, "missing").is_err());
assert!(safety_retry_fork_point(&previous_in_progress, "turn-2").is_err());
}

#[test]
fn retry_accepts_a_targeted_latest_turn_and_its_completed_predecessor() {
let turns = vec![
turn("turn-1", TurnStatus::Completed),
turn("turn-2", TurnStatus::Interrupted),
];

assert!(safety_retry_fork_point(&turns, "turn-2").is_ok());
assert!(safety_retry_fork_point(&turns[1..], "turn-2").is_ok());
}
28 changes: 15 additions & 13 deletions codex-rs/tui/src/app/session_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,22 +384,24 @@ impl App {
error = %resume_err,
"failed to resume live thread for selection; falling back to thread/read"
);
let (thread, turns) = match app_server
.thread_read(thread_id, /*include_turns*/ true)
let mut thread = app_server
.thread_read(thread_id, /*include_turns*/ false)
.await?;
match app_server
.hydrate_initial_thread_history(
&mut thread,
/*turn_cursor*/ None,
/*item_cursor*/ None,
Some(&self.config),
crate::app_server_session::HistoryHydrationScope::Initial,
)
.await
{
Ok(thread) => {
let turns = thread.turns.clone();
(thread, turns)
}
Err(err) if Self::can_fallback_from_include_turns_error(&err) => {
let thread = app_server
.thread_read(thread_id, /*include_turns*/ false)
.await?;
(thread, Vec::new())
}
Ok(()) => {}
Err(err) if Self::can_fallback_from_include_turns_error(&err) => {}
Err(err) => return Err(err),
};
}
let turns = thread.turns.clone();
if turns.is_empty() {
// A `thread/read` fallback without turns would create a blank local replay
// channel with no live listener attached, which blocks later real re-attach.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/app/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub(super) async fn make_test_app() -> App {
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
initial_history_replay_buffer: None,
scrollback_has_older_history: false,
enhanced_keys_supported: false,
keymap: crate::keymap::RuntimeKeymap::defaults(),
key_chord_matcher: crate::keymap::KeyChordMatcher::default(),
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4740,6 +4740,7 @@ async fn make_test_app() -> App {
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
initial_history_replay_buffer: None,
scrollback_has_older_history: false,
enhanced_keys_supported: false,
keymap: crate::keymap::RuntimeKeymap::defaults(),
key_chord_matcher: crate::keymap::KeyChordMatcher::default(),
Expand Down Expand Up @@ -4809,6 +4810,7 @@ async fn make_test_app_with_channels() -> (
has_emitted_history_lines: false,
transcript_reflow: TranscriptReflowState::default(),
initial_history_replay_buffer: None,
scrollback_has_older_history: false,
enhanced_keys_supported: false,
keymap: crate::keymap::RuntimeKeymap::defaults(),
key_chord_matcher: crate::keymap::KeyChordMatcher::default(),
Expand Down
Loading
Loading