mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(tui): show context used in plan implementation prompt (#18573)
# Summary When a user finishes planning, the TUI asks whether to implement in the current conversation or start fresh with the approved plan. The clear-context choice is easier to evaluate when the prompt shows how much context has already been used, because the user can see when carrying the full prior conversation is likely to be less useful than preserving only the plan. <img width="1612" height="1312" alt="image" src="https://github.com/user-attachments/assets/694bcf87-8be5-4e88-a412-e562af62d5f7" /> This PR adds that context signal directly to the clear-context option while keeping the copy compact enough for the Plan-mode selection popup. # What Changed - Compute an optional context-usage label when opening the plan implementation prompt. - Show the label only on `Yes, clear context and implement`, where it informs the cleanup decision. - Prefer a percentage-used label when context-window information is available, with a compact token-used fallback when only token totals are known. - Preserve the original option description when usage is unknown or effectively zero. - Add rustdoc comments around the prompt-copy boundary so future changes keep the context label formatting and selection rendering responsibilities clear. # Testing - `cargo test -p codex-tui plan_implementation` # Notes The footer continues to show context remaining as ambient status. The implementation prompt intentionally shows context used because the user is choosing whether to clean up the current thread before implementation.
This commit is contained in:
committed by
GitHub
Unverified
parent
996aa23e4c
commit
241136b0e9
@@ -2480,17 +2480,48 @@ 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(),
|
||||
});
|
||||
}
|
||||
|
||||
/// 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<String> {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -20,9 +20,15 @@ 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<CollaborationModeMask>,
|
||||
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 +69,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 +91,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,
|
||||
|
||||
+11
@@ -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
|
||||
@@ -11,6 +11,19 @@ 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(
|
||||
/*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();
|
||||
|
||||
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 +87,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 +100,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]
|
||||
|
||||
Reference in New Issue
Block a user