mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Remove the Plan mode composer nudge (#40200)
## What changed - Keep the normal composer footer visible when a draft contains the word `plan`. - Remove the contextual “Create a plan?” prompt, its Escape dismissal behavior, and its per-thread dismissal state. ## Testing - Update wide and narrow composer snapshots to cover the standard footer for plan-related drafts. GitOrigin-RevId: 1a3c8cfce9d4effa2149790975133f652aa7c914
This commit is contained in:
@@ -556,19 +556,6 @@ pub(crate) struct ComposerDraftSnapshot {
|
||||
|
||||
const FOOTER_SPACING_HEIGHT: u16 = 0;
|
||||
|
||||
/// Builds the one-line nudge that replaces the ambient footer without adding layout height.
|
||||
fn plan_mode_nudge_line() -> Line<'static> {
|
||||
Line::from(vec![
|
||||
"Create a plan?".magenta(),
|
||||
" ".into(),
|
||||
key_hint::shift(KeyCode::Tab).into(),
|
||||
" use Plan mode".into(),
|
||||
" ".into(),
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
" dismiss".into(),
|
||||
])
|
||||
}
|
||||
|
||||
impl ChatComposer {
|
||||
fn slash_input(&self) -> SlashInput<'_> {
|
||||
SlashInput::new(
|
||||
@@ -639,7 +626,6 @@ impl ChatComposer {
|
||||
use_shift_enter_hint,
|
||||
mode: FooterMode::ComposerEmpty,
|
||||
hint_override: None,
|
||||
plan_mode_nudge_visible: false,
|
||||
flash: None,
|
||||
context_window_percent: None,
|
||||
context_window_used_tokens: None,
|
||||
@@ -746,7 +732,6 @@ impl ChatComposer {
|
||||
self.effort_ignition = Some(EffortIgnition::new(tier, style));
|
||||
self.effort_animation_style = Some(style);
|
||||
if self.footer.status_line_enabled
|
||||
&& !self.footer.plan_mode_nudge_visible
|
||||
&& let Some(previous) = passive_footer_status_line(&self.footer_props())
|
||||
{
|
||||
self.effort_status_line_transition =
|
||||
@@ -1440,23 +1425,6 @@ impl ChatComposer {
|
||||
self.footer.hint_override = items;
|
||||
}
|
||||
|
||||
/// Updates whether the Plan-mode nudge replaces the ambient footer row.
|
||||
///
|
||||
/// Returns `true` only when the rendered footer can change so callers can avoid scheduling
|
||||
/// redundant redraws while reevaluating nudge policy on routine composer updates.
|
||||
pub(crate) fn set_plan_mode_nudge_visible(&mut self, visible: bool) -> bool {
|
||||
if self.footer.plan_mode_nudge_visible == visible {
|
||||
return false;
|
||||
}
|
||||
self.footer.plan_mode_nudge_visible = visible;
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn plan_mode_nudge_visible(&self) -> bool {
|
||||
self.footer.plan_mode_nudge_visible
|
||||
}
|
||||
|
||||
pub(crate) fn set_remote_image_urls(&mut self, urls: Vec<String>) {
|
||||
self.attachments
|
||||
.set_remote_image_urls(urls, &mut self.draft.textarea);
|
||||
@@ -4275,7 +4243,6 @@ impl ChatComposer {
|
||||
self.footer.quit_shortcut_expires_at = None;
|
||||
self.footer.mode = FooterMode::ComposerEmpty;
|
||||
self.footer.hint_override = Some(Vec::new());
|
||||
self.footer.plan_mode_nudge_visible = false;
|
||||
self.footer.flash = None;
|
||||
}
|
||||
|
||||
@@ -4596,17 +4563,6 @@ impl ChatComposer {
|
||||
};
|
||||
if let Some(line) = self.history_search_footer_line() {
|
||||
render_footer_line(hint_rect, buf, line);
|
||||
} else if self.footer.plan_mode_nudge_visible {
|
||||
let available_width =
|
||||
hint_rect.width.saturating_sub(FOOTER_INDENT_COLS as u16) as usize;
|
||||
render_footer_line(
|
||||
hint_rect,
|
||||
buf,
|
||||
truncate_line_with_ellipsis_if_overflow(
|
||||
plan_mode_nudge_line(),
|
||||
available_width,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
let available_width =
|
||||
hint_rect.width.saturating_sub(FOOTER_INDENT_COLS as u16) as usize;
|
||||
|
||||
@@ -19,7 +19,6 @@ pub(super) struct FooterState {
|
||||
pub(super) use_shift_enter_hint: bool,
|
||||
pub(super) mode: FooterMode,
|
||||
pub(super) hint_override: Option<Vec<(String, String)>>,
|
||||
pub(super) plan_mode_nudge_visible: bool,
|
||||
pub(super) flash: Option<FooterFlash>,
|
||||
pub(super) context_window_percent: Option<i64>,
|
||||
pub(super) context_window_used_tokens: Option<i64>,
|
||||
|
||||
@@ -59,25 +59,6 @@ fn effort_transition_does_not_queue_a_missing_outgoing_status_line() {
|
||||
assert!(composer.effort_status_line_transition.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effort_transition_does_not_queue_while_plan_mode_nudge_is_visible() {
|
||||
let (mut composer, _rx) = new_test_composer();
|
||||
composer.set_status_line_enabled(/*enabled*/ true);
|
||||
composer.set_status_line(Some(Line::from("gpt-5.4 high · main")));
|
||||
composer.set_plan_mode_nudge_visible(/*visible*/ true);
|
||||
composer.set_active_reasoning_effort(
|
||||
Some(&ReasoningEffort::High),
|
||||
/*animations_enabled*/ true,
|
||||
);
|
||||
|
||||
assert!(composer.set_active_reasoning_effort(
|
||||
Some(&ReasoningEffort::Ultra),
|
||||
/*animations_enabled*/ true,
|
||||
));
|
||||
assert!(composer.effort_ignition.is_some());
|
||||
assert!(composer.effort_status_line_transition.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effort_composer_restored_baseline_and_reduced_motion_do_not_start() {
|
||||
let (mut composer, _rx) = new_test_composer();
|
||||
|
||||
@@ -933,18 +933,6 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
/// Applies the externally decided Plan-mode nudge visibility to the footer presentation.
|
||||
pub(crate) fn set_plan_mode_nudge_visible(&mut self, visible: bool) {
|
||||
if self.composer.set_plan_mode_nudge_visible(visible) {
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn plan_mode_nudge_visible(&self) -> bool {
|
||||
self.composer.plan_mode_nudge_visible()
|
||||
}
|
||||
|
||||
pub(crate) fn set_remote_image_urls(&mut self, urls: Vec<String>) {
|
||||
self.composer.set_remote_image_urls(urls);
|
||||
self.request_redraw();
|
||||
|
||||
@@ -667,11 +667,6 @@ pub(crate) struct ChatWidget {
|
||||
#[cfg(test)]
|
||||
pet_image_support_override: Option<crate::pets::PetImageSupport>,
|
||||
thread_id: Option<ThreadId>,
|
||||
/// Nudge dismissals that should survive draft edits within the current thread scope.
|
||||
///
|
||||
/// The nudge is only a discovery aid, so once a user dismisses it or enters Plan mode we keep it
|
||||
/// hidden for that thread instead of resurfacing it on every matching draft.
|
||||
dismissed_plan_mode_nudge_scopes: HashSet<PlanModeNudgeScope>,
|
||||
thread_name: Option<String>,
|
||||
thread_rename_block_message: Option<String>,
|
||||
active_side_conversation: bool,
|
||||
@@ -835,31 +830,12 @@ enum SessionConfiguredDisplay {
|
||||
SideConversation,
|
||||
}
|
||||
|
||||
/// Scope used to keep Plan-mode nudge dismissal local to one conversation context.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
enum PlanModeNudgeScope {
|
||||
/// Drafts entered before the server has assigned a thread id.
|
||||
NewThread,
|
||||
/// Drafts associated with one configured thread.
|
||||
Thread(ThreadId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub(crate) enum TurnAbortReason {
|
||||
Interrupted,
|
||||
BudgetLimited,
|
||||
}
|
||||
|
||||
/// Returns whether `text` contains the standalone word `plan`.
|
||||
///
|
||||
/// This intentionally mirrors the App suggestion heuristic instead of trying to infer broader
|
||||
/// planning intent from substrings such as `planning`. Slash and shell drafts still match here so
|
||||
/// callers can keep lexical matching separate from presentation policy.
|
||||
fn contains_plan_keyword(text: &str) -> bool {
|
||||
text.split(|ch: char| !ch.is_alphanumeric() && ch != '_')
|
||||
.any(|word| word.eq_ignore_ascii_case("plan"))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum ThreadItemRenderSource {
|
||||
Live,
|
||||
@@ -1224,7 +1200,6 @@ impl ChatWidget {
|
||||
if let Some(pet) = self.ambient_pet.as_ref() {
|
||||
pet.schedule_next_frame();
|
||||
}
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.refresh_goal_status_indicator_for_time_tick();
|
||||
if self.terminal_title_shows_action_required() != self.last_terminal_title_requires_action {
|
||||
self.refresh_terminal_title();
|
||||
|
||||
@@ -198,7 +198,6 @@ impl ChatWidget {
|
||||
#[cfg(test)]
|
||||
pet_image_support_override: None,
|
||||
thread_id: None,
|
||||
dismissed_plan_mode_nudge_scopes: HashSet::new(),
|
||||
thread_name: None,
|
||||
thread_rename_block_message: None,
|
||||
active_side_conversation: false,
|
||||
|
||||
@@ -78,7 +78,6 @@ impl ChatWidget {
|
||||
if had_modal_or_popup && self.bottom_pane.no_modal_or_popup_active() {
|
||||
self.maybe_send_next_queued_input();
|
||||
}
|
||||
self.refresh_plan_mode_nudge();
|
||||
}
|
||||
|
||||
pub(super) fn defer_input_until_settings_applied(&mut self) {
|
||||
|
||||
@@ -153,14 +153,6 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(key_event.code, KeyCode::Esc)
|
||||
&& key_event.kind == KeyEventKind::Press
|
||||
&& self.should_show_plan_mode_nudge()
|
||||
{
|
||||
self.dismiss_plan_mode_nudge();
|
||||
return;
|
||||
}
|
||||
|
||||
if self.handle_plugins_popup_key_event(key_event) {
|
||||
return;
|
||||
}
|
||||
@@ -178,7 +170,6 @@ impl ChatWidget {
|
||||
self.add_error_message(PARENT_OWNED_INPUT_MESSAGE.to_string());
|
||||
} else {
|
||||
self.cycle_collaboration_mode();
|
||||
self.refresh_plan_mode_nudge();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -220,7 +211,6 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn apply_external_edit(&mut self, text: String) {
|
||||
self.bottom_pane.apply_external_edit(text);
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -238,13 +228,11 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn show_selection_view(&mut self, params: SelectionViewParams) {
|
||||
self.bottom_pane.show_selection_view(params);
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn show_bottom_pane_view(&mut self, view: Box<dyn BottomPaneView>) {
|
||||
self.bottom_pane.show_view(view);
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -254,7 +242,6 @@ impl ChatWidget {
|
||||
view: Box<dyn BottomPaneView>,
|
||||
) {
|
||||
self.bottom_pane.replace_view_if_present(view_id, view);
|
||||
self.refresh_plan_mode_nudge();
|
||||
}
|
||||
|
||||
pub(crate) fn selected_index_for_present_view(&self, view_id: &'static str) -> Option<usize> {
|
||||
@@ -266,13 +253,8 @@ impl ChatWidget {
|
||||
view_id: &'static str,
|
||||
params: SelectionViewParams,
|
||||
) -> bool {
|
||||
let replaced = self
|
||||
.bottom_pane
|
||||
.replace_selection_view_if_present(view_id, params);
|
||||
if replaced {
|
||||
self.refresh_plan_mode_nudge();
|
||||
}
|
||||
replaced
|
||||
self.bottom_pane
|
||||
.replace_selection_view_if_present(view_id, params)
|
||||
}
|
||||
|
||||
pub(crate) fn no_modal_or_popup_active(&self) -> bool {
|
||||
@@ -465,13 +447,11 @@ impl ChatWidget {
|
||||
|
||||
pub(crate) fn handle_paste(&mut self, text: String) {
|
||||
self.bottom_pane.handle_paste(text);
|
||||
self.refresh_plan_mode_nudge();
|
||||
}
|
||||
|
||||
// Returns true if caller should skip rendering this frame (a future frame is scheduled).
|
||||
pub(crate) fn handle_paste_burst_tick(&mut self, frame_requester: FrameRequester) -> bool {
|
||||
if self.bottom_pane.flush_paste_burst_if_due() {
|
||||
self.refresh_plan_mode_nudge();
|
||||
// A paste just flushed; request an immediate redraw and skip this frame.
|
||||
self.request_redraw();
|
||||
true
|
||||
|
||||
@@ -28,7 +28,6 @@ impl ChatWidget {
|
||||
self.review.recent_auto_review_denials = RecentAutoReviewDenials::default();
|
||||
self.clear_thread_usage_state();
|
||||
}
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.turn_lifecycle.reset_thread();
|
||||
self.clear_safety_buffering();
|
||||
self.thread_name = session.thread_name.clone();
|
||||
@@ -106,7 +105,6 @@ impl ChatWidget {
|
||||
mask.reasoning_effort = Some(session.reasoning_effort.clone());
|
||||
}
|
||||
self.update_collaboration_mode_indicator();
|
||||
self.refresh_plan_mode_nudge();
|
||||
}
|
||||
}
|
||||
let effort = self.effective_reasoning_effort();
|
||||
|
||||
@@ -384,48 +384,6 @@ impl ChatWidget {
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns the dismissal scope that applies to the currently visible draft.
|
||||
fn plan_mode_nudge_scope(&self) -> PlanModeNudgeScope {
|
||||
self.thread_id
|
||||
.map_or(PlanModeNudgeScope::NewThread, PlanModeNudgeScope::Thread)
|
||||
}
|
||||
|
||||
/// Returns whether the current draft should replace the normal footer with the Plan-mode nudge.
|
||||
///
|
||||
/// `ChatWidget` owns this policy because it can combine lexical draft matching with mode
|
||||
/// availability, interaction state, and thread-scoped dismissal. `ChatComposer` only renders
|
||||
/// the resulting visibility bit. Keeping slash and shell drafts out here avoids advertising a
|
||||
/// mode switch while the user is intentionally composing another local command.
|
||||
pub(super) fn should_show_plan_mode_nudge(&self) -> bool {
|
||||
let text = self.bottom_pane.composer_text();
|
||||
let trimmed = text.trim_start();
|
||||
self.collaboration_modes_enabled()
|
||||
&& collaboration_modes::plan_mask(self.model_catalog.as_ref()).is_some()
|
||||
&& self.active_mode_kind() != ModeKind::Plan
|
||||
&& self.bottom_pane.composer_input_enabled()
|
||||
&& !self.bottom_pane.is_task_running()
|
||||
&& self.bottom_pane.no_modal_or_popup_active()
|
||||
&& !trimmed.starts_with('/')
|
||||
&& !trimmed.starts_with('!')
|
||||
&& contains_plan_keyword(&text)
|
||||
&& !self
|
||||
.dismissed_plan_mode_nudge_scopes
|
||||
.contains(&self.plan_mode_nudge_scope())
|
||||
}
|
||||
|
||||
/// Synchronizes the footer presentation with the current Plan-mode nudge policy.
|
||||
pub(super) fn refresh_plan_mode_nudge(&mut self) {
|
||||
self.bottom_pane
|
||||
.set_plan_mode_nudge_visible(self.should_show_plan_mode_nudge());
|
||||
}
|
||||
|
||||
/// Hides the nudge for the current thread scope until the user changes conversation context.
|
||||
pub(super) fn dismiss_plan_mode_nudge(&mut self) {
|
||||
self.dismissed_plan_mode_nudge_scopes
|
||||
.insert(self.plan_mode_nudge_scope());
|
||||
self.refresh_plan_mode_nudge();
|
||||
}
|
||||
|
||||
pub(super) fn initial_collaboration_mask(
|
||||
_config: &Config,
|
||||
model_catalog: &ModelCatalog,
|
||||
@@ -580,7 +538,6 @@ impl ChatWidget {
|
||||
developer_instructions: Some(settings.developer_instructions),
|
||||
});
|
||||
self.update_collaboration_mode_indicator();
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.refresh_model_dependent_surfaces();
|
||||
}
|
||||
|
||||
@@ -702,13 +659,8 @@ impl ChatWidget {
|
||||
{
|
||||
mask.reasoning_effort = Some(Some(effort));
|
||||
}
|
||||
if mask.mode == Some(ModeKind::Plan) {
|
||||
self.dismissed_plan_mode_nudge_scopes
|
||||
.insert(self.plan_mode_nudge_scope());
|
||||
}
|
||||
self.active_collaboration_mask = Some(mask);
|
||||
self.update_collaboration_mode_indicator();
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.refresh_model_dependent_surfaces();
|
||||
let next_mode = self.active_mode_kind();
|
||||
let next_model = self.current_model();
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
› make a plan
|
||||
|
||||
Create a plan? shift + tab use Plan mode esc dismiss
|
||||
gpt-5 default · /tmp/project
|
||||
+1
-1
@@ -4,4 +4,4 @@ expression: "render_bottom_popup(&chat, 36)"
|
||||
---
|
||||
› make a plan
|
||||
|
||||
Create a plan? shift + tab use P…
|
||||
gpt-5 default · /tmp/project
|
||||
@@ -1,12 +1,6 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn set_composer_text(chat: &mut ChatWidget, text: &str) {
|
||||
chat.bottom_pane
|
||||
.set_composer_text(text.to_string(), Vec::new(), Vec::new());
|
||||
chat.refresh_plan_mode_nudge();
|
||||
}
|
||||
|
||||
fn paste_hidden_plan_shell_payload(chat: &mut ChatWidget) -> String {
|
||||
let payload = format!("!echo {}", "x".repeat(1000));
|
||||
chat.bottom_pane
|
||||
@@ -68,125 +62,32 @@ fn assert_literal_plan_prompt(chat: &ChatWidget, op: Result<Op, TryRecvError>, p
|
||||
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Plan);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_mode_nudge_matches_only_standalone_plain_text_keyword() {
|
||||
assert!(contains_plan_keyword("plan"));
|
||||
assert!(contains_plan_keyword("Make a Plan first."));
|
||||
assert!(!contains_plan_keyword("plane"));
|
||||
assert!(!contains_plan_keyword("planning"));
|
||||
assert!(contains_plan_keyword("/plan"));
|
||||
assert!(contains_plan_keyword("!plan"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_mode_nudge_shows_only_for_eligible_default_mode_drafts() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
set_composer_text(&mut chat, "make a plan");
|
||||
chat.pre_draw_tick();
|
||||
assert!(chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
set_composer_text(&mut chat, "/plan");
|
||||
chat.pre_draw_tick();
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
set_composer_text(&mut chat, "!plan");
|
||||
chat.pre_draw_tick();
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
set_composer_text(&mut chat, "make a plan");
|
||||
let plan_mask = collaboration_modes::plan_mask(chat.model_catalog.as_ref())
|
||||
.expect("expected plan collaboration mode");
|
||||
chat.set_collaboration_mask(plan_mask);
|
||||
chat.pre_draw_tick();
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_mode_nudge_hides_while_task_or_modal_is_active() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
set_composer_text(&mut chat, "make a plan");
|
||||
chat.pre_draw_tick();
|
||||
assert!(chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
chat.on_task_started();
|
||||
chat.pre_draw_tick();
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
chat.on_task_complete(
|
||||
/*last_agent_message*/ None, /*duration_ms*/ None, /*from_replay*/ false,
|
||||
);
|
||||
chat.show_selection_view(SelectionViewParams {
|
||||
items: vec![SelectionItem {
|
||||
name: "Keep planning".to_string(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
chat.pre_draw_tick();
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_mode_nudge_dismissal_is_scoped_to_current_thread() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
let first_thread = ThreadId::new();
|
||||
let second_thread = ThreadId::new();
|
||||
chat.thread_id = Some(first_thread);
|
||||
set_composer_text(&mut chat, "make a plan");
|
||||
chat.pre_draw_tick();
|
||||
assert!(chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
chat.pre_draw_tick();
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
chat.thread_id = Some(second_thread);
|
||||
chat.pre_draw_tick();
|
||||
assert!(chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
chat.pre_draw_tick();
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
chat.thread_id = Some(first_thread);
|
||||
chat.pre_draw_tick();
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_mode_nudge_shift_tab_uses_existing_mode_cycle_path() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
set_composer_text(&mut chat, "make a plan");
|
||||
chat.pre_draw_tick();
|
||||
assert!(chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::BackTab));
|
||||
chat.pre_draw_tick();
|
||||
assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Plan);
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_mode_nudge_snapshot() {
|
||||
async fn plan_draft_footer_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
chat.set_token_info(Some(make_token_info(
|
||||
/*total_tokens*/ 50_000, /*context_window*/ 100_000,
|
||||
)));
|
||||
set_composer_text(&mut chat, "make a plan");
|
||||
chat.pre_draw_tick();
|
||||
|
||||
assert_chatwidget_snapshot!("plan_mode_nudge", render_bottom_popup(&chat, /*width*/ 80));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_mode_nudge_narrow_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
set_composer_text(&mut chat, "make a plan");
|
||||
chat.bottom_pane
|
||||
.set_composer_text("make a plan".to_string(), Vec::new(), Vec::new());
|
||||
chat.pre_draw_tick();
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"plan_mode_nudge_narrow",
|
||||
render_bottom_popup(&chat, /*width*/ 36)
|
||||
"plan_draft_footer",
|
||||
normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 80))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_draft_footer_narrow_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
chat.bottom_pane
|
||||
.set_composer_text("make a plan".to_string(), Vec::new(), Vec::new());
|
||||
chat.pre_draw_tick();
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"plan_draft_footer_narrow",
|
||||
normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 36))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ impl ChatWidget {
|
||||
|| self.review.is_review_mode
|
||||
|| self.mcp_startup_status.is_some(),
|
||||
);
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.refresh_status_surfaces();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user