From bc3fffa7e57dae20b1878a1aa74e9f003d0617ce Mon Sep 17 00:00:00 2001 From: Varoon Kodithala Date: Tue, 28 Apr 2026 14:51:29 -0400 Subject: [PATCH] [APP-4285] Send slash commands as follow-ups with active AI streams (#9243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Running `/pr-comments` (or any other slash command that targets the currently-selected conversation) while an AI response stream is in-flight crashed in debug builds. `BlocklistAIController::send_request_input` hits its in-flight invariant and fires `safe_assert!(false, ...)` (panics in debug, returns `Err` in release). The user-query path (`send_query`) avoids this because it pre-cancels any active stream on the target conversation with `CancellationReason::FollowUpSubmitted` before calling `send_request_input`. Slash commands bypass that path and dispatch directly via `SlashCommandRequest::send_request`, so the cancel never happens. This PR makes `send_slash_command_request` mirror that cancel-and-resend: if the target conversation has an in-flight stream, cancel it before dispatching. All `SlashCommandRequest` variants are conceptually a fresh user turn (a follow-up), so this matches the semantics users already get from typing a follow-up message. `send_queued_slash_command_request` is unchanged — it's only invoked from `Input::submit_queued_prompt` once the conversation is idle, so no pre-cancel is needed. ## Testing Verified locally. Demo [here](https://www.loom.com/share/a9638ac9a53b48349ce21d03eaba516a)! - Manually reproduced the crash on a dogfood debug build by triggering `/pr-comments` mid-stream; confirmed the panic at the `safe_assert!` in `BlocklistAIController::send_request_input`. - After the fix: same repro cancels the in-flight turn and dispatches `/pr-comments` cleanly. Same behavior verified for `/skills` (InvokeSkill), `/compact` (Summarize), and `/create-environment`. ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode ## Changelog Entries for Stable CHANGELOG-BUG-FIX: Fixed an issue where slash commands sent while an agent was still responding were silently dropped. Now, slash commands like `/pr-comments` run as follow-ups, just like typed messages. --- Run: https://staging.warp.dev/conversation/aeecfa2d-1456-440b-9961-c27295d82531 Plan: https://staging.warp.dev/drive/notebook/Ryj2w6xDSqDwNJYhpwwI0Vly --- app/src/ai/blocklist/controller.rs | 42 +++++++++++++++---- .../ai/blocklist/controller/slash_command.rs | 2 +- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/app/src/ai/blocklist/controller.rs b/app/src/ai/blocklist/controller.rs index eaa3d5f9..75809b95 100644 --- a/app/src/ai/blocklist/controller.rs +++ b/app/src/ai/blocklist/controller.rs @@ -599,15 +599,8 @@ impl BlocklistAIController { .remove(&conversation_id) .unwrap_or_default(); - let ai_history_model = BlocklistAIHistoryModel::as_ref(ctx); - let active_conversation_id = ai_history_model.active_conversation_id(self.terminal_view_id); - let cancellation_reason = CancellationReason::FollowUpSubmitted { - is_for_same_conversation: active_conversation_id - .is_some_and(|id| id == conversation_id), - }; - if let Some(active_conversation_id) = active_conversation_id { - self.cancel_conversation_progress(active_conversation_id, cancellation_reason, ctx); - } + let cancellation_reason = + self.cancel_active_conversation_for_follow_up(conversation_id, ctx); if let Some(slash_command_request) = SlashCommandRequest::from_query(query.as_str()) { slash_command_request.send_request(self, is_queued_prompt, ctx); @@ -1209,9 +1202,40 @@ impl BlocklistAIController { slash_command: SlashCommandRequest, ctx: &mut ModelContext, ) { + // Slash commands are a fresh user turn; mirror `send_query`'s + // cancel-and-resend so we don't trip `send_request_input`'s in-flight + // invariant. + if let Some(conversation_id) = slash_command.conversation_id(self, ctx) { + self.cancel_active_conversation_for_follow_up(conversation_id, ctx); + } slash_command.send_request(self, /*is_queued_prompt*/ false, ctx); } + /// Cancel any in-flight progress on the active conversation in preparation + /// for sending a follow-up turn that will land on `target_conversation_id`. + /// Without this pre-cancel, [`Self::send_request_input`] would trip its + /// in-flight invariant when the new turn re-uses an existing conversation. + /// + /// Returns the [`CancellationReason::FollowUpSubmitted`] reason used so + /// callers can reuse it for downstream side effects (e.g. cancelling + /// pending actions on the target conversation). + fn cancel_active_conversation_for_follow_up( + &mut self, + target_conversation_id: AIConversationId, + ctx: &mut ModelContext, + ) -> CancellationReason { + let active_conversation_id = + BlocklistAIHistoryModel::as_ref(ctx).active_conversation_id(self.terminal_view_id); + let reason = CancellationReason::FollowUpSubmitted { + is_for_same_conversation: active_conversation_id + .is_some_and(|id| id == target_conversation_id), + }; + if let Some(active_conversation_id) = active_conversation_id { + self.cancel_conversation_progress(active_conversation_id, reason, ctx); + } + reason + } + /// Same as [`Self::send_slash_command_request`] but marks the emitted `SentRequest` /// event as a queued prompt submission so UI subscribers (e.g. the input editor) /// don't clear the input buffer on the auto-send. diff --git a/app/src/ai/blocklist/controller/slash_command.rs b/app/src/ai/blocklist/controller/slash_command.rs index 5b1ff5d1..15f6ddc8 100644 --- a/app/src/ai/blocklist/controller/slash_command.rs +++ b/app/src/ai/blocklist/controller/slash_command.rs @@ -168,7 +168,7 @@ impl SlashCommandRequest { } } - fn conversation_id( + pub(super) fn conversation_id( &self, controller: &BlocklistAIController, app: &AppContext,