mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(tui): add clear-context plan implementation (#17499)
## TL;DR - Adds a second Plan Mode handoff: implement the approved plan after clearing context. - Keeps the existing same-thread `Yes, implement this plan` action unchanged. - Reuses the `/clear` thread-start path and submits the approved plan as the fresh thread's first prompt. - Covers the new popup option, event plumbing, initial-message behavior, and disabled states in TUI tests. ## Problem Plan Mode already asks whether to implement an approved plan, but the only affirmative path continues in the same thread. That is useful when the planning conversation itself is still valuable, but it does not support the workflow where exploratory planning context is discarded and implementation starts from the final approved plan as the only model-visible handoff. <img width="1253" height="869" alt="image" src="https://github.com/user-attachments/assets/90023d75-c330-4919-bed8-518671c3474b" /> ## Mental model There are now two implementation choices after a proposed plan. The existing choice, `Yes, implement this plan`, is unchanged: it switches to Default mode and submits `Implement the plan.` in the current thread. The new choice, `Yes, clear context and implement`, treats the proposed plan as a handoff artifact. It clears the UI/session context through the same thread-start source used by `/clear`, then submits an initial prompt containing the approved plan after the fresh thread is configured. The important distinction is that the new path is not compaction. The model receives a deliberate implementation prompt built from the approved plan markdown, not a summary of the previous planning transcript. Both implementation choices require the Default collaboration preset to be available, so the popup does not offer a coding handoff when the fresh thread would fall back to another mode. ## Non-goals This change does not alter `/clear`, `/compact`, or the existing same-context Plan Mode implementation option. It does not add protocol surface area or app-server schema changes. It also does not carry the previous transcript path or a generated planning summary into the new model context. ## Tradeoffs The fresh-context option relies on the approved plan being sufficiently complete. That matches the Plan Mode contract, but it means vague plans will produce weaker implementation starts than a compacted transcript would. The upside is that rejected ideas, exploratory dead ends, and planning corrections do not leak into the implementation turn. The current implementation stores the latest proposed plan in `ChatWidget` rather than deriving it from history cells at selection time. This keeps the popup action simple and deterministic, but it makes the cache lifecycle important: it must be reset when a new task starts so an old plan cannot be submitted later. ## Architecture The TUI stores the most recent completed proposed-plan markdown when a plan item completes. The Plan Mode approval popup uses that cache to enable the fresh-context option and to build a first-turn prompt that instructs the model to implement the approved plan in a fresh context. Selecting the new option emits a TUI-internal `ClearUiAndSubmitUserMessage` event. `App` handles that event by reusing the existing clear flow: clear terminal state, reset app UI state, start a new app-server thread with `ThreadStartSource::Clear`, and attach a replacement `ChatWidget` with an initial user message. The existing initial-message suppression in `enqueue_primary_thread_session` ensures the prompt is submitted only after the new session is configured and any startup replay is rendered. ## Observability The previous thread remains resumable through the existing clear-session summary hint. There is no new telemetry or protocol event for this path, so debugging should start at the TUI event boundary: confirm the popup emitted `ClearUiAndSubmitUserMessage`, confirm the app-server thread start used `ThreadStartSource::Clear`, then confirm the fresh widget submitted the initial user message after `SessionConfigured`. ## Tests The Plan Mode popup snapshots cover the new option and preserve the original option as the first/default action. Unit coverage verifies the original same-context option still emits `SubmitUserMessageWithMode`, the new option emits `ClearUiAndSubmitUserMessage` with the approved plan embedded verbatim, and the clear-context option is disabled when Default mode is unavailable or no approved plan exists. The broader `codex-tui` test package passes with the updated fresh-thread initial-message plumbing.
This commit is contained in:
committed by
GitHub
Unverified
parent
ea84537369
commit
d3692b14c9
@@ -44,6 +44,8 @@ In the codex-rs folder where the rust code lives:
|
||||
`codex-rs/tui/src/bottom_pane/mod.rs`, and similarly central orchestration modules.
|
||||
- When extracting code from a large module, move the related tests and module/type docs toward
|
||||
the new implementation so the invariants stay close to the code that owns them.
|
||||
- Avoid adding new standalone methods to `codex-rs/tui/src/chatwidget.rs` unless the change is
|
||||
trivial; prefer new modules/files and keep `chatwidget.rs` focused on orchestration.
|
||||
- When running Rust commands (e.g. `just fix` or `cargo test`) be patient with the command and never try to kill them using the PID. Rust lock can make the execution slow, this is expected.
|
||||
|
||||
Run `just fmt` (in `codex-rs` directory) automatically after you have finished making Rust code changes; do not ask for approval to run it. Additionally, run the tests:
|
||||
|
||||
+49
-8
@@ -1118,13 +1118,13 @@ impl App {
|
||||
&self,
|
||||
tui: &mut tui::Tui,
|
||||
cfg: crate::legacy_core::config::Config,
|
||||
initial_user_message: Option<crate::chatwidget::UserMessage>,
|
||||
) -> crate::chatwidget::ChatWidgetInit {
|
||||
crate::chatwidget::ChatWidgetInit {
|
||||
config: cfg,
|
||||
frame_requester: tui.frame_requester(),
|
||||
app_event_tx: self.app_event_tx.clone(),
|
||||
// Fork/resume bootstraps here don't carry any prefilled message content.
|
||||
initial_user_message: None,
|
||||
initial_user_message,
|
||||
enhanced_keys_supported: self.enhanced_keys_supported,
|
||||
has_chatgpt_account: self.chat_widget.has_chatgpt_account(),
|
||||
model_catalog: self.model_catalog.clone(),
|
||||
@@ -3418,7 +3418,11 @@ impl App {
|
||||
self.active_thread_id = Some(thread_id);
|
||||
self.active_thread_rx = Some(receiver);
|
||||
|
||||
let init = self.chatwidget_init_for_forked_or_resumed_thread(tui, self.config.clone());
|
||||
let init = self.chatwidget_init_for_forked_or_resumed_thread(
|
||||
tui,
|
||||
self.config.clone(),
|
||||
/*initial_user_message*/ None,
|
||||
);
|
||||
self.replace_chat_widget(ChatWidget::new_with_app_event(init));
|
||||
|
||||
self.reset_for_thread_switch(tui)?;
|
||||
@@ -3479,9 +3483,11 @@ impl App {
|
||||
tui: &mut tui::Tui,
|
||||
app_server: &mut AppServerSession,
|
||||
session_start_source: Option<ThreadStartSource>,
|
||||
initial_user_message: Option<crate::chatwidget::UserMessage>,
|
||||
) {
|
||||
// Start a fresh in-memory session while preserving resumability via persisted rollout
|
||||
// history.
|
||||
// history. If an initial message is provided, `enqueue_primary_thread_session` suppresses it
|
||||
// until the new session is configured and any replayed turns have been rendered.
|
||||
self.refresh_in_memory_config_from_disk_best_effort("starting a new thread")
|
||||
.await;
|
||||
let model = self.chat_widget.current_model().to_string();
|
||||
@@ -3507,7 +3513,12 @@ impl App {
|
||||
{
|
||||
Ok(started) => {
|
||||
if let Err(err) = self
|
||||
.replace_chat_widget_with_app_server_thread(tui, app_server, started)
|
||||
.replace_chat_widget_with_app_server_thread(
|
||||
tui,
|
||||
app_server,
|
||||
started,
|
||||
initial_user_message,
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.chat_widget.add_error_message(format!(
|
||||
@@ -3540,9 +3551,17 @@ impl App {
|
||||
tui: &mut tui::Tui,
|
||||
app_server: &mut AppServerSession,
|
||||
started: AppServerStartedThread,
|
||||
initial_user_message: Option<crate::chatwidget::UserMessage>,
|
||||
) -> Result<()> {
|
||||
// Initial messages are for freshly attached primary threads only. Thread switches and
|
||||
// resume/fork flows pass `None` so they cannot replay old history and then auto-submit a new
|
||||
// user turn by accident.
|
||||
self.reset_thread_event_state();
|
||||
let init = self.chatwidget_init_for_forked_or_resumed_thread(tui, self.config.clone());
|
||||
let init = self.chatwidget_init_for_forked_or_resumed_thread(
|
||||
tui,
|
||||
self.config.clone(),
|
||||
initial_user_message,
|
||||
);
|
||||
self.replace_chat_widget(ChatWidget::new_with_app_event(init));
|
||||
self.enqueue_primary_thread_session(started.session, started.turns)
|
||||
.await?;
|
||||
@@ -4312,7 +4331,9 @@ impl App {
|
||||
self.file_search
|
||||
.update_search_dir(self.config.cwd.to_path_buf());
|
||||
match self
|
||||
.replace_chat_widget_with_app_server_thread(tui, app_server, resumed)
|
||||
.replace_chat_widget_with_app_server_thread(
|
||||
tui, app_server, resumed, /*initial_user_message*/ None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
@@ -4357,6 +4378,7 @@ impl App {
|
||||
AppEvent::NewSession => {
|
||||
self.start_fresh_session_with_summary_hint(
|
||||
tui, app_server, /*session_start_source*/ None,
|
||||
/*initial_user_message*/ None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -4368,6 +4390,23 @@ impl App {
|
||||
tui,
|
||||
app_server,
|
||||
Some(ThreadStartSource::Clear),
|
||||
/*initial_user_message*/ None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
AppEvent::ClearUiAndSubmitUserMessage { text } => {
|
||||
self.clear_terminal_ui(tui, /*redraw_header*/ false)?;
|
||||
self.reset_app_ui_state_after_clear();
|
||||
|
||||
self.start_fresh_session_with_summary_hint(
|
||||
tui,
|
||||
app_server,
|
||||
Some(ThreadStartSource::Clear),
|
||||
crate::chatwidget::create_initial_user_message(
|
||||
Some(text),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -4462,7 +4501,9 @@ impl App {
|
||||
Ok(forked) => {
|
||||
self.shutdown_current_thread(app_server).await;
|
||||
match self
|
||||
.replace_chat_widget_with_app_server_thread(tui, app_server, forked)
|
||||
.replace_chat_widget_with_app_server_thread(
|
||||
tui, app_server, forked, /*initial_user_message*/ None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
|
||||
@@ -122,6 +122,14 @@ pub(crate) enum AppEvent {
|
||||
/// previous chat resumable.
|
||||
ClearUi,
|
||||
|
||||
/// Clear the current context, start a fresh session, and submit an initial user message.
|
||||
///
|
||||
/// This is the Plan Mode handoff path: the previous thread remains resumable, but the model
|
||||
/// sees only the explicit prompt carried in `text` once the new session is configured.
|
||||
ClearUiAndSubmitUserMessage {
|
||||
text: String,
|
||||
},
|
||||
|
||||
/// Open the resume picker inside the running TUI session.
|
||||
OpenResumePicker,
|
||||
|
||||
|
||||
@@ -245,10 +245,6 @@ use tracing::debug;
|
||||
use tracing::warn;
|
||||
|
||||
const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading";
|
||||
const PLAN_IMPLEMENTATION_TITLE: &str = "Implement this plan?";
|
||||
const PLAN_IMPLEMENTATION_YES: &str = "Yes, implement this plan";
|
||||
const PLAN_IMPLEMENTATION_NO: &str = "No, stay in Plan mode";
|
||||
const PLAN_IMPLEMENTATION_CODING_MESSAGE: &str = "Implement the plan.";
|
||||
const MULTI_AGENT_ENABLE_TITLE: &str = "Enable subagents?";
|
||||
const MULTI_AGENT_ENABLE_YES: &str = "Yes, enable";
|
||||
const MULTI_AGENT_ENABLE_NO: &str = "Not now";
|
||||
@@ -370,6 +366,8 @@ use self::skills::find_app_mentions;
|
||||
use self::skills::find_skill_mentions_with_tool_mentions;
|
||||
mod plugins;
|
||||
use self::plugins::PluginsCacheState;
|
||||
mod plan_implementation;
|
||||
use self::plan_implementation::PLAN_IMPLEMENTATION_TITLE;
|
||||
mod realtime;
|
||||
use self::realtime::RealtimeConversationUiState;
|
||||
use self::realtime::RenderedUserMessageEvent;
|
||||
@@ -794,6 +792,11 @@ pub(crate) struct ChatWidget {
|
||||
/// may still return the response from before the rollback. Keeping this as
|
||||
/// a single cache avoids coupling copy state to the backtrack transcript.
|
||||
last_agent_markdown: Option<String>,
|
||||
/// Raw markdown of the most recently completed proposed plan.
|
||||
///
|
||||
/// This is cached only for the approval popup. It is reset at the start of each new task so the
|
||||
/// fresh-context action cannot accidentally submit an older plan after a later turn begins.
|
||||
latest_proposed_plan_markdown: Option<String>,
|
||||
/// Whether this turn already produced a copyable response.
|
||||
///
|
||||
/// `TurnComplete.last_agent_message` is a fallback source: use it only when no earlier
|
||||
@@ -2248,6 +2251,7 @@ impl ChatWidget {
|
||||
};
|
||||
if !plan_text.trim().is_empty() {
|
||||
self.record_agent_markdown(&plan_text);
|
||||
self.latest_proposed_plan_markdown = Some(plan_text.clone());
|
||||
}
|
||||
// Plan commit ticks can hide the status row; remember whether we streamed plan output so
|
||||
// completion can restore it once stream queues are idle.
|
||||
@@ -2327,6 +2331,7 @@ impl ChatWidget {
|
||||
self.saw_copy_source_this_turn = false;
|
||||
self.saw_plan_update_this_turn = false;
|
||||
self.saw_plan_item_this_turn = false;
|
||||
self.latest_proposed_plan_markdown = None;
|
||||
self.last_plan_progress = None;
|
||||
self.plan_delta_buffer.clear();
|
||||
self.plan_item_active = false;
|
||||
@@ -2474,48 +2479,12 @@ impl ChatWidget {
|
||||
|
||||
fn open_plan_implementation_prompt(&mut self) {
|
||||
let default_mask = collaboration_modes::default_mode_mask(self.model_catalog.as_ref());
|
||||
let (implement_actions, implement_disabled_reason) = match default_mask {
|
||||
Some(mask) => {
|
||||
let user_text = PLAN_IMPLEMENTATION_CODING_MESSAGE.to_string();
|
||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::SubmitUserMessageWithMode {
|
||||
text: user_text.clone(),
|
||||
collaboration_mode: mask.clone(),
|
||||
});
|
||||
})];
|
||||
(actions, None)
|
||||
}
|
||||
None => (Vec::new(), Some("Default mode unavailable".to_string())),
|
||||
};
|
||||
let items = vec![
|
||||
SelectionItem {
|
||||
name: PLAN_IMPLEMENTATION_YES.to_string(),
|
||||
description: Some("Switch to Default and start coding.".to_string()),
|
||||
selected_description: None,
|
||||
is_current: false,
|
||||
actions: implement_actions,
|
||||
disabled_reason: implement_disabled_reason,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: PLAN_IMPLEMENTATION_NO.to_string(),
|
||||
description: Some("Continue planning with the model.".to_string()),
|
||||
selected_description: None,
|
||||
is_current: false,
|
||||
actions: Vec::new(),
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
title: Some(PLAN_IMPLEMENTATION_TITLE.to_string()),
|
||||
subtitle: None,
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
..Default::default()
|
||||
});
|
||||
self.bottom_pane
|
||||
.show_selection_view(plan_implementation::selection_view_params(
|
||||
default_mask,
|
||||
self.latest_proposed_plan_markdown.as_deref(),
|
||||
));
|
||||
self.notify(Notification::PlanModePrompt {
|
||||
title: PLAN_IMPLEMENTATION_TITLE.to_string(),
|
||||
});
|
||||
@@ -4896,6 +4865,7 @@ impl ChatWidget {
|
||||
agent_turn_running: false,
|
||||
mcp_startup_status: None,
|
||||
last_agent_markdown: None,
|
||||
latest_proposed_plan_markdown: None,
|
||||
saw_copy_source_this_turn: false,
|
||||
mcp_startup_expected_servers: None,
|
||||
mcp_startup_ignore_updates_until_next_start: false,
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::bottom_pane::SelectionAction;
|
||||
use crate::bottom_pane::SelectionItem;
|
||||
use crate::bottom_pane::SelectionViewParams;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||
|
||||
pub(super) const PLAN_IMPLEMENTATION_TITLE: &str = "Implement this plan?";
|
||||
const PLAN_IMPLEMENTATION_YES: &str = "Yes, implement this plan";
|
||||
const PLAN_IMPLEMENTATION_CLEAR_CONTEXT: &str = "Yes, clear context and implement";
|
||||
const PLAN_IMPLEMENTATION_NO: &str = "No, stay in Plan mode";
|
||||
pub(super) const PLAN_IMPLEMENTATION_CODING_MESSAGE: &str = "Implement the plan.";
|
||||
pub(super) const PLAN_IMPLEMENTATION_CLEAR_CONTEXT_PREFIX: &str = concat!(
|
||||
"A previous agent produced the plan below to accomplish the user's task. ",
|
||||
"Implement the plan in a fresh context. Treat the plan as the source of ",
|
||||
"user intent, re-read files as needed, and carry the work through ",
|
||||
"implementation and verification."
|
||||
);
|
||||
pub(super) const PLAN_IMPLEMENTATION_DEFAULT_UNAVAILABLE: &str = "Default mode unavailable";
|
||||
pub(super) const PLAN_IMPLEMENTATION_NO_APPROVED_PLAN: &str = "No approved plan available";
|
||||
|
||||
pub(super) fn selection_view_params(
|
||||
default_mask: Option<CollaborationModeMask>,
|
||||
plan_markdown: Option<&str>,
|
||||
) -> SelectionViewParams {
|
||||
let (implement_actions, implement_disabled_reason) = match default_mask.clone() {
|
||||
Some(mask) => {
|
||||
let user_text = PLAN_IMPLEMENTATION_CODING_MESSAGE.to_string();
|
||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::SubmitUserMessageWithMode {
|
||||
text: user_text.clone(),
|
||||
collaboration_mode: mask.clone(),
|
||||
});
|
||||
})];
|
||||
(actions, None)
|
||||
}
|
||||
None => (
|
||||
Vec::new(),
|
||||
Some(PLAN_IMPLEMENTATION_DEFAULT_UNAVAILABLE.to_string()),
|
||||
),
|
||||
};
|
||||
|
||||
let (clear_context_actions, clear_context_disabled_reason) = match (default_mask, plan_markdown)
|
||||
{
|
||||
(None, _) => (
|
||||
Vec::new(),
|
||||
Some(PLAN_IMPLEMENTATION_DEFAULT_UNAVAILABLE.to_string()),
|
||||
),
|
||||
(Some(_), Some(plan_markdown)) if !plan_markdown.trim().is_empty() => {
|
||||
let user_text =
|
||||
format!("{PLAN_IMPLEMENTATION_CLEAR_CONTEXT_PREFIX}\n\n{plan_markdown}");
|
||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::ClearUiAndSubmitUserMessage {
|
||||
text: user_text.clone(),
|
||||
});
|
||||
})];
|
||||
(actions, None)
|
||||
}
|
||||
(Some(_), _) => (
|
||||
Vec::new(),
|
||||
Some(PLAN_IMPLEMENTATION_NO_APPROVED_PLAN.to_string()),
|
||||
),
|
||||
};
|
||||
|
||||
SelectionViewParams {
|
||||
title: Some(PLAN_IMPLEMENTATION_TITLE.to_string()),
|
||||
subtitle: None,
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items: vec![
|
||||
SelectionItem {
|
||||
name: PLAN_IMPLEMENTATION_YES.to_string(),
|
||||
description: Some("Switch to Default and start coding.".to_string()),
|
||||
selected_description: None,
|
||||
is_current: false,
|
||||
actions: implement_actions,
|
||||
disabled_reason: implement_disabled_reason,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: PLAN_IMPLEMENTATION_CLEAR_CONTEXT.to_string(),
|
||||
description: Some("Fresh thread with this plan.".to_string()),
|
||||
selected_description: None,
|
||||
is_current: false,
|
||||
actions: clear_context_actions,
|
||||
disabled_reason: clear_context_disabled_reason,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: PLAN_IMPLEMENTATION_NO.to_string(),
|
||||
description: Some("Continue planning with the model.".to_string()),
|
||||
selected_description: None,
|
||||
is_current: false,
|
||||
actions: Vec::new(),
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -4,7 +4,8 @@ expression: popup
|
||||
---
|
||||
Implement this plan?
|
||||
|
||||
› 1. Yes, implement this plan Switch to Default and start coding.
|
||||
2. No, stay in Plan mode Continue planning with the model.
|
||||
› 1. Yes, implement this plan Switch to Default and start coding.
|
||||
2. Yes, clear context and implement Fresh thread with this plan.
|
||||
3. No, stay in Plan mode Continue planning with the model.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
+3
-2
@@ -4,7 +4,8 @@ expression: popup
|
||||
---
|
||||
Implement this plan?
|
||||
|
||||
1. Yes, implement this plan Switch to Default and start coding.
|
||||
› 2. No, stay in Plan mode Continue planning with the model.
|
||||
1. Yes, implement this plan Switch to Default and start coding.
|
||||
› 2. Yes, clear context and implement Fresh thread with this plan.
|
||||
3. No, stay in Plan mode Continue planning with the model.
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
|
||||
@@ -209,6 +209,7 @@ pub(super) async fn make_chatwidget_manual(
|
||||
pending_guardian_review_status: PendingGuardianReviewStatus::default(),
|
||||
terminal_title_status_kind: TerminalTitleStatusKind::Working,
|
||||
last_agent_markdown: None,
|
||||
latest_proposed_plan_markdown: None,
|
||||
saw_copy_source_this_turn: false,
|
||||
running_commands: HashMap::new(),
|
||||
collab_agent_metadata: HashMap::new(),
|
||||
|
||||
@@ -4,6 +4,7 @@ use pretty_assertions::assert_eq;
|
||||
#[tokio::test]
|
||||
async fn plan_implementation_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
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);
|
||||
@@ -13,6 +14,7 @@ async fn plan_implementation_popup_snapshot() {
|
||||
#[tokio::test]
|
||||
async fn plan_implementation_popup_no_selected_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
chat.on_plan_item_completed("- Step 1\n- Step 2\n".to_string());
|
||||
chat.open_plan_implementation_prompt();
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
|
||||
@@ -35,10 +37,71 @@ async fn plan_implementation_popup_yes_emits_submit_message_event() {
|
||||
else {
|
||||
panic!("expected SubmitUserMessageWithMode, got {event:?}");
|
||||
};
|
||||
assert_eq!(text, PLAN_IMPLEMENTATION_CODING_MESSAGE);
|
||||
assert_eq!(
|
||||
text,
|
||||
plan_implementation::PLAN_IMPLEMENTATION_CODING_MESSAGE
|
||||
);
|
||||
assert_eq!(collaboration_mode.mode, Some(ModeKind::Default));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_implementation_popup_clear_context_emits_clear_submit_event() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
let plan_markdown = "- Step 1\n- Step 2\n";
|
||||
chat.on_plan_item_completed(plan_markdown.to_string());
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
chat.open_plan_implementation_prompt();
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
let event = rx.try_recv().expect("expected AppEvent");
|
||||
let AppEvent::ClearUiAndSubmitUserMessage { text } = event else {
|
||||
panic!("expected ClearUiAndSubmitUserMessage, got {event:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
text,
|
||||
"A previous agent produced the plan below to accomplish the user's task. \
|
||||
Implement the plan in a fresh context. Treat the plan as the source of \
|
||||
user intent, re-read files as needed, and carry the work through \
|
||||
implementation and verification.\n\n- Step 1\n- Step 2\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_implementation_clear_context_requires_default_mode_and_plan() {
|
||||
let (chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
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"));
|
||||
assert_eq!(
|
||||
params.items[1].disabled_reason.as_deref(),
|
||||
Some(plan_implementation::PLAN_IMPLEMENTATION_DEFAULT_UNAVAILABLE)
|
||||
);
|
||||
|
||||
let params = plan_implementation::selection_view_params(
|
||||
Some(default_mask.clone()),
|
||||
/*plan_markdown*/ 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"));
|
||||
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"));
|
||||
assert_eq!(params.items[1].disabled_reason, None);
|
||||
assert!(!params.items[1].actions.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn submit_user_message_with_mode_sets_coding_collaboration_mode() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
|
||||
Reference in New Issue
Block a user