192 lines
8.1 KiB
Rust
192 lines
8.1 KiB
Rust
use galaxy_core::features::FeatureFlag;
|
|
use galaxyui::{SingletonEntity, ViewContext};
|
|
|
|
use crate::{
|
|
ai::{
|
|
agent::{conversation::AIConversationId, CancellationReason},
|
|
blocklist::block::{FinishReason, PendingUserQueryBlock, PendingUserQueryBlockEvent},
|
|
},
|
|
auth::AuthStateProvider,
|
|
terminal::TerminalView,
|
|
};
|
|
|
|
use super::rich_content::RichContentMetadata;
|
|
|
|
impl TerminalView {
|
|
pub(super) fn pending_user_query_conversation_id(&self) -> Option<AIConversationId> {
|
|
let view_id = self.pending_user_query_view_id?;
|
|
self.rich_content_views
|
|
.iter()
|
|
.find(|rich_content| rich_content.view_id() == view_id)
|
|
.and_then(|rich_content| rich_content.agent_view_conversation_id())
|
|
}
|
|
|
|
/// Inserts a pending user query block into the blocklist, showing the user that
|
|
/// a follow-up query is queued and will be sent after the current conversation completes.
|
|
/// `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(
|
|
&mut self,
|
|
prompt: String,
|
|
show_close_button: bool,
|
|
show_send_now_button: bool,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
self.remove_pending_user_query_block(ctx);
|
|
let auth_state = AuthStateProvider::as_ref(ctx).get().clone();
|
|
let user_display_name = auth_state
|
|
.username_for_display()
|
|
.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,
|
|
user_display_name,
|
|
profile_image_path,
|
|
show_close_button,
|
|
show_send_now_button,
|
|
ctx,
|
|
)
|
|
});
|
|
if show_close_button || show_send_now_button {
|
|
ctx.subscribe_to_view(&handle, move |me, _, event, ctx| match event {
|
|
PendingUserQueryBlockEvent::Dismissed => {
|
|
me.remove_pending_user_query_block(ctx);
|
|
}
|
|
PendingUserQueryBlockEvent::SendNow => {
|
|
me.send_queued_prompt_now(prompt_for_send_now.clone(), ctx);
|
|
}
|
|
});
|
|
}
|
|
let view_id = handle.id();
|
|
|
|
self.insert_rich_content(
|
|
None,
|
|
handle,
|
|
Some(RichContentMetadata::PendingUserQuery),
|
|
super::rich_content::RichContentInsertionPosition::PinToBottom,
|
|
ctx,
|
|
);
|
|
self.pending_user_query_view_id = Some(view_id);
|
|
}
|
|
|
|
/// 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
|
|
/// queued state is owned by the run's lifecycle (harness start, failure, cancel,
|
|
/// or auth required), not by a local `/queue`-style callback, so the prompt is not
|
|
/// re-submitted when the block is removed.
|
|
pub(in crate::terminal::view) fn insert_cloud_mode_queued_user_query_block(
|
|
&mut self,
|
|
prompt: String,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
self.insert_pending_user_query_block(
|
|
prompt, /* show_close_button */ false, /* show_send_now_button */ false, ctx,
|
|
);
|
|
}
|
|
|
|
/// Removes the pending user query block, if one exists. No-op if none is present.
|
|
/// Also cancels the queued prompt callback so the prompt is not sent.
|
|
/// (Safe to call from within the callback itself — the caller `.take()`s it first.)
|
|
pub(super) fn remove_pending_user_query_block(&mut self, ctx: &mut ViewContext<Self>) {
|
|
self.queued_prompt_callback = None;
|
|
if let Some(view_id) = self.pending_user_query_view_id.take() {
|
|
self.model
|
|
.lock()
|
|
.block_list_mut()
|
|
.remove_rich_content(view_id);
|
|
self.rich_content_views.retain(|rc| rc.view_id() != view_id);
|
|
ctx.notify();
|
|
}
|
|
}
|
|
|
|
/// Removes the pending block and immediately submits the queued prompt.
|
|
///
|
|
/// The plain-text submission path cancels any in-flight stream itself (via
|
|
/// `send_query` -> `cancel_conversation_progress`), but slash- and skill-command
|
|
/// submissions route through `send_request_input` directly without cancelling,
|
|
/// which trips the in-flight-request assertion when the agent is still streaming.
|
|
///
|
|
/// Cancel the active stream explicitly here so "Send now" works for any prompt type.
|
|
/// Use `FollowUpSubmitted { is_for_same_conversation: true }` so the conversation
|
|
/// status stays `InProgress` across the cancel+resend (see `mark_request_cancelled`
|
|
/// in `conversation.rs`), keeping the warping indicator visible throughout.
|
|
fn send_queued_prompt_now(&mut self, prompt: String, ctx: &mut ViewContext<Self>) {
|
|
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.input.update(ctx, |input, ctx| {
|
|
input.submit_queued_prompt(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
|
|
/// commands, skill commands, and session sharing are all handled correctly).
|
|
/// The pending indicator is removed regardless of the finish reason.
|
|
///
|
|
/// `show_close_button` controls whether a dismiss ("X") button appears on the pending
|
|
/// block. `show_send_now_button` controls whether a "Send now" button appears that
|
|
/// interrupts the active conversation and sends the queued prompt immediately. This
|
|
/// should be false for summarization-triggered queuing (e.g. `/compact-and`).
|
|
pub fn send_user_query_after_next_conversation_finished(
|
|
&mut self,
|
|
prompt: String,
|
|
show_close_button: bool,
|
|
show_send_now_button: bool,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
if FeatureFlag::PendingUserQueryIndicator.is_enabled() {
|
|
self.insert_pending_user_query_block(
|
|
prompt.clone(),
|
|
show_close_button,
|
|
show_send_now_button,
|
|
ctx,
|
|
);
|
|
}
|
|
// Replace any previously queued prompt so the latest one always wins.
|
|
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.input.update(ctx, |input, ctx| {
|
|
input.submit_queued_prompt(prompt, ctx);
|
|
});
|
|
}
|
|
FinishReason::Error
|
|
| FinishReason::Cancelled
|
|
| FinishReason::CancelledDuringRequestedCommandExecution => {
|
|
// Conversation failed or was cancelled — reinsert the pending
|
|
// query into the input so the user doesn't lose it.
|
|
terminal_view.input.update(ctx, |input, ctx| {
|
|
if input.buffer_text(ctx).is_empty() {
|
|
input.replace_buffer_content(&prompt, ctx);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
}
|