mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
tui: resolve interactive slash commands before queueing
Mark popup-opening slash commands as requiring interaction, open them immediately instead of queueing bare drafts while a task is running, and make queued replay restore any legacy bare interactive command draft instead of opening UI mid-drain. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
co-authored by
Codex
parent
8267155494
commit
173d0ef4e3
@@ -4321,12 +4321,13 @@ impl ChatWidget {
|
||||
///
|
||||
/// Live callers usually ignore the return value, but queued replay uses it to decide whether
|
||||
/// draining can continue after this command. `Continue` means the command only changed local
|
||||
/// UI/config state, while `Stop` means it opened UI, submitted work, or otherwise hit a
|
||||
/// boundary that should stop queued draining.
|
||||
/// UI/config state. `Stop` means it submitted or queued work, changed session/navigation
|
||||
/// state, or otherwise hit a boundary where queued draining must stop. Commands that require
|
||||
/// interactive UI are resolved before queueing and should not open that UI during replay.
|
||||
fn dispatch_command(&mut self, cmd: SlashCommand) -> QueueReplayControl {
|
||||
if self.bottom_pane.is_task_running()
|
||||
&& !cmd.available_during_task()
|
||||
&& !matches!(cmd, SlashCommand::Model | SlashCommand::Review)
|
||||
&& !cmd.requires_interaction()
|
||||
{
|
||||
self.queue_user_message(format!("/{}", cmd.command()).into());
|
||||
// This busy-path queueing only happens for live command dispatch. Queued replay
|
||||
@@ -4707,7 +4708,7 @@ impl ChatWidget {
|
||||
let trimmed = args.trim();
|
||||
let should_queue = self.bottom_pane.is_task_running() && !cmd.available_during_task();
|
||||
if trimmed.is_empty() {
|
||||
if should_queue && !matches!(cmd, SlashCommand::Model | SlashCommand::Review) {
|
||||
if should_queue && !cmd.requires_interaction() {
|
||||
self.queue_current_inline_bare_slash_command(cmd);
|
||||
} else {
|
||||
self.dispatch_command(cmd);
|
||||
@@ -5081,6 +5082,13 @@ impl ChatWidget {
|
||||
return QueueReplayControl::Stop;
|
||||
};
|
||||
if rest.trim().is_empty() {
|
||||
if cmd.requires_interaction() {
|
||||
self.add_error_message(format!(
|
||||
"Failed to replay queued slash command requiring interaction: {preview}"
|
||||
));
|
||||
self.restore_user_message_to_composer(draft);
|
||||
return QueueReplayControl::Stop;
|
||||
}
|
||||
let replay_control = self.dispatch_command(cmd);
|
||||
if replay_control == QueueReplayControl::Continue
|
||||
&& self.bottom_pane.no_modal_or_popup_active()
|
||||
|
||||
@@ -8232,6 +8232,18 @@ async fn model_slash_command_while_task_running_opens_popup_snapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn theme_slash_command_while_task_running_opens_popup_instead_of_queueing() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.on_task_started();
|
||||
|
||||
chat.dispatch_command(SlashCommand::Theme);
|
||||
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
assert!(chat.has_active_view(), "expected /theme popup to open");
|
||||
assert!(drain_insert_history(&mut rx).is_empty());
|
||||
}
|
||||
|
||||
#[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;
|
||||
@@ -11570,6 +11582,25 @@ async fn queued_review_selection_replays_after_turn_complete() {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_bare_theme_command_restores_to_composer_instead_of_opening_popup() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
chat.on_task_started();
|
||||
chat.queued_user_messages
|
||||
.push_back(UserMessage::from("/theme".to_string()));
|
||||
|
||||
chat.on_task_complete(None, false);
|
||||
|
||||
assert_eq!(chat.bottom_pane.composer_text(), "/theme");
|
||||
assert!(!chat.has_active_view(), "expected no popup during replay");
|
||||
assert!(chat.queued_user_messages.is_empty());
|
||||
assert_no_submit_op(&mut op_rx);
|
||||
assert!(
|
||||
!drain_insert_history(&mut rx).is_empty(),
|
||||
"expected replay failure to be surfaced to the user"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn queued_custom_review_selection_preserves_branch_like_instructions_after_turn_complete() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
@@ -133,6 +133,29 @@ impl SlashCommand {
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether bare dispatch opens interactive UI that should be resolved before queueing.
|
||||
pub fn requires_interaction(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
SlashCommand::Feedback
|
||||
| SlashCommand::Resume
|
||||
| SlashCommand::Review
|
||||
| SlashCommand::Rename
|
||||
| SlashCommand::Model
|
||||
| SlashCommand::Settings
|
||||
| SlashCommand::Personality
|
||||
| SlashCommand::Collab
|
||||
| SlashCommand::Agent
|
||||
| SlashCommand::MultiAgents
|
||||
| SlashCommand::Approvals
|
||||
| SlashCommand::Permissions
|
||||
| SlashCommand::Experimental
|
||||
| SlashCommand::Skills
|
||||
| SlashCommand::Statusline
|
||||
| SlashCommand::Theme
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this command can be run while a task is in progress.
|
||||
pub fn available_during_task(self) -> bool {
|
||||
match self {
|
||||
|
||||
Reference in New Issue
Block a user