Show live context compaction status in the TUI (#42319)

## Why

Context compaction was only reported after it completed, so the TUI did not
show when compaction was actively making room to continue.

## What changed

- Show `Compacting context` as soon as manual or server-driven compaction
  starts, with an elapsed timer independent of the current turn timer.
- Preserve the compaction status across follow-up input and transient activity,
  while still allowing retry errors to take precedence.
- Restore in-progress compaction timing from thread snapshots and add the live
  duration to the completion message.
- Clear stale compaction state when the turn starts, completes, or terminates.

## Testing

Added TUI tests covering manual and server-driven compaction, narrow rendering,
follow-up input, replay and task switching, retry status, timer preservation,
and turn termination.

GitOrigin-RevId: 804e1bc553315da24ff1f99462e8e282818cada3
This commit is contained in:
Eric Traut authored and copyberry committed 2026-09-02 18:03:55 +00:00
1 parent a94a5db629
commit a526f54b00
17 files changed
+392 -8

No files matched your search

+18 -3
View File
@@ -70,7 +70,7 @@ impl ChatWidget {
self.handle_turn_completed_notification(notification, replay_kind);
}
ServerNotification::ItemStarted(notification) => {
self.handle_item_started_notification(notification, replay_kind.is_some());
self.handle_item_started_notification(notification, replay_kind);
}
ServerNotification::ItemCompleted(notification) => {
self.handle_item_completed_notification(notification, replay_kind);
@@ -352,9 +352,24 @@ impl ChatWidget {
fn handle_item_started_notification(
&mut self,
notification: ItemStartedNotification,
from_replay: bool,
replay_kind: Option<ReplayKind>,
) {
match notification.item {
ThreadItem::ContextCompaction { id }
if !matches!(replay_kind, Some(ReplayKind::ResumeInitialMessages)) =>
{
// Buffered starts reconstruct an in-flight compaction when switching tasks.
let elapsed = if replay_kind == Some(ReplayKind::ThreadSnapshot) {
let elapsed_ms = chrono::Utc::now()
.timestamp_millis()
.saturating_sub(notification.started_at_ms)
.max(0);
Duration::from_millis(elapsed_ms as u64)
} else {
Duration::ZERO
};
self.on_context_compaction_started(id, elapsed);
}
item @ ThreadItem::CommandExecution { .. } => self.on_command_execution_started(item),
ThreadItem::FileChange { id: _, changes, .. } => {
self.on_patch_apply_begin(file_update_changes_to_display(changes));
@@ -387,7 +402,7 @@ impl ChatWidget {
reasoning_effort,
agents_states,
}),
ThreadItem::EnteredReviewMode { review, .. } if !from_replay => {
ThreadItem::EnteredReviewMode { review, .. } if replay_kind.is_none() => {
self.enter_review_mode_with_hint(review, /*from_replay*/ false);
}
_ => {}