Compare commits

...

6 Commits

Author SHA1 Message Date
Ahmed Ibrahim
a91722a94f fix 2026-01-30 13:07:39 -08:00
Ahmed Ibrahim
4bad0ac715 Merge branch 'main' of https://github.com/openai/codex into tui-compaction-active-cell 2026-01-30 12:53:50 -08:00
Ahmed Ibrahim
c350201329 Merge branch 'main' into tui-compaction-active-cell 2026-01-30 12:07:55 -08:00
Ahmed Ibrahim
36c3330d9b Match TurnItem exhaustively for compaction 2026-01-28 22:50:18 -08:00
Ahmed Ibrahim
67cb0d5b0e Add context compaction snapshots 2026-01-28 22:28:22 -08:00
Ahmed Ibrahim
c27e15222f Use active cell for context compaction 2026-01-28 22:24:18 -08:00
5 changed files with 227 additions and 7 deletions

View File

@@ -61,6 +61,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;
@@ -103,6 +105,8 @@ 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::ContextCompactionItem;
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;
@@ -3415,6 +3419,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),
@@ -3437,7 +3443,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(_) => {}
@@ -3450,19 +3456,80 @@ impl ChatWidget {
EventMsg::CollabCloseEnd(ev) => self.on_collab_event(collab::close_end(ev)),
EventMsg::ThreadRolledBack(_) => {}
EventMsg::RawResponseItem(_)
| EventMsg::ItemStarted(_)
| EventMsg::AgentMessageContentDelta(_)
| EventMsg::ReasoningContentDelta(_)
| EventMsg::ReasoningRawContentDelta(_)
| EventMsg::DynamicToolCallRequest(_) => {}
EventMsg::ItemCompleted(event) => {
if let codex_protocol::items::TurnItem::Plan(plan_item) = event.item {
self.on_plan_item_completed(plan_item.text);
}
}
}
}
fn on_item_started(&mut self, event: ItemStartedEvent, from_replay: bool) {
if from_replay {
return;
}
match event.item {
TurnItem::ContextCompaction(item) => self.on_context_compaction_started(item),
TurnItem::UserMessage(_)
| TurnItem::AgentMessage(_)
| TurnItem::Reasoning(_)
| TurnItem::Plan(_)
| TurnItem::WebSearch(_) => {}
};
}
fn on_item_completed(&mut self, event: ItemCompletedEvent, from_replay: bool) {
if from_replay {
return;
}
match event.item {
TurnItem::ContextCompaction(item) => self.on_context_compaction_completed(item),
TurnItem::Plan(plan_item) => self.on_plan_item_completed(plan_item.text),
TurnItem::UserMessage(_)
| TurnItem::AgentMessage(_)
| TurnItem::Reasoning(_)
| TurnItem::WebSearch(_) => {}
};
}
fn on_context_compaction_started(&mut self, item: ContextCompactionItem) {
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_context_compaction_completed(&mut self, item: ContextCompactionItem) {
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

@@ -30,6 +30,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;
@@ -40,6 +41,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;
@@ -68,6 +71,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;
@@ -4580,6 +4585,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

@@ -1465,6 +1465,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
@@ -2352,6 +2422,22 @@ mod tests {
insta::assert_snapshot!(rendered);
}
#[test]
fn context_compaction_active_snapshot() {
let cell = new_active_context_compaction("compaction-1".to_string(), false);
let rendered = render_lines(&cell.display_lines(64)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn context_compaction_completed_snapshot() {
let cell = new_context_compaction_completed("compaction-1".to_string());
let rendered = render_lines(&cell.display_lines(64)).join("\n");
insta::assert_snapshot!(rendered);
}
#[test]
fn active_mcp_tool_call_snapshot() {
let invocation = McpInvocation {

View File

@@ -0,0 +1,5 @@
---
source: tui/src/history_cell.rs
expression: rendered
---
• Compacting context

View File

@@ -0,0 +1,5 @@
---
source: tui/src/history_cell.rs
expression: rendered
---
• Context compacted.