mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Keep fork lineage ahead of paginated transcript history (#54044)
## Why Loading older history could place transcript entries before the `Thread forked from` notice, separating fork lineage from the session header. ## What changed Treat fork lineage as a session prefix in both the TUI transcript and transcript overlay so older pages are inserted after it. ## Testing Add regression tests for repeated history prepends in both paths, verifying that fork lineage stays first. GitOrigin-RevId: 71497fb7ffa992c72ee08617d318aa3119e71275
This commit is contained in:
@@ -6,9 +6,8 @@ use std::ops::Range;
|
||||
use super::*;
|
||||
use crate::app_server_session::HISTORY_ITEM_PAGE_LIMIT;
|
||||
use crate::app_server_session::thread_items_page_params;
|
||||
use crate::history_cell::SessionHeaderHistoryCell;
|
||||
use crate::history_cell::SessionInfoCell;
|
||||
use crate::history_cell::UserHistoryCell;
|
||||
use crate::history_cell::is_session_history_prefix_cell;
|
||||
use crate::pager_overlay::TranscriptHistoryState;
|
||||
use crate::thread_transcript::RawReasoningVisibility;
|
||||
use crate::thread_transcript::thread_items_to_transcript_cells;
|
||||
@@ -265,10 +264,7 @@ impl App {
|
||||
} else {
|
||||
self.transcript_cells
|
||||
.iter()
|
||||
.rposition(|cell| {
|
||||
cell.as_any().is::<SessionInfoCell>()
|
||||
|| cell.as_any().is::<SessionHeaderHistoryCell>()
|
||||
})
|
||||
.rposition(|cell| is_session_history_prefix_cell(cell.as_ref()))
|
||||
.map_or(/*default*/ 0, |index| index.saturating_add(/*rhs*/ 1))
|
||||
};
|
||||
let inserted = index..index + cells.len();
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::*;
|
||||
use crate::app::test_support::make_test_app;
|
||||
use crate::history_cell::ComputerActivityCell;
|
||||
use crate::history_cell::FinalMessageSeparator;
|
||||
use crate::history_cell::ForkedThreadHistoryCell;
|
||||
use codex_app_server_protocol::TurnItemsView;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -60,6 +61,45 @@ fn user_cell(message: &str) -> Arc<dyn HistoryCell> {
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fork_lineage_stays_before_repeated_older_history_pages() {
|
||||
let mut app = make_test_app().await;
|
||||
app.transcript_cells = vec![
|
||||
Arc::new(ForkedThreadHistoryCell(
|
||||
crate::history_cell::PlainHistoryCell::new(vec!["Thread forked from parent".into()]),
|
||||
)),
|
||||
Arc::new(crate::history_cell::PlainHistoryCell::new(vec![
|
||||
"recent history".into(),
|
||||
])),
|
||||
];
|
||||
|
||||
app.prepend_older_transcript_cells(vec![Arc::new(crate::history_cell::PlainHistoryCell::new(
|
||||
vec!["older history".into()],
|
||||
))]);
|
||||
app.prepend_older_transcript_cells(vec![Arc::new(crate::history_cell::PlainHistoryCell::new(
|
||||
vec!["oldest history".into()],
|
||||
))]);
|
||||
|
||||
let rendered = app
|
||||
.transcript_cells
|
||||
.iter()
|
||||
.flat_map(|cell| cell.display_lines(/*width*/ 80))
|
||||
.map(|line| {
|
||||
line.spans
|
||||
.into_iter()
|
||||
.map(|span| span.content.into_owned())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
insta::assert_snapshot!(rendered, @r###"
|
||||
Thread forked from parent
|
||||
oldest history
|
||||
older history
|
||||
recent history
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_prompt_matching_removes_only_the_hidden_occurrence() {
|
||||
let cells = vec![user_cell("repeat"), user_cell("other"), user_cell("repeat")];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! checks the previous model when it applies the final selection.
|
||||
|
||||
use super::*;
|
||||
use crate::history_cell::ForkedThreadHistoryCell;
|
||||
use crate::style::accent_color;
|
||||
|
||||
impl ChatWidget {
|
||||
@@ -310,7 +311,7 @@ impl ChatWidget {
|
||||
.into()
|
||||
};
|
||||
self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new(
|
||||
PlainHistoryCell::new(vec![line]),
|
||||
ForkedThreadHistoryCell(PlainHistoryCell::new(vec![line])),
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,20 @@ impl HistoryCell for SessionNoticeCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fork lineage that stays with the session header when older history is prepended.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ForkedThreadHistoryCell(pub(crate) PlainHistoryCell);
|
||||
|
||||
impl HistoryCell for ForkedThreadHistoryCell {
|
||||
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
self.0.display_lines(width)
|
||||
}
|
||||
|
||||
fn raw_lines(&self) -> Vec<Line<'static>> {
|
||||
self.0.raw_lines()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SessionInfoCell(CompositeHistoryCell);
|
||||
|
||||
@@ -165,6 +179,13 @@ impl HistoryCell for SessionInfoCell {
|
||||
}
|
||||
}
|
||||
|
||||
/// Session-level cells that stay ahead of every loaded transcript page.
|
||||
pub(crate) fn is_session_history_prefix_cell(cell: &dyn HistoryCell) -> bool {
|
||||
cell.as_any().is::<SessionInfoCell>()
|
||||
|| cell.as_any().is::<SessionHeaderHistoryCell>()
|
||||
|| cell.as_any().is::<ForkedThreadHistoryCell>()
|
||||
}
|
||||
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "keep local preferences separate while the legacy Config parameter is still required"
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
use super::*;
|
||||
use crate::history_cell::HistoryRenderMode;
|
||||
use crate::history_cell::SessionHeaderHistoryCell;
|
||||
use crate::history_cell::SessionInfoCell;
|
||||
use crate::history_cell::is_session_history_prefix_cell;
|
||||
use crate::keymap::RuntimeKeymap;
|
||||
use crate::motion::MotionMode;
|
||||
use crate::transcript_view::TranscriptBookmark;
|
||||
@@ -313,10 +312,7 @@ impl TranscriptOverlay {
|
||||
let index = self
|
||||
.cells
|
||||
.iter()
|
||||
.rposition(|cell| {
|
||||
cell.as_any().is::<SessionInfoCell>()
|
||||
|| cell.as_any().is::<SessionHeaderHistoryCell>()
|
||||
})
|
||||
.rposition(|cell| is_session_history_prefix_cell(cell.as_ref()))
|
||||
.map_or(/*default*/ 0, |index| index + 1);
|
||||
let added = cells.len();
|
||||
self.cells.splice(index..index, cells);
|
||||
|
||||
@@ -60,6 +60,29 @@ fn scrolling_prepend_and_resize_preserve_reading_content() {
|
||||
assert!(!overlay.is_scrolled_to_bottom());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_prepend_keeps_fork_lineage_first() {
|
||||
let lineage: Arc<dyn HistoryCell> = Arc::new(history_cell::ForkedThreadHistoryCell(
|
||||
history_cell::PlainHistoryCell::new(vec!["fork lineage".into()]),
|
||||
));
|
||||
let mut overlay = transcript_overlay(vec![
|
||||
Arc::clone(&lineage),
|
||||
Arc::new(TestCell {
|
||||
lines: vec!["recent".into()],
|
||||
}),
|
||||
]);
|
||||
|
||||
let older = overlay.prepend(vec![Arc::new(TestCell {
|
||||
lines: vec!["older".into()],
|
||||
})]);
|
||||
let oldest = overlay.prepend(vec![Arc::new(TestCell {
|
||||
lines: vec!["oldest".into()],
|
||||
})]);
|
||||
|
||||
assert_eq!((older, oldest), (1, 1));
|
||||
assert!(Arc::ptr_eq(&overlay.cells[0], &lineage));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn activity_hint_uses_the_configured_pager_jump_binding() -> Result<()> {
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
|
||||
Reference in New Issue
Block a user