Skip to content

Commit 692989c

Browse files
authored
fix(context left after review): review footer context after /review (#5610)
## Summary - show live review token usage while `/review` runs and restore the main session indicator afterward - add regression coverage for the footer behavior ## Testing - just fmt - cargo test -p codex-tui Fixes #5604 --------- Signed-off-by: Fahad <[email protected]>
1 parent 2fde03b commit 692989c

File tree

3 files changed

+138
-8
lines changed

3 files changed

+138
-8
lines changed

codex-rs/tui/src/bottom_pane/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ impl BottomPane {
114114
self.status.as_ref()
115115
}
116116

117+
#[cfg(test)]
118+
pub(crate) fn context_window_percent(&self) -> Option<i64> {
119+
self.context_window_percent
120+
}
121+
117122
fn active_view(&self) -> Option<&dyn BottomPaneView> {
118123
self.view_stack.last().map(std::convert::AsRef::as_ref)
119124
}

codex-rs/tui/src/chatwidget.rs

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,8 @@ pub(crate) struct ChatWidget {
290290
pending_notification: Option<Notification>,
291291
// Simple review mode flag; used to adjust layout and banners.
292292
is_review_mode: bool,
293+
// Snapshot of token usage to restore after review mode exits.
294+
pre_review_token_info: Option<Option<TokenUsageInfo>>,
293295
// Whether to add a final message separator after the last message
294296
needs_final_message_separator: bool,
295297

@@ -489,16 +491,39 @@ impl ChatWidget {
489491
}
490492

491493
pub(crate) fn set_token_info(&mut self, info: Option<TokenUsageInfo>) {
492-
if let Some(info) = info {
493-
let context_window = info
494-
.model_context_window
495-
.or(self.config.model_context_window);
496-
let percent = context_window.map(|window| {
494+
match info {
495+
Some(info) => self.apply_token_info(info),
496+
None => {
497+
self.bottom_pane.set_context_window_percent(None);
498+
self.token_info = None;
499+
}
500+
}
501+
}
502+
503+
fn apply_token_info(&mut self, info: TokenUsageInfo) {
504+
let percent = self.context_remaining_percent(&info);
505+
self.bottom_pane.set_context_window_percent(percent);
506+
self.token_info = Some(info);
507+
}
508+
509+
fn context_remaining_percent(&self, info: &TokenUsageInfo) -> Option<i64> {
510+
info.model_context_window
511+
.or(self.config.model_context_window)
512+
.map(|window| {
497513
info.last_token_usage
498514
.percent_of_context_window_remaining(window)
499-
});
500-
self.bottom_pane.set_context_window_percent(percent);
501-
self.token_info = Some(info);
515+
})
516+
}
517+
518+
fn restore_pre_review_token_info(&mut self) {
519+
if let Some(saved) = self.pre_review_token_info.take() {
520+
match saved {
521+
Some(info) => self.apply_token_info(info),
522+
None => {
523+
self.bottom_pane.set_context_window_percent(None);
524+
self.token_info = None;
525+
}
526+
}
502527
}
503528
}
504529

@@ -1150,6 +1175,7 @@ impl ChatWidget {
11501175
suppress_session_configured_redraw: false,
11511176
pending_notification: None,
11521177
is_review_mode: false,
1178+
pre_review_token_info: None,
11531179
needs_final_message_separator: false,
11541180
last_rendered_width: std::cell::Cell::new(None),
11551181
feedback,
@@ -1223,6 +1249,7 @@ impl ChatWidget {
12231249
suppress_session_configured_redraw: true,
12241250
pending_notification: None,
12251251
is_review_mode: false,
1252+
pre_review_token_info: None,
12261253
needs_final_message_separator: false,
12271254
last_rendered_width: std::cell::Cell::new(None),
12281255
feedback,
@@ -1693,6 +1720,9 @@ impl ChatWidget {
16931720

16941721
fn on_entered_review_mode(&mut self, review: ReviewRequest) {
16951722
// Enter review mode and emit a concise banner
1723+
if self.pre_review_token_info.is_none() {
1724+
self.pre_review_token_info = Some(self.token_info.clone());
1725+
}
16961726
self.is_review_mode = true;
16971727
let banner = format!(">> Code review started: {} <<", review.user_facing_hint);
16981728
self.add_to_history(history_cell::new_review_status_line(banner));
@@ -1733,6 +1763,7 @@ impl ChatWidget {
17331763
}
17341764

17351765
self.is_review_mode = false;
1766+
self.restore_pre_review_token_info();
17361767
// Append a finishing banner at the end of this turn.
17371768
self.add_to_history(history_cell::new_review_status_line(
17381769
"<< Code review finished >>".to_string(),

codex-rs/tui/src/chatwidget/tests.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ use codex_core::protocol::ReviewRequest;
3838
use codex_core::protocol::StreamErrorEvent;
3939
use codex_core::protocol::TaskCompleteEvent;
4040
use codex_core::protocol::TaskStartedEvent;
41+
use codex_core::protocol::TokenCountEvent;
42+
use codex_core::protocol::TokenUsage;
43+
use codex_core::protocol::TokenUsageInfo;
4144
use codex_core::protocol::UndoCompletedEvent;
4245
use codex_core::protocol::UndoStartedEvent;
4346
use codex_core::protocol::ViewImageToolCallEvent;
@@ -215,6 +218,81 @@ fn exited_review_mode_emits_results_and_finishes() {
215218
assert!(!chat.is_review_mode);
216219
}
217220

221+
/// Exiting review restores the pre-review context window indicator.
222+
#[test]
223+
fn review_restores_context_window_indicator() {
224+
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
225+
226+
let context_window = 13_000;
227+
let pre_review_tokens = 12_700; // ~30% remaining after subtracting baseline.
228+
let review_tokens = 12_030; // ~97% remaining after subtracting baseline.
229+
230+
chat.handle_codex_event(Event {
231+
id: "token-before".into(),
232+
msg: EventMsg::TokenCount(TokenCountEvent {
233+
info: Some(make_token_info(pre_review_tokens, context_window)),
234+
rate_limits: None,
235+
}),
236+
});
237+
assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));
238+
239+
chat.handle_codex_event(Event {
240+
id: "review-start".into(),
241+
msg: EventMsg::EnteredReviewMode(ReviewRequest {
242+
prompt: "Review the latest changes".to_string(),
243+
user_facing_hint: "feature branch".to_string(),
244+
append_to_original_thread: true,
245+
}),
246+
});
247+
248+
chat.handle_codex_event(Event {
249+
id: "token-review".into(),
250+
msg: EventMsg::TokenCount(TokenCountEvent {
251+
info: Some(make_token_info(review_tokens, context_window)),
252+
rate_limits: None,
253+
}),
254+
});
255+
assert_eq!(chat.bottom_pane.context_window_percent(), Some(97));
256+
257+
chat.handle_codex_event(Event {
258+
id: "review-end".into(),
259+
msg: EventMsg::ExitedReviewMode(ExitedReviewModeEvent {
260+
review_output: None,
261+
}),
262+
});
263+
let _ = drain_insert_history(&mut rx);
264+
265+
assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));
266+
assert!(!chat.is_review_mode);
267+
}
268+
269+
/// Receiving a TokenCount event without usage clears the context indicator.
270+
#[test]
271+
fn token_count_none_resets_context_indicator() {
272+
let (mut chat, _rx, _ops) = make_chatwidget_manual();
273+
274+
let context_window = 13_000;
275+
let pre_compact_tokens = 12_700;
276+
277+
chat.handle_codex_event(Event {
278+
id: "token-before".into(),
279+
msg: EventMsg::TokenCount(TokenCountEvent {
280+
info: Some(make_token_info(pre_compact_tokens, context_window)),
281+
rate_limits: None,
282+
}),
283+
});
284+
assert_eq!(chat.bottom_pane.context_window_percent(), Some(30));
285+
286+
chat.handle_codex_event(Event {
287+
id: "token-cleared".into(),
288+
msg: EventMsg::TokenCount(TokenCountEvent {
289+
info: None,
290+
rate_limits: None,
291+
}),
292+
});
293+
assert_eq!(chat.bottom_pane.context_window_percent(), None);
294+
}
295+
218296
#[cfg_attr(
219297
target_os = "macos",
220298
ignore = "system configuration APIs are blocked under macOS seatbelt"
@@ -292,6 +370,7 @@ fn make_chatwidget_manual() -> (
292370
suppress_session_configured_redraw: false,
293371
pending_notification: None,
294372
is_review_mode: false,
373+
pre_review_token_info: None,
295374
needs_final_message_separator: false,
296375
last_rendered_width: std::cell::Cell::new(None),
297376
feedback: codex_feedback::CodexFeedback::new(),
@@ -338,6 +417,21 @@ fn lines_to_single_string(lines: &[ratatui::text::Line<'static>]) -> String {
338417
s
339418
}
340419

420+
fn make_token_info(total_tokens: i64, context_window: i64) -> TokenUsageInfo {
421+
fn usage(total_tokens: i64) -> TokenUsage {
422+
TokenUsage {
423+
total_tokens,
424+
..TokenUsage::default()
425+
}
426+
}
427+
428+
TokenUsageInfo {
429+
total_token_usage: usage(total_tokens),
430+
last_token_usage: usage(total_tokens),
431+
model_context_window: Some(context_window),
432+
}
433+
}
434+
341435
#[test]
342436
fn rate_limit_warnings_emit_thresholds() {
343437
let mut state = RateLimitWarningState::default();

0 commit comments

Comments
 (0)