Include buffered turns when editing prompts (#37622)

## Why

Prompt editing looks up the selected user message in the thread's turns, but
new live turns may still exist only in the replay buffer.

## What changed

Reconstruct buffered turns from turn and item notifications before locating
the prompt to edit. Preserve completion metadata and avoid duplicating turns
or items already present in the snapshot.

## Testing

Extend the prompt-edit fork test to cover a selected turn supplied through the
replay buffer and verify that the source thread remains unchanged.

GitOrigin-RevId: e357098085a4def871f3df65b6721a31e07b737f
This commit is contained in:
Eric Traut
2026-08-08 21:01:19 +00:00
committed by copyberry
parent dd43a9967f
commit 266c6920d9
2 changed files with 81 additions and 1 deletions
+51 -1
View File
@@ -321,7 +321,57 @@ impl App {
.await;
let config = self.fresh_session_config();
let turns = match self.thread_event_channels.get(&thread_id) {
Some(channel) => Some(channel.store.lock().await.turns.clone()),
Some(channel) => {
let store = channel.store.lock().await;
let mut turns = store.turns.clone();
// Snapshot turns contain loaded history; newer live turns remain in
// the replay buffer and must also be visible to prompt-edit lookups.
for event in &store.buffer {
let ThreadBufferedEvent::Notification(notification) = event else {
continue;
};
match notification.as_ref() {
ServerNotification::TurnStarted(notification)
if !turns
.iter()
.any(|turn| turn.id == notification.turn.id) =>
{
turns.push(notification.turn.clone());
}
ServerNotification::ItemCompleted(notification) => {
if matches!(
notification.item,
ThreadItem::UserMessage { .. }
| ThreadItem::EnteredReviewMode { .. }
| ThreadItem::ExitedReviewMode { .. }
) && let Some(turn) = turns
.iter_mut()
.find(|turn| turn.id == notification.turn_id)
&& !turn
.items
.iter()
.any(|item| item.id() == notification.item.id())
{
turn.items.push(notification.item.clone());
}
}
ServerNotification::TurnCompleted(notification) => {
if let Some(turn) = turns
.iter_mut()
.find(|turn| turn.id == notification.turn.id)
{
turn.status = notification.turn.status.clone();
turn.error = notification.turn.error.clone();
turn.started_at = notification.turn.started_at;
turn.completed_at = notification.turn.completed_at;
turn.duration_ms = notification.turn.duration_ms;
}
}
_ => {}
}
}
Some(turns)
}
None => None,
};
let started = match turns {
+30
View File
@@ -6590,8 +6590,38 @@ async fn prompt_edit_forks_before_selected_prompt_and_preserves_source() -> Resu
crate::app_server_session::ResumeModelSettings::OverrideFromCurrentConfig,
)
.await?;
let selected_turn = started.turns[1].clone();
app.enqueue_primary_thread_session(started.session, started.turns)
.await?;
{
let mut store = app
.thread_event_channels
.get(&source_thread_id)
.expect("source thread event channel")
.store
.lock()
.await;
store.turns.pop();
store.push_notification(turn_started_notification(
source_thread_id,
&selected_turn.id,
));
for item in selected_turn.items {
store.push_notification(ServerNotification::ItemCompleted(
codex_app_server_protocol::ItemCompletedNotification {
thread_id: source_thread_id.to_string(),
turn_id: selected_turn.id.clone(),
completed_at_ms: 0,
item,
},
));
}
store.push_notification(turn_completed_notification(
source_thread_id,
&selected_turn.id,
TurnStatus::Interrupted,
));
}
while app_event_rx.try_recv().is_ok() {}
let source_before = std::fs::read_to_string(&source_path)?;
let mut tui = crate::tui::test_support::make_test_tui()?;