mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Fix resume transcript preview line selection (#38605)
## What changed - Scan legacy and paginated transcript items newest-first and stop after collecting the six newest nonblank preview lines. - Keep paging through bounded paginated history when newer items do not produce visible preview text, while preserving chronological display order and assistant markdown rewrites. - Move transcript preview loading into a focused module and add coverage for reverse scanning, legacy/paginated parity, and paging past invisible items. GitOrigin-RevId: 576afe37d034b32db363691bd4f401815fffaea7
This commit is contained in:
@@ -5,13 +5,9 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::app_server_session::HISTORY_ITEM_PAGE_LIMIT;
|
||||
use crate::app_server_session::HISTORY_ITEM_SCAN_LIMIT;
|
||||
use crate::clipboard_paste::normalize_pasted_search_query;
|
||||
use crate::color::blend;
|
||||
use crate::color::is_light;
|
||||
use crate::git_action_directives::parse_assistant_markdown;
|
||||
use crate::inline_visualization::InlineVisualizationContext;
|
||||
use crate::key_hint::KeyBindingListExt;
|
||||
use crate::key_hint::is_plain_text_key_event;
|
||||
use crate::keymap::ListAction;
|
||||
@@ -44,8 +40,6 @@ use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadArchiveResponse;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadListCwdFilter;
|
||||
use codex_app_server_protocol::ThreadListParams;
|
||||
use codex_app_server_protocol::ThreadSortKey;
|
||||
@@ -85,6 +79,11 @@ mod page_loading;
|
||||
use page_loading::PageLoadMode;
|
||||
use page_loading::PaginationState;
|
||||
|
||||
#[path = "resume_picker_transcript_preview.rs"]
|
||||
mod transcript_preview;
|
||||
|
||||
pub(crate) use transcript_preview::load_transcript_preview;
|
||||
|
||||
const PAGE_SIZE: usize = 25;
|
||||
const LOAD_NEAR_THRESHOLD: usize = 5;
|
||||
const SESSION_META_INDENT_WIDTH: usize = 2;
|
||||
@@ -836,13 +835,13 @@ enum SessionTranscriptState {
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct TranscriptPreviewLine {
|
||||
speaker: TranscriptPreviewSpeaker,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum TranscriptPreviewSpeaker {
|
||||
User,
|
||||
Assistant,
|
||||
@@ -882,145 +881,6 @@ async fn load_app_server_page(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn load_transcript_preview(
|
||||
app_server: &mut AppServerSession,
|
||||
thread_id: ThreadId,
|
||||
config: Option<&Config>,
|
||||
) -> std::io::Result<Vec<TranscriptPreviewLine>> {
|
||||
const MAX_PREVIEW_LINES: usize = 6;
|
||||
|
||||
let mut thread = app_server
|
||||
.thread_read(thread_id, /*include_turns*/ false)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
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
|
||||
.map_err(std::io::Error::other)?;
|
||||
}
|
||||
let cwd = thread.cwd.as_path();
|
||||
let inline_visualization_context = config.and_then(|config| {
|
||||
ThreadId::from_string(&thread.id)
|
||||
.ok()
|
||||
.and_then(|thread_id| InlineVisualizationContext::from_config(config, thread_id))
|
||||
});
|
||||
let mut lines = if thread.history_mode == ThreadHistoryMode::Paginated {
|
||||
let mut groups = Vec::new();
|
||||
let mut visible_lines = 0_usize;
|
||||
let mut scanned_items = 0_usize;
|
||||
let mut cursor = None;
|
||||
let mut seen_cursors = HashSet::new();
|
||||
loop {
|
||||
let page = app_server
|
||||
.thread_items_page(
|
||||
thread_id,
|
||||
/*turn_id*/ None,
|
||||
cursor.clone(),
|
||||
HISTORY_ITEM_PAGE_LIMIT,
|
||||
)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
scanned_items = scanned_items.saturating_add(page.data.len());
|
||||
for entry in page.data {
|
||||
let item_lines = transcript_preview_lines_for_item(
|
||||
&entry.item,
|
||||
cwd,
|
||||
inline_visualization_context.as_ref(),
|
||||
);
|
||||
visible_lines = visible_lines.saturating_add(item_lines.len());
|
||||
if !item_lines.is_empty() {
|
||||
groups.push(item_lines);
|
||||
}
|
||||
if visible_lines >= MAX_PREVIEW_LINES {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if visible_lines >= MAX_PREVIEW_LINES || scanned_items >= HISTORY_ITEM_SCAN_LIMIT {
|
||||
break;
|
||||
}
|
||||
let Some(next_cursor) = page
|
||||
.next_cursor
|
||||
.filter(|next| seen_cursors.insert(next.clone()))
|
||||
else {
|
||||
break;
|
||||
};
|
||||
cursor = Some(next_cursor);
|
||||
}
|
||||
groups.into_iter().rev().flatten().collect::<Vec<_>>()
|
||||
} else {
|
||||
thread
|
||||
.turns
|
||||
.iter()
|
||||
.flat_map(|turn| turn.items.iter())
|
||||
.flat_map(|item| {
|
||||
transcript_preview_lines_for_item(item, cwd, inline_visualization_context.as_ref())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
if lines.len() > MAX_PREVIEW_LINES {
|
||||
lines.drain(..lines.len() - MAX_PREVIEW_LINES);
|
||||
}
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
fn transcript_preview_lines_for_item(
|
||||
item: &ThreadItem,
|
||||
cwd: &Path,
|
||||
inline_visualization_context: Option<&InlineVisualizationContext>,
|
||||
) -> Vec<TranscriptPreviewLine> {
|
||||
let line = match item {
|
||||
ThreadItem::UserMessage { content, .. } => TranscriptPreviewLine {
|
||||
speaker: TranscriptPreviewSpeaker::User,
|
||||
text: content
|
||||
.iter()
|
||||
.filter_map(|input| match input {
|
||||
codex_app_server_protocol::UserInput::Text { text, .. } => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
},
|
||||
ThreadItem::AgentMessage { text, .. } => {
|
||||
let visible_markdown = parse_assistant_markdown(text, cwd).visible_markdown;
|
||||
let rewritten = crate::inline_visualization::rewrite_inline_visualizations(
|
||||
&visible_markdown,
|
||||
inline_visualization_context,
|
||||
);
|
||||
let mut text = rewritten.markdown.into_owned();
|
||||
for (placeholder, link) in &rewritten.trusted_file_links {
|
||||
text = text.replace(
|
||||
&format!(
|
||||
"{} \n[{}]({placeholder})",
|
||||
link.markdown_label, link.markdown_destination_label
|
||||
),
|
||||
&format!("{} \n{}", link.display_label, link.destination),
|
||||
);
|
||||
}
|
||||
TranscriptPreviewLine {
|
||||
speaker: TranscriptPreviewSpeaker::Assistant,
|
||||
text,
|
||||
}
|
||||
}
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
line.text
|
||||
.lines()
|
||||
.filter(|text| !text.trim().is_empty())
|
||||
.map(|text| TranscriptPreviewLine {
|
||||
speaker: line.speaker,
|
||||
text: text.trim().to_string(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl SearchState {
|
||||
fn active_token(&self) -> Option<usize> {
|
||||
match self {
|
||||
@@ -3567,6 +3427,7 @@ fn render_empty_state_line(state: &PickerState) -> Line<'static> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::Duration;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadSourceKind;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_protocol::ThreadId;
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use super::TranscriptPreviewLine;
|
||||
use super::TranscriptPreviewSpeaker;
|
||||
use crate::app_server_session::AppServerSession;
|
||||
use crate::app_server_session::HISTORY_ITEM_PAGE_LIMIT;
|
||||
use crate::app_server_session::HISTORY_ITEM_SCAN_LIMIT;
|
||||
use crate::app_server_session::HistoryHydrationScope;
|
||||
use crate::git_action_directives::parse_assistant_markdown;
|
||||
use crate::inline_visualization::InlineVisualizationContext;
|
||||
use crate::legacy_core::config::Config;
|
||||
use codex_app_server_protocol::ThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_protocol::ThreadId;
|
||||
|
||||
const MAX_TRANSCRIPT_PREVIEW_LINES: usize = 6;
|
||||
const TRANSCRIPT_PREVIEW_ITEMS_PAGE_SIZE: u32 = 6;
|
||||
|
||||
/// Loads the newest nonblank transcript lines within a bounded paginated-item scan.
|
||||
pub(crate) async fn load_transcript_preview(
|
||||
app_server: &mut AppServerSession,
|
||||
thread_id: ThreadId,
|
||||
config: Option<&Config>,
|
||||
) -> std::io::Result<Vec<TranscriptPreviewLine>> {
|
||||
let mut thread = app_server
|
||||
.thread_read(thread_id, /*include_turns*/ false)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
if thread.history_mode == ThreadHistoryMode::Legacy {
|
||||
app_server
|
||||
.hydrate_initial_thread_history(
|
||||
&mut thread,
|
||||
/*turn_cursor*/ None,
|
||||
/*item_cursor*/ None,
|
||||
/*config*/ None,
|
||||
HistoryHydrationScope::Initial,
|
||||
)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
}
|
||||
let cwd = thread.cwd.as_path();
|
||||
let inline_visualization_context = config.and_then(|config| {
|
||||
ThreadId::from_string(&thread.id)
|
||||
.ok()
|
||||
.and_then(|thread_id| InlineVisualizationContext::from_config(config, thread_id))
|
||||
});
|
||||
let mut lines = Vec::with_capacity(MAX_TRANSCRIPT_PREVIEW_LINES);
|
||||
match thread.history_mode {
|
||||
ThreadHistoryMode::Legacy => {
|
||||
append_transcript_preview_lines(
|
||||
&mut lines,
|
||||
thread
|
||||
.turns
|
||||
.iter()
|
||||
.rev()
|
||||
.flat_map(|turn| turn.items.iter().rev()),
|
||||
cwd,
|
||||
inline_visualization_context.as_ref(),
|
||||
);
|
||||
}
|
||||
ThreadHistoryMode::Paginated => {
|
||||
let mut cursor = None;
|
||||
let mut seen_cursors = HashSet::new();
|
||||
let mut scanned_items = 0_usize;
|
||||
loop {
|
||||
let remaining_items = HISTORY_ITEM_SCAN_LIMIT.saturating_sub(scanned_items);
|
||||
let page_size = if cursor.is_none() {
|
||||
TRANSCRIPT_PREVIEW_ITEMS_PAGE_SIZE
|
||||
} else {
|
||||
HISTORY_ITEM_PAGE_LIMIT
|
||||
}
|
||||
.min(remaining_items as u32);
|
||||
if page_size == 0 {
|
||||
break;
|
||||
}
|
||||
let page = app_server
|
||||
.thread_items_page(thread_id, /*turn_id*/ None, cursor.clone(), page_size)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
scanned_items = scanned_items.saturating_add(page.data.len());
|
||||
append_transcript_preview_lines(
|
||||
&mut lines,
|
||||
page.data
|
||||
.iter()
|
||||
.take(remaining_items)
|
||||
.map(|entry| &entry.item),
|
||||
cwd,
|
||||
inline_visualization_context.as_ref(),
|
||||
);
|
||||
if lines.len() == MAX_TRANSCRIPT_PREVIEW_LINES
|
||||
|| scanned_items >= HISTORY_ITEM_SCAN_LIMIT
|
||||
{
|
||||
break;
|
||||
}
|
||||
let Some(next_cursor) = page
|
||||
.next_cursor
|
||||
.filter(|next| seen_cursors.insert(next.clone()))
|
||||
else {
|
||||
break;
|
||||
};
|
||||
cursor = Some(next_cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines.reverse();
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
/// Appends the newest preview lines from items already ordered newest-first.
|
||||
fn append_transcript_preview_lines<'a>(
|
||||
lines: &mut Vec<TranscriptPreviewLine>,
|
||||
items: impl Iterator<Item = &'a ThreadItem>,
|
||||
cwd: &Path,
|
||||
inline_visualization_context: Option<&InlineVisualizationContext>,
|
||||
) {
|
||||
for item in items {
|
||||
match item {
|
||||
ThreadItem::UserMessage { content, .. } => {
|
||||
let text = content
|
||||
.iter()
|
||||
.filter_map(|input| match input {
|
||||
codex_app_server_protocol::UserInput::Text { text, .. } => {
|
||||
Some(text.as_str())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
append_transcript_preview_text(
|
||||
lines,
|
||||
TranscriptPreviewSpeaker::User,
|
||||
&text,
|
||||
cwd,
|
||||
inline_visualization_context,
|
||||
);
|
||||
}
|
||||
ThreadItem::AgentMessage { text, .. } => {
|
||||
append_transcript_preview_text(
|
||||
lines,
|
||||
TranscriptPreviewSpeaker::Assistant,
|
||||
text,
|
||||
cwd,
|
||||
inline_visualization_context,
|
||||
);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
if lines.len() == MAX_TRANSCRIPT_PREVIEW_LINES {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends a message's newest nonblank lines while preserving assistant display rewrites.
|
||||
fn append_transcript_preview_text(
|
||||
lines: &mut Vec<TranscriptPreviewLine>,
|
||||
speaker: TranscriptPreviewSpeaker,
|
||||
text: &str,
|
||||
cwd: &Path,
|
||||
inline_visualization_context: Option<&InlineVisualizationContext>,
|
||||
) {
|
||||
let visible_markdown;
|
||||
let text = match speaker {
|
||||
TranscriptPreviewSpeaker::User => Cow::Borrowed(text),
|
||||
TranscriptPreviewSpeaker::Assistant => {
|
||||
visible_markdown = parse_assistant_markdown(text, cwd).visible_markdown;
|
||||
let rewritten = crate::inline_visualization::rewrite_inline_visualizations(
|
||||
&visible_markdown,
|
||||
inline_visualization_context,
|
||||
);
|
||||
let mut text = rewritten.markdown;
|
||||
for (placeholder, link) in &rewritten.trusted_file_links {
|
||||
text = Cow::Owned(text.replace(
|
||||
&format!(
|
||||
"{} \n[{}]({placeholder})",
|
||||
link.markdown_label, link.markdown_destination_label
|
||||
),
|
||||
&format!("{} \n{}", link.display_label, link.destination),
|
||||
));
|
||||
}
|
||||
text
|
||||
}
|
||||
};
|
||||
|
||||
let remaining = MAX_TRANSCRIPT_PREVIEW_LINES - lines.len();
|
||||
lines.extend(
|
||||
text.lines()
|
||||
.rev()
|
||||
.map(str::trim)
|
||||
.filter(|text| !text.is_empty())
|
||||
.take(remaining)
|
||||
.map(|text| TranscriptPreviewLine {
|
||||
speaker,
|
||||
text: text.to_string(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "resume_picker_transcript_preview_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,230 @@
|
||||
use super::*;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::write_mock_responses_config_toml;
|
||||
use codex_app_server_client::AppServerEvent;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::SortDirection;
|
||||
use codex_app_server_protocol::ThreadItemsListParams;
|
||||
use codex_app_server_protocol::ThreadItemsListResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use core_test_support::responses;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn preview_line(speaker: TranscriptPreviewSpeaker, text: &str) -> TranscriptPreviewLine {
|
||||
TranscriptPreviewLine {
|
||||
speaker,
|
||||
text: text.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcript_preview_reverse_scan_stops_before_older_items() {
|
||||
let newest = ThreadItem::AgentMessage {
|
||||
id: String::from("final"),
|
||||
text: String::from("one\ntwo\nthree\nfour\nfive\nsix"),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
};
|
||||
let mut lines = Vec::new();
|
||||
|
||||
append_transcript_preview_lines(
|
||||
&mut lines,
|
||||
std::iter::once(&newest).chain(std::iter::from_fn(|| {
|
||||
panic!("preview should stop after finding six lines")
|
||||
})),
|
||||
Path::new("/tmp"),
|
||||
/*inline_visualization_context*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "six"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "five"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "four"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "three"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "two"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "one"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async fn transcript_preview_for_history_mode(
|
||||
history_mode: ThreadHistoryMode,
|
||||
invisible_newer_turns: usize,
|
||||
) -> Vec<TranscriptPreviewLine> {
|
||||
let model_response = responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_assistant_message("commentary", "commentary one\ncommentary two"),
|
||||
responses::ev_assistant_message("final", "final one\nfinal two\nfinal three"),
|
||||
responses::ev_completed("resp-1"),
|
||||
]);
|
||||
let model_responses = std::iter::once(model_response)
|
||||
.chain((0..invisible_newer_turns).map(|index| {
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created(&format!("empty-resp-{index}")),
|
||||
responses::ev_completed(&format!("empty-resp-{index}")),
|
||||
])
|
||||
}))
|
||||
.collect();
|
||||
let server = create_mock_responses_server_sequence(model_responses).await;
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
write_mock_responses_config_toml(
|
||||
codex_home.path(),
|
||||
&server.uri(),
|
||||
&Default::default(),
|
||||
/*auto_compact_limit*/ 100_000,
|
||||
/*requires_openai_auth*/ None,
|
||||
"mock_provider",
|
||||
"compact",
|
||||
)
|
||||
.expect("write mock config");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.build()
|
||||
.await
|
||||
.expect("build config");
|
||||
let mut app_server = crate::start_embedded_app_server_for_picker(&config)
|
||||
.await
|
||||
.expect("start app server");
|
||||
let request_handle = app_server.request_handle();
|
||||
let started: ThreadStartResponse = request_handle
|
||||
.request_typed(ClientRequest::ThreadStart {
|
||||
request_id: RequestId::String(String::from("preview-thread-start")),
|
||||
params: ThreadStartParams {
|
||||
model: Some(String::from("mock-model")),
|
||||
model_provider: Some(String::from("mock_provider")),
|
||||
cwd: Some(codex_home.path().display().to_string()),
|
||||
history_mode: Some(history_mode),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await
|
||||
.expect("start thread");
|
||||
let thread_id = ThreadId::from_string(&started.thread.id).expect("thread id");
|
||||
for index in 0..=invisible_newer_turns {
|
||||
let input = if index == 0 {
|
||||
UserInput::Text {
|
||||
text: String::from("recent user"),
|
||||
text_elements: Vec::new(),
|
||||
}
|
||||
} else {
|
||||
UserInput::Image {
|
||||
url: String::from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==",
|
||||
),
|
||||
detail: None,
|
||||
}
|
||||
};
|
||||
let _: TurnStartResponse = request_handle
|
||||
.request_typed(ClientRequest::TurnStart {
|
||||
request_id: RequestId::String(format!("preview-turn-start-{index}")),
|
||||
params: TurnStartParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
input: vec![input],
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await
|
||||
.expect("start turn");
|
||||
tokio::time::timeout(std::time::Duration::from_secs(10), async {
|
||||
loop {
|
||||
let Some(event) = app_server.next_event().await else {
|
||||
panic!("app-server event stream closed before turn completed");
|
||||
};
|
||||
if matches!(
|
||||
event,
|
||||
AppServerEvent::ServerNotification(notification)
|
||||
if matches!(notification.as_ref(), ServerNotification::TurnCompleted(_))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("turn should complete");
|
||||
}
|
||||
|
||||
if history_mode == ThreadHistoryMode::Paginated {
|
||||
let first_page: ThreadItemsListResponse = request_handle
|
||||
.request_typed(ClientRequest::ThreadItemsList {
|
||||
request_id: RequestId::String(String::from("preview-items-list")),
|
||||
params: ThreadItemsListParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
turn_id: None,
|
||||
cursor: None,
|
||||
limit: Some(TRANSCRIPT_PREVIEW_ITEMS_PAGE_SIZE),
|
||||
sort_direction: Some(SortDirection::Desc),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.expect("list first items page");
|
||||
assert_eq!(first_page.data.len(), MAX_TRANSCRIPT_PREVIEW_LINES);
|
||||
let mut first_page_lines = Vec::new();
|
||||
append_transcript_preview_lines(
|
||||
&mut first_page_lines,
|
||||
first_page.data.iter().map(|entry| &entry.item),
|
||||
codex_home.path(),
|
||||
/*inline_visualization_context*/ None,
|
||||
);
|
||||
assert_eq!(first_page_lines, Vec::new());
|
||||
assert!(first_page.next_cursor.is_some());
|
||||
}
|
||||
|
||||
load_transcript_preview(&mut app_server, thread_id, Some(&config))
|
||||
.await
|
||||
.expect("load transcript preview")
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn transcript_preview_preserves_legacy_and_paginated_output() {
|
||||
let invisible_newer_turns = TRANSCRIPT_PREVIEW_ITEMS_PAGE_SIZE as usize;
|
||||
let legacy =
|
||||
transcript_preview_for_history_mode(ThreadHistoryMode::Legacy, invisible_newer_turns).await;
|
||||
let paginated =
|
||||
transcript_preview_for_history_mode(ThreadHistoryMode::Paginated, invisible_newer_turns)
|
||||
.await;
|
||||
|
||||
assert_eq!(legacy, paginated);
|
||||
assert_eq!(
|
||||
paginated,
|
||||
vec![
|
||||
preview_line(TranscriptPreviewSpeaker::User, "recent user"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "commentary one"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "commentary two"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "final one"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "final two"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "final three"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn paginated_transcript_preview_pages_past_old_item_budget() {
|
||||
let invisible_newer_turns = TRANSCRIPT_PREVIEW_ITEMS_PAGE_SIZE as usize * 4;
|
||||
|
||||
let lines =
|
||||
transcript_preview_for_history_mode(ThreadHistoryMode::Paginated, invisible_newer_turns)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
lines,
|
||||
vec![
|
||||
preview_line(TranscriptPreviewSpeaker::User, "recent user"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "commentary one"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "commentary two"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "final one"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "final two"),
|
||||
preview_line(TranscriptPreviewSpeaker::Assistant, "final three"),
|
||||
]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user