mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(tui): suggest plan mode from composer drafts (#19901)
## Summary - suggest Plan mode when the current composer draft contains the standalone word `plan` - shares the Codex App heuristics for detection - excludes things line `/plan` and the word plan in shell mode - reuse the existing `Shift+Tab` mode cycle and add thread-scoped dismissal with `Esc` - replace the normal footer hint while the reminder is visible so the statusline stays anchored https://github.com/user-attachments/assets/01123ae8-cee6-4e95-b563-44655c071cde ## Why The desktop app already nudges users toward Plan mode when their draft clearly signals planning intent. The TUI had the underlying `/plan` and `Shift+Tab` flows, but no equivalent reminder at the moment the user was most likely to benefit from them. ## Details The reminder is shown only when Plan mode is available, the draft contains standalone `plan`, the user is not already in Plan mode, the composer is actionable, and the current thread has not dismissed the reminder. Slash-command and shell-command drafts are excluded. The first implementation used an extra composer row, but that moved the statusline whenever the heuristic fired. This version keeps the layout stable by rendering the reminder in the existing footer row instead. ## Validation - `INSTA_UPDATE=always cargo test -p codex-tui chatwidget::tests::plan_mode::plan_mode_nudge -- --nocapture` - `just fmt` - `just fix -p codex-tui` - `./tools/argument-comment-lint/run.py -p codex-tui` - `cargo insta pending-snapshots` - `git diff --check`
This commit is contained in:
committed by
GitHub
Unverified
parent
273c2e21a9
commit
c6bcd27832
@@ -354,6 +354,11 @@ pub(crate) struct ChatComposer {
|
||||
disable_paste_burst: bool,
|
||||
footer_mode: FooterMode,
|
||||
footer_hint_override: Option<Vec<(String, String)>>,
|
||||
/// Whether the ambient footer row is currently replaced by the Plan-mode nudge.
|
||||
///
|
||||
/// Eligibility is decided by `ChatWidget`; the composer only owns presentation so enabling
|
||||
/// the nudge never changes layout height or reimplements mode-selection policy here.
|
||||
plan_mode_nudge_visible: bool,
|
||||
remote_image_urls: Vec<String>,
|
||||
/// Tracks keyboard selection for the remote-image rows so Up/Down + Delete/Backspace
|
||||
/// can highlight and remove remote attachments from the composer UI.
|
||||
@@ -459,6 +464,19 @@ fn status_line_right_indicator(
|
||||
.or_else(|| goal_status_indicator_line(goal_status_indicator))
|
||||
}
|
||||
|
||||
/// 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 builtin_command_flags(&self) -> BuiltinCommandFlags {
|
||||
BuiltinCommandFlags {
|
||||
@@ -534,6 +552,7 @@ impl ChatComposer {
|
||||
disable_paste_burst: false,
|
||||
footer_mode: FooterMode::ComposerEmpty,
|
||||
footer_hint_override: None,
|
||||
plan_mode_nudge_visible: false,
|
||||
remote_image_urls: Vec::new(),
|
||||
selected_remote_image_index: None,
|
||||
pending_slash_command_history: None,
|
||||
@@ -1027,6 +1046,11 @@ impl ChatComposer {
|
||||
text
|
||||
}
|
||||
|
||||
/// Returns whether the composer currently accepts interactive draft edits.
|
||||
pub(crate) fn input_enabled(&self) -> bool {
|
||||
self.input_enabled
|
||||
}
|
||||
|
||||
pub(crate) fn pending_pastes(&self) -> Vec<(String, String)> {
|
||||
self.pending_pastes.clone()
|
||||
}
|
||||
@@ -1045,6 +1069,23 @@ 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.plan_mode_nudge_visible == visible {
|
||||
return false;
|
||||
}
|
||||
self.plan_mode_nudge_visible = visible;
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn plan_mode_nudge_visible(&self) -> bool {
|
||||
self.plan_mode_nudge_visible
|
||||
}
|
||||
|
||||
pub(crate) fn set_remote_image_urls(&mut self, urls: Vec<String>) {
|
||||
self.remote_image_urls = urls;
|
||||
self.selected_remote_image_index = None;
|
||||
@@ -4047,6 +4088,17 @@ impl ChatComposer {
|
||||
};
|
||||
if let Some(line) = self.history_search_footer_line() {
|
||||
render_footer_line(hint_rect, buf, line);
|
||||
} else if self.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;
|
||||
|
||||
@@ -774,6 +774,11 @@ impl BottomPane {
|
||||
self.composer.current_text_with_pending()
|
||||
}
|
||||
|
||||
/// Returns whether the composer currently accepts interactive draft edits.
|
||||
pub(crate) fn composer_input_enabled(&self) -> bool {
|
||||
self.composer.input_enabled()
|
||||
}
|
||||
|
||||
pub(crate) fn composer_pending_pastes(&self) -> Vec<(String, String)> {
|
||||
self.composer.pending_pastes()
|
||||
}
|
||||
@@ -788,6 +793,18 @@ 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();
|
||||
|
||||
@@ -934,6 +934,11 @@ pub(crate) struct ChatWidget {
|
||||
pending_status_indicator_restore: bool,
|
||||
suppress_queue_autosend: bool,
|
||||
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>,
|
||||
last_turn_id: Option<String>,
|
||||
budget_limited_turn_ids: HashSet<String>,
|
||||
thread_name: Option<String>,
|
||||
@@ -1594,6 +1599,25 @@ 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),
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -1993,6 +2017,7 @@ impl ChatWidget {
|
||||
fn update_task_running_state(&mut self) {
|
||||
self.bottom_pane
|
||||
.set_task_running(self.agent_turn_running || self.mcp_startup_status.is_some());
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.refresh_status_surfaces();
|
||||
}
|
||||
|
||||
@@ -2331,6 +2356,7 @@ impl ChatWidget {
|
||||
if previous_thread_id != self.thread_id {
|
||||
self.recent_auto_review_denials = RecentAutoReviewDenials::default();
|
||||
}
|
||||
self.refresh_plan_mode_nudge();
|
||||
self.last_turn_id = None;
|
||||
self.thread_name = event.thread_name.clone();
|
||||
self.current_goal_status_indicator = None;
|
||||
@@ -4915,6 +4941,7 @@ impl ChatWidget {
|
||||
self.update_due_hook_visibility();
|
||||
self.schedule_hook_timer_if_needed();
|
||||
self.bottom_pane.pre_draw_tick();
|
||||
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();
|
||||
@@ -5584,6 +5611,7 @@ impl ChatWidget {
|
||||
pending_status_indicator_restore: false,
|
||||
suppress_queue_autosend: false,
|
||||
thread_id: None,
|
||||
dismissed_plan_mode_nudge_scopes: HashSet::new(),
|
||||
last_turn_id: None,
|
||||
budget_limited_turn_ids: HashSet::new(),
|
||||
thread_name: None,
|
||||
@@ -5806,6 +5834,14 @@ 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;
|
||||
}
|
||||
|
||||
match key_event {
|
||||
KeyEvent {
|
||||
code: KeyCode::BackTab,
|
||||
@@ -5816,6 +5852,7 @@ impl ChatWidget {
|
||||
&& self.bottom_pane.no_modal_or_popup_active() =>
|
||||
{
|
||||
self.cycle_collaboration_mode();
|
||||
self.refresh_plan_mode_nudge();
|
||||
}
|
||||
_ => {
|
||||
let had_modal_or_popup = !self.bottom_pane.no_modal_or_popup_active();
|
||||
@@ -5893,6 +5930,7 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5920,6 +5958,7 @@ 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();
|
||||
}
|
||||
|
||||
@@ -5937,6 +5976,7 @@ 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();
|
||||
}
|
||||
|
||||
@@ -6060,11 +6100,13 @@ 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
|
||||
@@ -10799,6 +10841,48 @@ 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.
|
||||
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.
|
||||
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.
|
||||
fn dismiss_plan_mode_nudge(&mut self) {
|
||||
self.dismissed_plan_mode_nudge_scopes
|
||||
.insert(self.plan_mode_nudge_scope());
|
||||
self.refresh_plan_mode_nudge();
|
||||
}
|
||||
|
||||
fn initial_collaboration_mask(
|
||||
_config: &Config,
|
||||
model_catalog: &ModelCatalog,
|
||||
@@ -10989,8 +11073,13 @@ 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();
|
||||
@@ -11616,6 +11705,7 @@ impl ChatWidget {
|
||||
) {
|
||||
self.bottom_pane
|
||||
.set_composer_text(text, text_elements, local_image_paths);
|
||||
self.refresh_plan_mode_nudge();
|
||||
}
|
||||
|
||||
pub(crate) fn set_remote_image_urls(&mut self, remote_image_urls: Vec<String>) {
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/plan_mode.rs
|
||||
expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
› make a plan
|
||||
|
||||
Create a plan? shift + tab use Plan mode esc dismiss
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/plan_mode.rs
|
||||
expression: "render_bottom_popup(&chat, 36)"
|
||||
---
|
||||
› make a plan
|
||||
|
||||
Create a plan? shift + tab use P…
|
||||
@@ -257,6 +257,7 @@ pub(super) async fn make_chatwidget_manual(
|
||||
pending_status_indicator_restore: false,
|
||||
suppress_queue_autosend: false,
|
||||
thread_id: None,
|
||||
dismissed_plan_mode_nudge_scopes: HashSet::new(),
|
||||
last_turn_id: None,
|
||||
budget_limited_turn_ids: HashSet::new(),
|
||||
thread_name: None,
|
||||
|
||||
@@ -1,6 +1,126 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[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;
|
||||
chat.set_composer_text("make a plan".to_string(), Vec::new(), Vec::new());
|
||||
chat.pre_draw_tick();
|
||||
assert!(chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
chat.set_composer_text("/plan".to_string(), Vec::new(), Vec::new());
|
||||
chat.pre_draw_tick();
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
chat.set_composer_text("!plan".to_string(), Vec::new(), Vec::new());
|
||||
chat.pre_draw_tick();
|
||||
assert!(!chat.bottom_pane.plan_mode_nudge_visible());
|
||||
|
||||
chat.set_composer_text("make a plan".to_string(), Vec::new(), Vec::new());
|
||||
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;
|
||||
chat.set_composer_text("make a plan".to_string(), Vec::new(), Vec::new());
|
||||
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, /*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);
|
||||
chat.set_composer_text("make a plan".to_string(), Vec::new(), Vec::new());
|
||||
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;
|
||||
chat.set_composer_text("make a plan".to_string(), Vec::new(), Vec::new());
|
||||
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() {
|
||||
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,
|
||||
)));
|
||||
chat.set_composer_text("make a plan".to_string(), Vec::new(), Vec::new());
|
||||
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;
|
||||
chat.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)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_implementation_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
|
||||
Reference in New Issue
Block a user