From d02582daa88f00edc9b9e71401225efc93f6bef1 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Sun, 19 Apr 2026 12:54:36 -0300 Subject: [PATCH 1/3] feat(tui): show context used in plan prompt Show context usage on the clear-context implementation option so users can see why starting fresh may help. Keep the prompt scoped to Plan mode and display used context instead of remaining context. --- codex-rs/tui/src/chatwidget.rs | 24 ++++++++++ .../tui/src/chatwidget/plan_implementation.rs | 8 +++- ...an_implementation_popup_context_usage.snap | 11 +++++ .../tui/src/chatwidget/tests/plan_mode.rs | 47 +++++++++++++++++-- 4 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_context_usage.snap diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 2c88baf85e2c..57d0b0e4c39a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2480,17 +2480,41 @@ impl ChatWidget { fn open_plan_implementation_prompt(&mut self) { let default_mask = collaboration_modes::default_mode_mask(self.model_catalog.as_ref()); + let context_usage_label = self.plan_implementation_context_usage_label(); self.bottom_pane .show_selection_view(plan_implementation::selection_view_params( default_mask, self.latest_proposed_plan_markdown.as_deref(), + context_usage_label.as_deref(), )); self.notify(Notification::PlanModePrompt { title: PLAN_IMPLEMENTATION_TITLE.to_string(), }); } + fn plan_implementation_context_usage_label(&self) -> Option { + let info = self.token_info.as_ref()?; + let percent = self.context_remaining_percent(info); + + let used_tokens = self.context_used_tokens(info, percent.is_some()); + if let Some(percent) = percent { + let used_percent = 100 - percent.clamp(0, 100); + if used_percent <= 0 { + return None; + } + return Some(format!("{used_percent}% used")); + } + + if let Some(tokens) = used_tokens + && tokens > 0 + { + return Some(format!("{} used", format_tokens_compact(tokens))); + } + + None + } + fn has_queued_follow_up_messages(&self) -> bool { !self.rejected_steers_queue.is_empty() || !self.queued_user_messages.is_empty() } diff --git a/codex-rs/tui/src/chatwidget/plan_implementation.rs b/codex-rs/tui/src/chatwidget/plan_implementation.rs index c5b253a8d199..129ed6c0304d 100644 --- a/codex-rs/tui/src/chatwidget/plan_implementation.rs +++ b/codex-rs/tui/src/chatwidget/plan_implementation.rs @@ -23,6 +23,7 @@ pub(super) const PLAN_IMPLEMENTATION_NO_APPROVED_PLAN: &str = "No approved plan pub(super) fn selection_view_params( default_mask: Option, plan_markdown: Option<&str>, + clear_context_usage_label: Option<&str>, ) -> SelectionViewParams { let (implement_actions, implement_disabled_reason) = match default_mask.clone() { Some(mask) => { @@ -63,6 +64,11 @@ pub(super) fn selection_view_params( ), }; + let clear_context_description = clear_context_usage_label.map_or_else( + || "Fresh thread with this plan.".to_string(), + |label| format!("Fresh thread. Context: {label}."), + ); + SelectionViewParams { title: Some(PLAN_IMPLEMENTATION_TITLE.to_string()), subtitle: None, @@ -80,7 +86,7 @@ pub(super) fn selection_view_params( }, SelectionItem { name: PLAN_IMPLEMENTATION_CLEAR_CONTEXT.to_string(), - description: Some("Fresh thread with this plan.".to_string()), + description: Some(clear_context_description), selected_description: None, is_current: false, actions: clear_context_actions, diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_context_usage.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_context_usage.snap new file mode 100644 index 000000000000..4b8ae0099aa5 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plan_implementation_popup_context_usage.snap @@ -0,0 +1,11 @@ +--- +source: tui/src/chatwidget/tests/plan_mode.rs +expression: popup +--- + Implement this plan? + +› 1. Yes, implement this plan Switch to Default and start coding. + 2. Yes, clear context and implement Fresh thread. Context: 89% used. + 3. No, stay in Plan mode Continue planning with the model. + + Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index fdf906db7ca6..764fb44694a3 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -11,6 +11,17 @@ async fn plan_implementation_popup_snapshot() { assert_chatwidget_snapshot!("plan_implementation_popup", popup); } +#[tokio::test] +async fn plan_implementation_popup_context_usage_snapshot() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; + chat.set_token_info(Some(make_token_info(90_000, 100_000))); + chat.on_plan_item_completed("- Step 1\n- Step 2\n".to_string()); + chat.open_plan_implementation_prompt(); + + let popup = render_bottom_popup(&chat, /*width*/ 80); + assert_chatwidget_snapshot!("plan_implementation_popup_context_usage", popup); +} + #[tokio::test] async fn plan_implementation_popup_no_selected_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; @@ -74,8 +85,11 @@ async fn plan_implementation_clear_context_requires_default_mode_and_plan() { let default_mask = collaboration_modes::default_mode_mask(chat.model_catalog.as_ref()) .expect("expected default collaboration mode"); - let params = - plan_implementation::selection_view_params(/*default_mask*/ None, Some("- Step\n")); + let params = plan_implementation::selection_view_params( + /*default_mask*/ None, + Some("- Step\n"), + /*clear_context_usage_label*/ None, + ); assert_eq!( params.items[1].disabled_reason.as_deref(), Some(plan_implementation::PLAN_IMPLEMENTATION_DEFAULT_UNAVAILABLE) @@ -84,22 +98,45 @@ async fn plan_implementation_clear_context_requires_default_mode_and_plan() { let params = plan_implementation::selection_view_params( Some(default_mask.clone()), /*plan_markdown*/ None, + /*clear_context_usage_label*/ None, ); assert_eq!( params.items[1].disabled_reason.as_deref(), Some(plan_implementation::PLAN_IMPLEMENTATION_NO_APPROVED_PLAN) ); - let params = - plan_implementation::selection_view_params(Some(default_mask.clone()), Some(" \n")); + let params = plan_implementation::selection_view_params( + Some(default_mask.clone()), + Some(" \n"), + /*clear_context_usage_label*/ None, + ); assert_eq!( params.items[1].disabled_reason.as_deref(), Some(plan_implementation::PLAN_IMPLEMENTATION_NO_APPROVED_PLAN) ); - let params = plan_implementation::selection_view_params(Some(default_mask), Some("- Step\n")); + let params = plan_implementation::selection_view_params( + Some(default_mask.clone()), + Some("- Step\n"), + /*clear_context_usage_label*/ None, + ); assert_eq!(params.items[1].disabled_reason, None); assert!(!params.items[1].actions.is_empty()); + + assert_eq!( + params.items[1].description.as_deref(), + Some("Fresh thread with this plan.") + ); + + let params = plan_implementation::selection_view_params( + Some(default_mask), + Some("- Step\n"), + Some("89% used"), + ); + assert_eq!( + params.items[1].description.as_deref(), + Some("Fresh thread. Context: 89% used.") + ); } #[tokio::test] From c9219a1bc0aad2419013bcd67bb6c4ad194bf44d Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Sun, 19 Apr 2026 13:28:43 -0300 Subject: [PATCH 2/3] docs(tui): document plan prompt context label Explain why the plan implementation prompt reports context used while ambient footer status continues to report context remaining. Document that the popup builder only owns display copy, not token accounting. --- codex-rs/tui/src/chatwidget.rs | 7 +++++++ codex-rs/tui/src/chatwidget/plan_implementation.rs | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 57d0b0e4c39a..1ddd8235ed02 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2493,6 +2493,13 @@ impl ChatWidget { }); } + /// Returns a context-used label for the plan implementation prompt. + /// + /// The footer reports context remaining because it is ambient status, but + /// this prompt is asking whether to discard prior conversation state before + /// implementing a plan. Reporting used context makes the cleanup tradeoff + /// explicit. A fully fresh or unknown context window returns no label so + /// the clear-context option does not imply urgency without evidence. fn plan_implementation_context_usage_label(&self) -> Option { let info = self.token_info.as_ref()?; let percent = self.context_remaining_percent(info); diff --git a/codex-rs/tui/src/chatwidget/plan_implementation.rs b/codex-rs/tui/src/chatwidget/plan_implementation.rs index 129ed6c0304d..dd810dc493c7 100644 --- a/codex-rs/tui/src/chatwidget/plan_implementation.rs +++ b/codex-rs/tui/src/chatwidget/plan_implementation.rs @@ -20,6 +20,11 @@ pub(super) const PLAN_IMPLEMENTATION_CLEAR_CONTEXT_PREFIX: &str = concat!( pub(super) const PLAN_IMPLEMENTATION_DEFAULT_UNAVAILABLE: &str = "Default mode unavailable"; pub(super) const PLAN_IMPLEMENTATION_NO_APPROVED_PLAN: &str = "No approved plan available"; +/// Builds the confirmation prompt shown after a plan is approved in Plan mode. +/// +/// The optional usage label is already phrased for display, such as `89% used` +/// or `123K used`. This module only decides where that label belongs in the +/// decision copy so action wiring stays separate from token accounting. pub(super) fn selection_view_params( default_mask: Option, plan_markdown: Option<&str>, From 0a18b16697b2697b6bf7994e36f643cc34cde699 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Sun, 19 Apr 2026 13:36:36 -0300 Subject: [PATCH 3/3] test(tui): annotate context usage test literals Add parameter comments to the plan implementation context usage test so anonymous numeric literals satisfy the argument comment lint. --- codex-rs/tui/src/chatwidget/tests/plan_mode.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index 764fb44694a3..757a46635fc3 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -14,7 +14,9 @@ async fn plan_implementation_popup_snapshot() { #[tokio::test] async fn plan_implementation_popup_context_usage_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; - chat.set_token_info(Some(make_token_info(90_000, 100_000))); + chat.set_token_info(Some(make_token_info( + /*total_tokens*/ 90_000, /*context_window*/ 100_000, + ))); chat.on_plan_item_completed("- Step 1\n- Step 2\n".to_string()); chat.open_plan_implementation_prompt();