diff --git a/app/src/ai/blocklist/controller.rs b/app/src/ai/blocklist/controller.rs index 87860d114..6b779f38f 100644 --- a/app/src/ai/blocklist/controller.rs +++ b/app/src/ai/blocklist/controller.rs @@ -1204,6 +1204,25 @@ impl BlocklistAIController { &mut self, ai_input: AIAgentInput, ctx: &mut ModelContext, + ) { + self.send_custom_ai_input_query_internal(ai_input, /*is_queued_prompt*/ false, ctx); + } + + /// Sends a custom [`AIAgentInput`] query that was previously queued behind + /// an in-progress conversation. + pub fn send_queued_custom_ai_input_query( + &mut self, + ai_input: AIAgentInput, + ctx: &mut ModelContext, + ) { + self.send_custom_ai_input_query_internal(ai_input, /*is_queued_prompt*/ true, ctx); + } + + fn send_custom_ai_input_query_internal( + &mut self, + ai_input: AIAgentInput, + is_queued_prompt: bool, + ctx: &mut ModelContext, ) { let participant_id = self.get_sharer_participant_id(); let which_task = match self.context_model.as_ref(ctx).selected_conversation_id(ctx) { @@ -1230,7 +1249,7 @@ impl BlocklistAIController { }, EntrypointType::UserInitiated, participant_id, - /*is_queued_prompt*/ false, + is_queued_prompt, ctx, ) } diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index 2a446e8e5..cf5c4f161 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -21169,6 +21169,16 @@ impl TerminalView { review_comments, }; + if self.should_queue_inline_review(ctx) { + self.send_custom_ai_input_after_next_conversation_finished( + code_review_input, + /* show_close_button */ true, + /* show_send_now_button */ true, + ctx, + ); + return Ok(()); + } + if FeatureFlag::AgentView.is_enabled() && !self.agent_view_controller.as_ref(ctx).is_active() { @@ -21200,6 +21210,35 @@ impl TerminalView { Ok(()) } + /// Returns `true` if a code review submission should be queued instead of sent immediately. + /// This occurs when queue mode is enabled and there's an in-progress or blocked conversation. + pub fn should_queue_inline_review(&self, ctx: &AppContext) -> bool { + if !FeatureFlag::QueueSlashCommand.is_enabled() + || !self + .ai_context_model + .as_ref(ctx) + .is_queue_next_prompt_enabled() + { + return false; + } + + let Some(conversation_id) = self + .ai_context_model + .as_ref(ctx) + .selected_conversation_id(ctx) + else { + return false; + }; + + BlocklistAIHistoryModel::as_ref(ctx) + .conversation(&conversation_id) + .is_some_and(|conversation| { + !conversation.is_empty() + && (conversation.status().is_in_progress() + || conversation.status().is_blocked()) + }) + } + /// Returns the CLI agent currently active in this terminal, if any. pub fn active_cli_agent(&self, ctx: &AppContext) -> Option { if !FeatureFlag::HoaCodeReview.is_enabled() { diff --git a/app/src/terminal/view/pending_user_query.rs b/app/src/terminal/view/pending_user_query.rs index 1d50f3274..6535080e6 100644 --- a/app/src/terminal/view/pending_user_query.rs +++ b/app/src/terminal/view/pending_user_query.rs @@ -3,7 +3,7 @@ use warpui::{SingletonEntity, ViewContext}; use crate::{ ai::{ - agent::{conversation::AIConversationId, CancellationReason}, + agent::{conversation::AIConversationId, AIAgentInput, CancellationReason}, blocklist::block::{FinishReason, PendingUserQueryBlock, PendingUserQueryBlockEvent}, }, auth::AuthStateProvider, @@ -26,12 +26,13 @@ impl TerminalView { /// `show_close_button` controls the dismiss ("X") button; `show_send_now_button` controls /// the "Send now" button that interrupts the active conversation and immediately submits /// the queued prompt. - fn insert_pending_user_query_block( + fn insert_pending_user_query_block_with_send_now( &mut self, prompt: String, show_close_button: bool, show_send_now_button: bool, ctx: &mut ViewContext, + on_send_now: impl Fn(&mut TerminalView, &mut ViewContext) + 'static, ) { self.remove_pending_user_query_block(ctx); let auth_state = AuthStateProvider::as_ref(ctx).get().clone(); @@ -40,7 +41,6 @@ impl TerminalView { .unwrap_or_else(|| "User".to_owned()); let profile_image_path = auth_state.user_photo_url(); - let prompt_for_send_now = prompt.clone(); let handle = ctx.add_typed_action_view(|ctx| { PendingUserQueryBlock::new( prompt, @@ -57,7 +57,7 @@ impl TerminalView { me.remove_pending_user_query_block(ctx); } PendingUserQueryBlockEvent::SendNow => { - me.send_queued_prompt_now(prompt_for_send_now.clone(), ctx); + on_send_now(me, ctx); } }); } @@ -73,6 +73,25 @@ impl TerminalView { self.pending_user_query_view_id = Some(view_id); } + fn insert_pending_user_query_block( + &mut self, + prompt: String, + show_close_button: bool, + show_send_now_button: bool, + ctx: &mut ViewContext, + ) { + let prompt_for_send_now = prompt.clone(); + self.insert_pending_user_query_block_with_send_now( + prompt, + show_close_button, + show_send_now_button, + ctx, + move |terminal_view, ctx| { + terminal_view.send_queued_prompt_now(prompt_for_send_now.clone(), ctx); + }, + ); + } + /// Inserts a pending user query block for a non-oz Cloud Mode run whose harness CLI /// has not yet started. /// The block shows the user's prompt with a "Queued" badge and no buttons: the @@ -138,6 +157,80 @@ impl TerminalView { }); } + fn send_queued_custom_ai_input_now( + &mut self, + ai_input: AIAgentInput, + ctx: &mut ViewContext, + ) { + self.remove_pending_user_query_block(ctx); + if let Some(conversation_id) = self + .ai_context_model + .as_ref(ctx) + .selected_conversation_id(ctx) + { + self.ai_controller.update(ctx, |controller, ctx| { + controller.cancel_conversation_progress( + conversation_id, + CancellationReason::FollowUpSubmitted { + is_for_same_conversation: true, + }, + ctx, + ); + }); + } + + self.ai_controller.update(ctx, move |controller, ctx| { + controller.send_queued_custom_ai_input_query(ai_input, ctx); + }); + } + + /// Queues a structured AI input to be sent after the current conversation finishes. + /// This mirrors [`Self::send_user_query_after_next_conversation_finished`] but preserves + /// non-text inputs such as inline code review comment batches. + pub fn send_custom_ai_input_after_next_conversation_finished( + &mut self, + ai_input: AIAgentInput, + show_close_button: bool, + show_send_now_button: bool, + ctx: &mut ViewContext, + ) { + let prompt = ai_input.user_query().unwrap_or_default(); + if FeatureFlag::PendingUserQueryIndicator.is_enabled() { + let ai_input_for_send_now = ai_input.clone(); + self.insert_pending_user_query_block_with_send_now( + prompt.clone(), + show_close_button, + show_send_now_button, + ctx, + move |terminal_view, ctx| { + terminal_view + .send_queued_custom_ai_input_now(ai_input_for_send_now.clone(), ctx); + }, + ); + } + self.queued_prompt_callback = Some(Box::new(move |terminal_view, reason, ctx| { + if FeatureFlag::PendingUserQueryIndicator.is_enabled() { + terminal_view.remove_pending_user_query_block(ctx); + } + match reason { + FinishReason::Complete => { + terminal_view.ai_controller.update(ctx, move |controller, ctx| { + controller.send_queued_custom_ai_input_query(ai_input, ctx); + }); + } + FinishReason::Error + | FinishReason::Cancelled + | FinishReason::CancelledDuringRequestedCommandExecution => { + terminal_view.input.update(ctx, |input, ctx| { + if input.buffer_text(ctx).is_empty() { + input.replace_buffer_content(&prompt, ctx); + } + }); + } + } + })); + } + /// Shows a pending user query indicator and queues the query to be sent after /// the current conversation finishes. If the conversation completes successfully, /// the queued prompt is re-submitted through the normal input flow (so slash diff --git a/app/src/workspace/view/right_panel.rs b/app/src/workspace/view/right_panel.rs index 6cf4671c9..91be717b8 100644 --- a/app/src/workspace/view/right_panel.rs +++ b/app/src/workspace/view/right_panel.rs @@ -1194,6 +1194,10 @@ impl RightPanelView { /// Routes review comments to the best available terminal. /// Tries the preferred terminal first, then falls back to other terminals /// in the same repo working directory. + /// + /// When queue mode is enabled with an in-progress conversation, code review comments + /// are routed through the inline review path to respect the queueing behavior, + /// regardless of whether a CLI agent is active. fn route_review_comments( &mut self, code_review_view: &ViewHandle, @@ -1232,8 +1236,14 @@ impl RightPanelView { .len(); let active_cli_agent = terminal_view.read(ctx, |t, ctx| t.active_cli_agent(ctx)); - - let (result, destination) = if active_cli_agent.is_some() { + + // Check if code review should be queued instead of sent immediately. + // When queue mode is active with an in-progress conversation, we route through + // send_inline_review which respects the queuing mechanism, regardless of whether + // a CLI agent is active. This ensures queue mode is respected for code review. + let should_queue_review = terminal_view.read(ctx, |t, ctx| t.should_queue_inline_review(ctx)); + + let (result, destination) = if active_cli_agent.is_some() && !should_queue_review { let r = terminal_view.update(ctx, |terminal, ctx| { terminal.send_review_to_cli_agent_or_rich_input(&comments, ctx) });