Use active cell for context compaction

This commit is contained in:
Ahmed Ibrahim
2026-01-28 22:24:18 -08:00
parent ff9fa56368
commit c27e15222f
3 changed files with 186 additions and 3 deletions

View File

@@ -59,6 +59,8 @@ use codex_core::protocol::ExecCommandEndEvent;
use codex_core::protocol::ExecCommandOutputDeltaEvent;
use codex_core::protocol::ExecCommandSource;
use codex_core::protocol::ExitedReviewModeEvent;
use codex_core::protocol::ItemCompletedEvent;
use codex_core::protocol::ItemStartedEvent;
use codex_core::protocol::ListCustomPromptsResponseEvent;
use codex_core::protocol::ListSkillsResponseEvent;
use codex_core::protocol::McpListToolsResponseEvent;
@@ -101,6 +103,7 @@ use codex_protocol::config_types::Personality;
use codex_protocol::config_types::Settings;
#[cfg(target_os = "windows")]
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::items::TurnItem;
use codex_protocol::models::local_image_label_text;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::request_user_input::RequestUserInputEvent;
@@ -3055,6 +3058,8 @@ impl ChatWidget {
force_reload: true,
});
}
EventMsg::ItemStarted(ev) => self.on_item_started(ev, from_replay),
EventMsg::ItemCompleted(ev) => self.on_item_completed(ev, from_replay),
EventMsg::ShutdownComplete => self.on_shutdown_complete(),
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => self.on_turn_diff(unified_diff),
EventMsg::DeprecationNotice(ev) => self.on_deprecation_notice(ev),
@@ -3077,7 +3082,7 @@ impl ChatWidget {
self.on_entered_review_mode(review_request, from_replay)
}
EventMsg::ExitedReviewMode(review) => self.on_exited_review_mode(review),
EventMsg::ContextCompacted(_) => self.on_agent_message("Context compacted".to_owned()),
EventMsg::ContextCompacted(_) => {}
EventMsg::CollabAgentSpawnBegin(_) => {}
EventMsg::CollabAgentSpawnEnd(ev) => self.on_collab_event(collab::spawn_end(ev)),
EventMsg::CollabAgentInteractionBegin(_) => {}
@@ -3090,8 +3095,6 @@ impl ChatWidget {
EventMsg::CollabCloseEnd(ev) => self.on_collab_event(collab::close_end(ev)),
EventMsg::ThreadRolledBack(_) => {}
EventMsg::RawResponseItem(_)
| EventMsg::ItemStarted(_)
| EventMsg::ItemCompleted(_)
| EventMsg::AgentMessageContentDelta(_)
| EventMsg::ReasoningContentDelta(_)
| EventMsg::ReasoningRawContentDelta(_)
@@ -3099,6 +3102,59 @@ impl ChatWidget {
}
}
fn on_item_started(&mut self, event: ItemStartedEvent, from_replay: bool) {
if from_replay {
return;
}
let TurnItem::ContextCompaction(item) = event.item else {
return;
};
self.flush_answer_stream_with_separator();
if let Some(cell) = self.active_cell.as_mut().and_then(|cell| {
cell.as_any_mut()
.downcast_mut::<history_cell::ContextCompactionCell>()
}) && cell.item_id() == item.id
{
self.bump_active_cell_revision();
self.request_redraw();
return;
}
self.flush_active_cell();
self.active_cell = Some(Box::new(history_cell::new_active_context_compaction(
item.id,
self.config.animations,
)));
self.bump_active_cell_revision();
self.request_redraw();
}
fn on_item_completed(&mut self, event: ItemCompletedEvent, from_replay: bool) {
if from_replay {
return;
}
let TurnItem::ContextCompaction(item) = event.item else {
return;
};
self.flush_answer_stream_with_separator();
if let Some(cell) = self.active_cell.as_mut().and_then(|cell| {
cell.as_any_mut()
.downcast_mut::<history_cell::ContextCompactionCell>()
}) && cell.item_id() == item.id
{
cell.complete();
self.bump_active_cell_revision();
self.flush_active_cell();
self.request_redraw();
return;
}
self.add_to_history(history_cell::new_context_compaction_completed(item.id));
self.request_redraw();
}
fn on_entered_review_mode(&mut self, review: ReviewRequest, from_replay: bool) {
// Enter review mode and emit a concise banner
if self.pre_review_token_info.is_none() {

View File

@@ -29,6 +29,7 @@ use codex_core::protocol::AgentReasoningDeltaEvent;
use codex_core::protocol::AgentReasoningEvent;
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
use codex_core::protocol::BackgroundEventEvent;
use codex_core::protocol::ContextCompactedEvent;
use codex_core::protocol::CreditsSnapshot;
use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
@@ -39,6 +40,8 @@ use codex_core::protocol::ExecCommandSource;
use codex_core::protocol::ExecPolicyAmendment;
use codex_core::protocol::ExitedReviewModeEvent;
use codex_core::protocol::FileChange;
use codex_core::protocol::ItemCompletedEvent;
use codex_core::protocol::ItemStartedEvent;
use codex_core::protocol::McpStartupCompleteEvent;
use codex_core::protocol::McpStartupStatus;
use codex_core::protocol::McpStartupUpdateEvent;
@@ -67,6 +70,8 @@ use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::Settings;
use codex_protocol::items::ContextCompactionItem;
use codex_protocol::items::TurnItem;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_protocol::parse_command::ParsedCommand;
@@ -4480,6 +4485,58 @@ async fn warning_event_adds_warning_history_cell() {
);
}
#[tokio::test]
async fn compaction_items_drive_history_and_context_compacted_is_ignored() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
let thread_id = ThreadId::new();
let compaction_item = ContextCompactionItem::new();
let turn_id = "turn-1".to_string();
chat.handle_codex_event(Event {
id: "item-start".into(),
msg: EventMsg::ItemStarted(ItemStartedEvent {
thread_id,
turn_id: turn_id.clone(),
item: TurnItem::ContextCompaction(compaction_item.clone()),
}),
});
let started_cells = drain_insert_history(&mut rx);
assert!(
started_cells.is_empty(),
"compaction start should keep an active cell instead of flushing history"
);
chat.handle_codex_event(Event {
id: "context-compacted".into(),
msg: EventMsg::ContextCompacted(ContextCompactedEvent {}),
});
let compacted_cells = drain_insert_history(&mut rx);
assert!(
compacted_cells.is_empty(),
"ContextCompacted should not emit history cells"
);
chat.handle_codex_event(Event {
id: "item-end".into(),
msg: EventMsg::ItemCompleted(ItemCompletedEvent {
thread_id,
turn_id,
item: TurnItem::ContextCompaction(compaction_item),
}),
});
let cells = drain_insert_history(&mut rx);
assert_eq!(
cells.len(),
1,
"expected a single finalized compaction cell"
);
let completed = lines_to_single_string(&cells[0]);
assert!(
completed.contains("Context compacted."),
"compaction completion message missing: {completed}"
);
}
#[tokio::test]
async fn stream_recovery_restores_previous_status_header() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;

View File

@@ -1429,6 +1429,76 @@ pub(crate) fn new_web_search_call(
cell
}
fn context_compaction_header(completed: bool) -> &'static str {
if completed {
"Context compacted."
} else {
"Compacting context"
}
}
#[derive(Debug)]
pub(crate) struct ContextCompactionCell {
item_id: String,
start_time: Instant,
completed: bool,
animations_enabled: bool,
}
impl ContextCompactionCell {
pub(crate) fn new(item_id: String, animations_enabled: bool) -> Self {
Self {
item_id,
start_time: Instant::now(),
completed: false,
animations_enabled,
}
}
pub(crate) fn item_id(&self) -> &str {
&self.item_id
}
pub(crate) fn complete(&mut self) {
self.completed = true;
}
}
impl HistoryCell for ContextCompactionCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
let bullet = if self.completed {
Line::from(vec!["".dim()])
} else {
Line::from(vec![
spinner(Some(self.start_time), self.animations_enabled),
" ".into(),
])
};
let header = context_compaction_header(self.completed);
PrefixedWrappedHistoryCell::new(header.bold(), bullet, " ").display_lines(width)
}
fn transcript_animation_tick(&self) -> Option<u64> {
if !self.animations_enabled || self.completed {
return None;
}
Some((self.start_time.elapsed().as_millis() / 50) as u64)
}
}
pub(crate) fn new_active_context_compaction(
item_id: String,
animations_enabled: bool,
) -> ContextCompactionCell {
ContextCompactionCell::new(item_id, animations_enabled)
}
pub(crate) fn new_context_compaction_completed(item_id: String) -> ContextCompactionCell {
let mut cell = ContextCompactionCell::new(item_id, false);
cell.complete();
cell
}
/// Returns an additional history cell if an MCP tool result includes a decodable image.
///
/// This intentionally returns at most one cell: the first image in `CallToolResult.content` that