[APP-4285] Send slash commands as follow-ups with active AI streams (#9243)
## 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
This commit is contained in:
@@ -599,15 +599,8 @@ impl BlocklistAIController {
|
|||||||
.remove(&conversation_id)
|
.remove(&conversation_id)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let ai_history_model = BlocklistAIHistoryModel::as_ref(ctx);
|
let cancellation_reason =
|
||||||
let active_conversation_id = ai_history_model.active_conversation_id(self.terminal_view_id);
|
self.cancel_active_conversation_for_follow_up(conversation_id, ctx);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(slash_command_request) = SlashCommandRequest::from_query(query.as_str()) {
|
if let Some(slash_command_request) = SlashCommandRequest::from_query(query.as_str()) {
|
||||||
slash_command_request.send_request(self, is_queued_prompt, ctx);
|
slash_command_request.send_request(self, is_queued_prompt, ctx);
|
||||||
@@ -1209,9 +1202,40 @@ impl BlocklistAIController {
|
|||||||
slash_command: SlashCommandRequest,
|
slash_command: SlashCommandRequest,
|
||||||
ctx: &mut ModelContext<Self>,
|
ctx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
|
// 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);
|
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<Self>,
|
||||||
|
) -> 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`
|
/// 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)
|
/// event as a queued prompt submission so UI subscribers (e.g. the input editor)
|
||||||
/// don't clear the input buffer on the auto-send.
|
/// don't clear the input buffer on the auto-send.
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ impl SlashCommandRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn conversation_id(
|
pub(super) fn conversation_id(
|
||||||
&self,
|
&self,
|
||||||
controller: &BlocklistAIController,
|
controller: &BlocklistAIController,
|
||||||
app: &AppContext,
|
app: &AppContext,
|
||||||
|
|||||||
Reference in New Issue
Block a user