mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
queue interactive slash command selections
Keep interactive slash flows usable while a turn is running, and queue the resulting action instead of the bare slash token. Also let Esc interrupt through popup-active states so queued drafts restore without dropping queued slash actions. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
co-authored by
Codex
parent
a8f1f43c1f
commit
e5f1b8435d
@@ -2717,6 +2717,15 @@ impl App {
|
||||
self.chat_widget.set_model(&model);
|
||||
self.refresh_status_line();
|
||||
}
|
||||
AppEvent::ApplyOrQueueModelSelection {
|
||||
model,
|
||||
effort,
|
||||
scope,
|
||||
} => {
|
||||
self.chat_widget
|
||||
.apply_or_queue_model_selection(model, effort, scope);
|
||||
self.refresh_status_line();
|
||||
}
|
||||
AppEvent::UpdateCollaborationMode(mask) => {
|
||||
self.chat_widget.set_collaboration_mask(mask);
|
||||
self.refresh_status_line();
|
||||
@@ -2737,6 +2746,9 @@ impl App {
|
||||
AppEvent::OpenAllModelsPopup { models } => {
|
||||
self.chat_widget.open_all_models_popup(models);
|
||||
}
|
||||
AppEvent::ApplyOrQueueReview { review_request } => {
|
||||
self.chat_widget.apply_or_queue_review(review_request);
|
||||
}
|
||||
AppEvent::OpenFullAccessConfirmation {
|
||||
preset,
|
||||
return_to_permissions,
|
||||
|
||||
@@ -16,6 +16,7 @@ use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_utils_approval_presets::ApprovalPreset;
|
||||
|
||||
use crate::bottom_pane::ApprovalRequest;
|
||||
@@ -53,6 +54,13 @@ impl RealtimeAudioDeviceKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ModelSelectionScope {
|
||||
Global,
|
||||
PlanOnly,
|
||||
AllModes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
pub(crate) enum WindowsSandboxEnableMode {
|
||||
@@ -182,6 +190,14 @@ pub(crate) enum AppEvent {
|
||||
/// Update the current model slug in the running app and widget.
|
||||
UpdateModel(String),
|
||||
|
||||
/// Apply a model selection immediately or queue it when chosen from an interactive picker
|
||||
/// while a task is still running.
|
||||
ApplyOrQueueModelSelection {
|
||||
model: String,
|
||||
effort: Option<ReasoningEffort>,
|
||||
scope: ModelSelectionScope,
|
||||
},
|
||||
|
||||
/// Update the active collaboration mask in the running app and widget.
|
||||
UpdateCollaborationMode(CollaborationModeMask),
|
||||
|
||||
@@ -240,6 +256,12 @@ pub(crate) enum AppEvent {
|
||||
models: Vec<ModelPreset>,
|
||||
},
|
||||
|
||||
/// Submit a review request immediately or queue it when chosen from an interactive picker
|
||||
/// while a task is still running.
|
||||
ApplyOrQueueReview {
|
||||
review_request: ReviewRequest,
|
||||
},
|
||||
|
||||
/// Open the confirmation prompt before enabling full access mode.
|
||||
OpenFullAccessConfirmation {
|
||||
preset: ApprovalPreset,
|
||||
|
||||
@@ -137,11 +137,11 @@ pub(crate) enum CancellationEvent {
|
||||
NotHandled,
|
||||
}
|
||||
|
||||
use crate::bottom_pane::prompt_args::parse_slash_name;
|
||||
pub(crate) use chat_composer::ChatComposer;
|
||||
pub(crate) use chat_composer::ChatComposerConfig;
|
||||
pub(crate) use chat_composer::InputResult;
|
||||
use codex_protocol::custom_prompts::CustomPrompt;
|
||||
pub(crate) use prompt_args::parse_slash_name;
|
||||
|
||||
use crate::status_indicator_widget::StatusDetailsCapitalization;
|
||||
use crate::status_indicator_widget::StatusIndicatorWidget;
|
||||
|
||||
+164
-44
@@ -216,6 +216,7 @@ fn queued_message_edit_binding_for_terminal(terminal_name: TerminalName) -> KeyB
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event::ConnectorsSnapshot;
|
||||
use crate::app_event::ExitMode;
|
||||
use crate::app_event::ModelSelectionScope;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::app_event::WindowsSandboxEnableMode;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
@@ -238,6 +239,7 @@ use crate::bottom_pane::SelectionAction;
|
||||
use crate::bottom_pane::SelectionItem;
|
||||
use crate::bottom_pane::SelectionViewParams;
|
||||
use crate::bottom_pane::custom_prompt_view::CustomPromptView;
|
||||
use crate::bottom_pane::parse_slash_name;
|
||||
use crate::bottom_pane::popup_consts::standard_popup_hint_line;
|
||||
use crate::clipboard_paste::paste_image_to_temp_png;
|
||||
use crate::clipboard_text;
|
||||
@@ -915,12 +917,20 @@ impl QueuedSlashCommand {
|
||||
enum QueuedSlashCommandAction {
|
||||
Command(SlashCommand),
|
||||
Fast(QueuedFastCommandAction),
|
||||
ModelSelection(QueuedModelSelection),
|
||||
Plan(UserMessage),
|
||||
Rename(String),
|
||||
Review(ReviewRequest),
|
||||
SandboxReadRoot(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct QueuedModelSelection {
|
||||
model: String,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
scope: ModelSelectionScope,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum QueuedFastCommandAction {
|
||||
On,
|
||||
@@ -4207,11 +4217,17 @@ impl ChatWidget {
|
||||
|
||||
if matches!(key_event.code, KeyCode::Esc)
|
||||
&& matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat)
|
||||
&& !self.pending_steers.is_empty()
|
||||
&& self.bottom_pane.is_task_running()
|
||||
&& self.bottom_pane.no_modal_or_popup_active()
|
||||
&& (self
|
||||
.bottom_pane
|
||||
.composer_text()
|
||||
.lines()
|
||||
.next()
|
||||
.and_then(parse_slash_name)
|
||||
.is_none_or(|(name, _, _)| name != "agent")
|
||||
|| !self.pending_steers.is_empty())
|
||||
{
|
||||
self.submit_pending_steers_after_interrupt = true;
|
||||
self.submit_pending_steers_after_interrupt = !self.pending_steers.is_empty();
|
||||
if !self.submit_op(Op::Interrupt) {
|
||||
self.submit_pending_steers_after_interrupt = false;
|
||||
}
|
||||
@@ -4372,6 +4388,17 @@ impl ChatWidget {
|
||||
|
||||
fn dispatch_command(&mut self, cmd: SlashCommand) {
|
||||
if self.bottom_pane.is_task_running() && !cmd.available_during_task() {
|
||||
match cmd {
|
||||
SlashCommand::Model => {
|
||||
self.open_model_popup();
|
||||
return;
|
||||
}
|
||||
SlashCommand::Review => {
|
||||
self.open_review_popup();
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.queue_slash_command(QueuedSlashCommand {
|
||||
draft: format!("/{}", cmd.command()).into(),
|
||||
action: QueuedSlashCommandAction::Command(cmd),
|
||||
@@ -4950,6 +4977,107 @@ impl ChatWidget {
|
||||
self.refresh_pending_input_preview();
|
||||
}
|
||||
|
||||
fn queued_model_selection_draft(
|
||||
model: &str,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
) -> UserMessage {
|
||||
let mut text = format!("/model {model}");
|
||||
if let Some(effort) = effort {
|
||||
text.push(' ');
|
||||
text.push_str(Self::status_line_reasoning_effort_label(Some(effort)));
|
||||
}
|
||||
text.into()
|
||||
}
|
||||
|
||||
fn queue_model_selection_if_task_running(
|
||||
&mut self,
|
||||
model: String,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
scope: ModelSelectionScope,
|
||||
) -> bool {
|
||||
if !self.bottom_pane.is_task_running() {
|
||||
return false;
|
||||
}
|
||||
let draft = Self::queued_model_selection_draft(&model, effort);
|
||||
self.queue_slash_command(QueuedSlashCommand {
|
||||
draft,
|
||||
action: QueuedSlashCommandAction::ModelSelection(QueuedModelSelection {
|
||||
model,
|
||||
effort,
|
||||
scope,
|
||||
}),
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn apply_or_queue_model_selection(
|
||||
&mut self,
|
||||
model: String,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
scope: ModelSelectionScope,
|
||||
) {
|
||||
if self.queue_model_selection_if_task_running(model.clone(), effort, scope) {
|
||||
return;
|
||||
}
|
||||
match scope {
|
||||
ModelSelectionScope::Global => {
|
||||
self.set_model(&model);
|
||||
self.set_reasoning_effort(effort);
|
||||
self.app_event_tx.send(AppEvent::UpdateModel(model.clone()));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdateReasoningEffort(effort));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistModelSelection { model, effort });
|
||||
}
|
||||
ModelSelectionScope::PlanOnly => {
|
||||
self.set_model(&model);
|
||||
self.set_plan_mode_reasoning_effort(effort);
|
||||
self.app_event_tx.send(AppEvent::UpdateModel(model));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdatePlanModeReasoningEffort(effort));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistPlanModeReasoningEffort(effort));
|
||||
}
|
||||
ModelSelectionScope::AllModes => {
|
||||
self.set_model(&model);
|
||||
self.set_reasoning_effort(effort);
|
||||
self.set_plan_mode_reasoning_effort(effort);
|
||||
self.app_event_tx.send(AppEvent::UpdateModel(model.clone()));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdateReasoningEffort(effort));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdatePlanModeReasoningEffort(effort));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistPlanModeReasoningEffort(effort));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistModelSelection { model, effort });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn queued_review_draft(review_request: &ReviewRequest) -> UserMessage {
|
||||
match &review_request.target {
|
||||
ReviewTarget::UncommittedChanges => "/review current changes".into(),
|
||||
ReviewTarget::BaseBranch { branch } => format!("/review {branch}").into(),
|
||||
ReviewTarget::Commit { sha, title } => {
|
||||
let subject = title.clone().unwrap_or_else(|| sha.clone());
|
||||
format!("/review {subject}").into()
|
||||
}
|
||||
ReviewTarget::Custom { instructions } => format!("/review {instructions}").into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_or_queue_review(&mut self, review_request: ReviewRequest) {
|
||||
if self.bottom_pane.is_task_running() {
|
||||
self.queue_slash_command(QueuedSlashCommand {
|
||||
draft: Self::queued_review_draft(&review_request),
|
||||
action: QueuedSlashCommandAction::Review(review_request),
|
||||
});
|
||||
return;
|
||||
}
|
||||
self.submit_op(Op::Review { review_request });
|
||||
}
|
||||
|
||||
fn submit_plan_user_message(&mut self, user_message: UserMessage) {
|
||||
if self.is_session_configured() {
|
||||
self.reasoning_buffer.clear();
|
||||
@@ -4978,6 +5106,13 @@ impl ChatWidget {
|
||||
self.add_info_message(format!("Fast mode is {status}."), None);
|
||||
}
|
||||
},
|
||||
QueuedSlashCommandAction::ModelSelection(selection) => {
|
||||
self.apply_or_queue_model_selection(
|
||||
selection.model,
|
||||
selection.effort,
|
||||
selection.scope,
|
||||
);
|
||||
}
|
||||
QueuedSlashCommandAction::Plan(user_message) => {
|
||||
self.dispatch_command(SlashCommand::Plan);
|
||||
if self.active_mode_kind() == ModeKind::Plan {
|
||||
@@ -6970,11 +7105,10 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
tx.send(AppEvent::UpdateModel(model_for_action.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort_for_action));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
tx.send(AppEvent::ApplyOrQueueModelSelection {
|
||||
model: model_for_action.clone(),
|
||||
effort: effort_for_action,
|
||||
scope: ModelSelectionScope::Global,
|
||||
});
|
||||
})]
|
||||
}
|
||||
@@ -7042,19 +7176,18 @@ impl ChatWidget {
|
||||
let plan_only_actions: Vec<SelectionAction> = vec![Box::new({
|
||||
let model = model.clone();
|
||||
move |tx| {
|
||||
tx.send(AppEvent::UpdateModel(model.clone()));
|
||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::ApplyOrQueueModelSelection {
|
||||
model: model.clone(),
|
||||
effort,
|
||||
scope: ModelSelectionScope::PlanOnly,
|
||||
});
|
||||
}
|
||||
})];
|
||||
let all_modes_actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::UpdateModel(model.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(effort));
|
||||
tx.send(AppEvent::UpdatePlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::PersistPlanModeReasoningEffort(effort));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
tx.send(AppEvent::ApplyOrQueueModelSelection {
|
||||
model: model.clone(),
|
||||
effort,
|
||||
scope: ModelSelectionScope::AllModes,
|
||||
});
|
||||
})];
|
||||
|
||||
@@ -7143,7 +7276,11 @@ impl ChatWidget {
|
||||
effort: selected_effort,
|
||||
});
|
||||
} else {
|
||||
self.apply_model_and_effort(selected_model, selected_effort);
|
||||
self.apply_or_queue_model_selection(
|
||||
selected_model,
|
||||
selected_effort,
|
||||
ModelSelectionScope::Global,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -7218,11 +7355,10 @@ impl ChatWidget {
|
||||
effort: choice_effort,
|
||||
});
|
||||
} else {
|
||||
tx.send(AppEvent::UpdateModel(model_for_action.clone()));
|
||||
tx.send(AppEvent::UpdateReasoningEffort(choice_effort));
|
||||
tx.send(AppEvent::PersistModelSelection {
|
||||
tx.send(AppEvent::ApplyOrQueueModelSelection {
|
||||
model: model_for_action.clone(),
|
||||
effort: choice_effort,
|
||||
scope: ModelSelectionScope::Global,
|
||||
});
|
||||
}
|
||||
})];
|
||||
@@ -7263,22 +7399,6 @@ impl ChatWidget {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_model_and_effort_without_persist(
|
||||
&self,
|
||||
model: String,
|
||||
effort: Option<ReasoningEffortConfig>,
|
||||
) {
|
||||
self.app_event_tx.send(AppEvent::UpdateModel(model));
|
||||
self.app_event_tx
|
||||
.send(AppEvent::UpdateReasoningEffort(effort));
|
||||
}
|
||||
|
||||
fn apply_model_and_effort(&self, model: String, effort: Option<ReasoningEffortConfig>) {
|
||||
self.apply_model_and_effort_without_persist(model.clone(), effort);
|
||||
self.app_event_tx
|
||||
.send(AppEvent::PersistModelSelection { model, effort });
|
||||
}
|
||||
|
||||
/// Open the permissions popup (alias for /permissions).
|
||||
pub(crate) fn open_approvals_popup(&mut self) {
|
||||
self.open_permissions_popup();
|
||||
@@ -9232,12 +9352,12 @@ impl ChatWidget {
|
||||
items.push(SelectionItem {
|
||||
name: "Review uncommitted changes".to_string(),
|
||||
actions: vec![Box::new(move |tx: &AppEventSender| {
|
||||
tx.send(AppEvent::CodexOp(Op::Review {
|
||||
tx.send(AppEvent::ApplyOrQueueReview {
|
||||
review_request: ReviewRequest {
|
||||
target: ReviewTarget::UncommittedChanges,
|
||||
user_facing_hint: None,
|
||||
},
|
||||
}));
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
@@ -9285,14 +9405,14 @@ impl ChatWidget {
|
||||
items.push(SelectionItem {
|
||||
name: format!("{current_branch} -> {branch}"),
|
||||
actions: vec![Box::new(move |tx3: &AppEventSender| {
|
||||
tx3.send(AppEvent::CodexOp(Op::Review {
|
||||
tx3.send(AppEvent::ApplyOrQueueReview {
|
||||
review_request: ReviewRequest {
|
||||
target: ReviewTarget::BaseBranch {
|
||||
branch: branch.clone(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
}));
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
search_value: Some(option),
|
||||
@@ -9322,7 +9442,7 @@ impl ChatWidget {
|
||||
items.push(SelectionItem {
|
||||
name: subject.clone(),
|
||||
actions: vec![Box::new(move |tx3: &AppEventSender| {
|
||||
tx3.send(AppEvent::CodexOp(Op::Review {
|
||||
tx3.send(AppEvent::ApplyOrQueueReview {
|
||||
review_request: ReviewRequest {
|
||||
target: ReviewTarget::Commit {
|
||||
sha: sha.clone(),
|
||||
@@ -9330,7 +9450,7 @@ impl ChatWidget {
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
}));
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
search_value: Some(search_val),
|
||||
@@ -9359,14 +9479,14 @@ impl ChatWidget {
|
||||
if trimmed.is_empty() {
|
||||
return;
|
||||
}
|
||||
tx.send(AppEvent::CodexOp(Op::Review {
|
||||
tx.send(AppEvent::ApplyOrQueueReview {
|
||||
review_request: ReviewRequest {
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: trimmed,
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
}));
|
||||
});
|
||||
}),
|
||||
);
|
||||
self.bottom_pane.show_view(Box::new(view));
|
||||
@@ -9715,7 +9835,7 @@ pub(crate) fn show_review_commit_picker_with_entries(
|
||||
items.push(SelectionItem {
|
||||
name: subject.clone(),
|
||||
actions: vec![Box::new(move |tx3: &AppEventSender| {
|
||||
tx3.send(AppEvent::CodexOp(Op::Review {
|
||||
tx3.send(AppEvent::ApplyOrQueueReview {
|
||||
review_request: ReviewRequest {
|
||||
target: ReviewTarget::Commit {
|
||||
sha: sha.clone(),
|
||||
@@ -9723,7 +9843,7 @@ pub(crate) fn show_review_commit_picker_with_entries(
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
}));
|
||||
});
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
search_value: Some(search_val),
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
assertion_line: 7753
|
||||
expression: term.backend().vt100().screen().contents()
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
Select Model and Effort
|
||||
Access legacy models by running codex -m <model_name> or in your config.toml
|
||||
|
||||
› 1. gpt-5.3-codex (default) Latest frontier agentic coding model.
|
||||
2. gpt-5.4 Latest frontier agentic coding model.
|
||||
3. gpt-5.2-codex Frontier agentic coding model.
|
||||
4. gpt-5.1-codex-max Codex-optimized flagship for deep and fast
|
||||
reasoning.
|
||||
5. gpt-5.2 Latest frontier model with improvements across
|
||||
knowledge, reasoning and coding
|
||||
6. gpt-5.1-codex-mini Optimized for codex. Cheaper, faster, but less
|
||||
capable.
|
||||
|
||||
Press enter to select reasoning effort, or esc to dismiss.
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: term.backend().vt100().screen().contents()
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
• Working (0s • esc to interrupt)
|
||||
|
||||
• Queued follow-up messages
|
||||
↳ /model
|
||||
⌥ + ↑ edit last queued message
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
? for shortcuts 100% context left
|
||||
@@ -2558,15 +2558,19 @@ async fn reasoning_selection_in_plan_mode_without_effort_change_does_not_open_sc
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::UpdateModel(model) if model == "gpt-5.1-codex-max"
|
||||
AppEvent::ApplyOrQueueModelSelection {
|
||||
model,
|
||||
effort: Some(ReasoningEffortConfig::Medium),
|
||||
scope: ModelSelectionScope::Global,
|
||||
} if model == "gpt-5.1-codex-max"
|
||||
)),
|
||||
"expected model update event; events: {events:?}"
|
||||
"expected model selection event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AppEvent::UpdateReasoningEffort(Some(_)))),
|
||||
"expected reasoning update event; events: {events:?}"
|
||||
.all(|event| !matches!(event, AppEvent::OpenPlanReasoningScopePrompt { .. })),
|
||||
"did not expect plan reasoning scope prompt event; events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2643,15 +2647,19 @@ async fn reasoning_selection_in_plan_mode_model_switch_does_not_open_scope_promp
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::UpdateModel(model) if model == "gpt-5"
|
||||
AppEvent::ApplyOrQueueModelSelection {
|
||||
model,
|
||||
effort: Some(ReasoningEffortConfig::Medium),
|
||||
scope: ModelSelectionScope::Global,
|
||||
} if model == "gpt-5"
|
||||
)),
|
||||
"expected model update event; events: {events:?}"
|
||||
"expected model selection event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|event| matches!(event, AppEvent::UpdateReasoningEffort(Some(_)))),
|
||||
"expected reasoning update event; events: {events:?}"
|
||||
.all(|event| !matches!(event, AppEvent::OpenPlanReasoningScopePrompt { .. })),
|
||||
"did not expect plan reasoning scope prompt event; events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2670,24 +2678,19 @@ async fn plan_reasoning_scope_popup_all_modes_persists_global_and_plan_override(
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::UpdatePlanModeReasoningEffort(Some(ReasoningEffortConfig::High))
|
||||
AppEvent::ApplyOrQueueModelSelection {
|
||||
model,
|
||||
effort: Some(ReasoningEffortConfig::High),
|
||||
scope: ModelSelectionScope::AllModes,
|
||||
} if model == "gpt-5.1-codex-max"
|
||||
)),
|
||||
"expected plan override to be updated; events: {events:?}"
|
||||
"expected all-modes model selection event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::PersistPlanModeReasoningEffort(Some(ReasoningEffortConfig::High))
|
||||
)),
|
||||
"expected updated plan override to be persisted; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::PersistModelSelection { model, effort: Some(ReasoningEffortConfig::High) }
|
||||
if model == "gpt-5.1-codex-max"
|
||||
)),
|
||||
"expected global model reasoning selection persistence; events: {events:?}"
|
||||
events
|
||||
.iter()
|
||||
.all(|event| !matches!(event, AppEvent::PersistPlanModeReasoningEffort(_))),
|
||||
"did not expect persistence events before app handling; events: {events:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2877,9 +2880,13 @@ async fn plan_reasoning_scope_popup_plan_only_does_not_update_all_modes_reasonin
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
AppEvent::UpdatePlanModeReasoningEffort(Some(ReasoningEffortConfig::High))
|
||||
AppEvent::ApplyOrQueueModelSelection {
|
||||
model,
|
||||
effort: Some(ReasoningEffortConfig::High),
|
||||
scope: ModelSelectionScope::PlanOnly,
|
||||
} if model == "gpt-5.1-codex-max"
|
||||
)),
|
||||
"expected plan-only reasoning update; events: {events:?}"
|
||||
"expected plan-only model selection event; events: {events:?}"
|
||||
);
|
||||
assert!(
|
||||
events
|
||||
@@ -6322,7 +6329,7 @@ async fn custom_prompt_submit_sends_review_op() {
|
||||
// Expect AppEvent::CodexOp(Op::Review { .. }) with trimmed prompt
|
||||
let evt = rx.try_recv().expect("expected one app event");
|
||||
match evt {
|
||||
AppEvent::CodexOp(Op::Review { review_request }) => {
|
||||
AppEvent::ApplyOrQueueReview { review_request } => {
|
||||
assert_eq!(
|
||||
review_request,
|
||||
ReviewRequest {
|
||||
@@ -8123,13 +8130,15 @@ async fn user_shell_command_renders_output_not_exploring() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_slash_command_while_task_running_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
async fn model_slash_command_while_task_running_popup_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.1-codex")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.on_task_started();
|
||||
|
||||
chat.dispatch_command(SlashCommand::Model);
|
||||
|
||||
assert_eq!(chat.queued_user_message_texts(), vec!["/model".to_string()]);
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
assert!(chat.has_active_view(), "expected /model popup to open");
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
let width: u16 = 80;
|
||||
@@ -8142,7 +8151,66 @@ async fn queued_slash_command_while_task_running_snapshot() {
|
||||
chat.render(f.area(), f.buffer_mut());
|
||||
})
|
||||
.unwrap();
|
||||
assert_snapshot!(term.backend().vt100().screen().contents());
|
||||
assert_snapshot!(
|
||||
"model_slash_command_while_task_running_popup",
|
||||
term.backend().vt100().screen().contents()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_selection_queues_selected_action_while_task_running() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.1-codex")).await;
|
||||
chat.on_task_started();
|
||||
|
||||
chat.apply_or_queue_model_selection(
|
||||
"gpt-5.1-codex-max".to_string(),
|
||||
Some(ReasoningEffortConfig::High),
|
||||
ModelSelectionScope::Global,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
chat.queued_user_message_texts(),
|
||||
vec!["/model gpt-5.1-codex-max high".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn esc_interrupts_running_task_even_with_model_popup_active() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.1-codex")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.on_task_started();
|
||||
chat.dispatch_command(SlashCommand::Model);
|
||||
|
||||
assert!(chat.has_active_view(), "expected /model popup to open");
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
next_interrupt_op(&mut op_rx);
|
||||
assert!(
|
||||
chat.has_active_view(),
|
||||
"expected popup to remain open during interrupt"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn interrupt_with_popup_active_restores_queued_drafts_into_composer() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.1-codex")).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
chat.on_task_started();
|
||||
chat.queue_user_message("queued draft".into());
|
||||
chat.dispatch_command(SlashCommand::Model);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
next_interrupt_op(&mut op_rx);
|
||||
chat.on_interrupted_turn(TurnAbortReason::Interrupted);
|
||||
|
||||
assert!(
|
||||
chat.has_active_view(),
|
||||
"expected popup to remain open after interrupt"
|
||||
);
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "queued draft");
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
let _ = drain_insert_history(&mut rx);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -11106,23 +11174,15 @@ async fn enter_queues_user_messages_while_review_is_running() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn review_slash_command_queues_while_task_running_and_opens_popup_after_turn_complete() {
|
||||
async fn review_slash_command_opens_popup_while_task_running() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.on_task_started();
|
||||
|
||||
chat.dispatch_command(SlashCommand::Review);
|
||||
|
||||
assert_eq!(
|
||||
chat.queued_user_message_texts(),
|
||||
vec!["/review".to_string()]
|
||||
);
|
||||
assert!(!chat.has_active_view());
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
|
||||
chat.on_task_complete(None, false);
|
||||
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
assert!(chat.has_active_view(), "expected /review popup to open");
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user