Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion app/src/ai/agent/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ use crate::ai::agent::linearization::compute_task_depths;
use crate::ai::agent::todos::AIAgentTodoList;
use crate::ai::agent::{
AIAgentOutputMessage, AIAgentOutputMessageType, AIIdentifiers, CancellationReason,
MessageToAIAgentOutputMessageError,
MessageToAIAgentOutputMessageError, SummarizationType,
};
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ai::artifacts::Artifact;
Expand Down Expand Up @@ -613,6 +613,26 @@ impl AIConversation {
self.conversation_usage_metadata.was_summarized
}

/// Returns true if the conversation is currently being summarized.
pub fn is_summarizing(&self) -> bool {
let Some(exchange) = self.latest_visible_exchange() else {
return false;
};
let Some(output) = exchange.output_status.output() else {
return false;
};
output.get().messages.last().is_some_and(|m| {
matches!(
m.message,
AIAgentOutputMessageType::Summarization {
finished_duration: None,
summarization_type: SummarizationType::ConversationSummary,
..
}
)
})
}

pub fn context_window_usage(&self) -> f32 {
self.conversation_usage_metadata.context_window_usage
}
Expand Down
6 changes: 6 additions & 0 deletions app/src/ai/blocklist/queued_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ pub enum QueuedQueryOrigin {
QueueSlashCommand,
/// Filed via the auto-queue toggle in the warping indicator.
AutoQueueToggle,
/// Filed as the follow-up prompt of a `/compact-and <prompt>` slash command, waiting for
/// the summarize to finish.
CompactAndSlashCommand,
/// Filed as the follow-up prompt of a `/fork-and-compact <prompt>` slash command on the
/// forked conversation, waiting for the fork's summarize to finish.
ForkAndCompactSlashCommand,
}

/// A single queued prompt.
Expand Down
4 changes: 4 additions & 0 deletions app/src/server/telemetry/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,8 @@ pub enum TelemetryQueuedQueryOrigin {
InitialCloudMode,
QueueSlashCommand,
AutoQueueToggle,
CompactAndSlashCommand,
ForkAndCompactSlashCommand,
}

impl From<QueuedQueryOrigin> for TelemetryQueuedQueryOrigin {
Expand All @@ -1200,6 +1202,8 @@ impl From<QueuedQueryOrigin> for TelemetryQueuedQueryOrigin {
QueuedQueryOrigin::InitialCloudMode => Self::InitialCloudMode,
QueuedQueryOrigin::QueueSlashCommand => Self::QueueSlashCommand,
QueuedQueryOrigin::AutoQueueToggle => Self::AutoQueueToggle,
QueuedQueryOrigin::CompactAndSlashCommand => Self::CompactAndSlashCommand,
QueuedQueryOrigin::ForkAndCompactSlashCommand => Self::ForkAndCompactSlashCommand,
}
}
}
Expand Down
11 changes: 8 additions & 3 deletions app/src/terminal/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13295,8 +13295,8 @@ impl Input {
}

/// Checks whether the current input should be queued instead of executed.
/// Returns true (and queues the prompt) when the queue-next-prompt toggle is
/// on and the active conversation is still in progress.
/// Returns true (and queues the prompt) when the active conversation is in progress
/// (or blocked) AND either the queue-next-prompt toggle is on or the conversation is summarizing.
/// Only queues when AI input is active — if the user is in shell mode the
/// input is not queued (so e.g. `ls` still runs in the terminal).
fn maybe_queue_input_for_in_progress_conversation(
Expand All @@ -13319,7 +13319,12 @@ impl Input {
return false;
};

if !QueuedQueryModel::as_ref(ctx).is_queue_next_prompt_enabled(conversation_id) {
let is_summarizing = BlocklistAIHistoryModel::as_ref(ctx)
.conversation(&conversation_id)
.is_some_and(|c| c.is_summarizing());
if !QueuedQueryModel::as_ref(ctx).is_queue_next_prompt_enabled(conversation_id)
&& !is_summarizing
{
return false;
}

Expand Down
24 changes: 24 additions & 0 deletions app/src/terminal/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5176,6 +5176,30 @@ impl TerminalView {
self.enqueue_prompt(prompt, QueuedQueryOrigin::InitialCloudMode, ctx)
}

/// Files a follow-up prompt that will run after the next conversation finishes on
/// `conversation_id`. Used by `/compact-and` (targets the active conversation) and
/// `/fork-and-compact` (targets the newly forked conversation, which may differ from the
/// currently selected one). Falls back to the legacy pending-user-query block when
/// `QueuedPromptsV2` is disabled.
pub fn enqueue_followup_prompt(
&mut self,
prompt: String,
origin: QueuedQueryOrigin,
conversation_id: AIConversationId,
ctx: &mut ViewContext<Self>,
) {
if FeatureFlag::QueuedPromptsV2.is_enabled() {
QueuedQueryModel::handle(ctx).update(ctx, |model, ctx| {
model.append(conversation_id, QueuedQuery::new(prompt, origin), ctx);
});
} else {
self.send_user_query_after_next_conversation_finished(
prompt, /* show_close_button */ true, /* show_send_now_button */ false,
ctx,
);
}
}

/// Drains one prompt from the queued-query singleton for `conversation_id` when that
/// conversation finishes.
fn drain_queued_prompts(
Expand Down
135 changes: 135 additions & 0 deletions app/src/terminal/view/queued_prompts_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,141 @@ fn error_or_cancel_drain_leaves_queue_intact_when_input_is_non_empty() {
});
}

#[test]
fn enqueue_followup_prompt_appends_compact_and_row_when_v2_is_enabled() {
// /compact-and follow-ups land in the queue with the CompactAndSlashCommand origin under V2.
App::test((), |mut app| async move {
initialize_app_for_terminal_view(&mut app);
let _agent_view = FeatureFlag::AgentView.override_enabled(true);
let _cloud_mode = FeatureFlag::CloudMode.override_enabled(true);
let _cloud_mode_setup_v2 = FeatureFlag::CloudModeSetupV2.override_enabled(true);
let _queued_prompts_v2 = FeatureFlag::QueuedPromptsV2.override_enabled(true);

let terminal = add_window_with_cloud_mode_terminal(&mut app);
terminal.update(&mut app, |view, ctx| {
let conversation_id = enter_cloud_setup_with_conversation(view, ctx);

view.enqueue_followup_prompt(
"follow up after summarize".to_owned(),
QueuedQueryOrigin::CompactAndSlashCommand,
conversation_id,
ctx,
);

assert_eq!(
queue_texts(view, ctx),
vec![(
"follow up after summarize".to_owned(),
QueuedQueryOrigin::CompactAndSlashCommand
)]
);
assert!(view.pending_user_query_view_id.is_none());
});
});
}

#[test]
fn enqueue_followup_prompt_appends_fork_and_compact_row_when_v2_is_enabled() {
// /fork-and-compact follow-ups land in the queue with the ForkAndCompactSlashCommand origin.
App::test((), |mut app| async move {
initialize_app_for_terminal_view(&mut app);
let _agent_view = FeatureFlag::AgentView.override_enabled(true);
let _cloud_mode = FeatureFlag::CloudMode.override_enabled(true);
let _cloud_mode_setup_v2 = FeatureFlag::CloudModeSetupV2.override_enabled(true);
let _queued_prompts_v2 = FeatureFlag::QueuedPromptsV2.override_enabled(true);

let terminal = add_window_with_cloud_mode_terminal(&mut app);
terminal.update(&mut app, |view, ctx| {
let conversation_id = enter_cloud_setup_with_conversation(view, ctx);

view.enqueue_followup_prompt(
"work on the forked branch".to_owned(),
QueuedQueryOrigin::ForkAndCompactSlashCommand,
conversation_id,
ctx,
);

assert_eq!(
queue_texts(view, ctx),
vec![(
"work on the forked branch".to_owned(),
QueuedQueryOrigin::ForkAndCompactSlashCommand
)]
);
});
});
}

#[test]
fn enqueue_followup_prompt_uses_supplied_conversation_id_when_v2_is_enabled() {
// /fork-and-compact passes the newly forked conversation id directly, which can differ from
// the currently selected conversation. The helper must respect that explicit id.
App::test((), |mut app| async move {
initialize_app_for_terminal_view(&mut app);
let _agent_view = FeatureFlag::AgentView.override_enabled(true);
let _cloud_mode = FeatureFlag::CloudMode.override_enabled(true);
let _cloud_mode_setup_v2 = FeatureFlag::CloudModeSetupV2.override_enabled(true);
let _queued_prompts_v2 = FeatureFlag::QueuedPromptsV2.override_enabled(true);

let terminal = add_window_with_cloud_mode_terminal(&mut app);
terminal.update(&mut app, |view, ctx| {
let selected_conversation_id = enter_cloud_setup_with_conversation(view, ctx);
let other_conversation_id = AIConversationId::new();
assert_ne!(selected_conversation_id, other_conversation_id);

view.enqueue_followup_prompt(
"goes to the forked id".to_owned(),
QueuedQueryOrigin::ForkAndCompactSlashCommand,
other_conversation_id,
ctx,
);

assert!(queue_texts(view, ctx).is_empty());
let other_queue = QueuedQueryModel::as_ref(ctx).queue(other_conversation_id);
assert_eq!(other_queue.len(), 1);
assert_eq!(other_queue[0].text(), "goes to the forked id");
assert_eq!(
other_queue[0].origin(),
QueuedQueryOrigin::ForkAndCompactSlashCommand
);
});
});
}

#[test]
fn enqueue_followup_prompt_falls_back_to_pending_block_when_v2_is_disabled() {
// With V2 off, the helper must call into the legacy send_user_query_after_next_conversation_finished
// path: no row gets appended to the queue model, and the queued_prompt_callback is armed so the
// pending-user-query block lifecycle continues to handle the follow-up exactly as today.
App::test((), |mut app| async move {
initialize_app_for_terminal_view(&mut app);
let _agent_view = FeatureFlag::AgentView.override_enabled(true);
let _cloud_mode = FeatureFlag::CloudMode.override_enabled(true);
let _cloud_mode_setup_v2 = FeatureFlag::CloudModeSetupV2.override_enabled(true);
let _queued_prompts_v2 = FeatureFlag::QueuedPromptsV2.override_enabled(false);
let _pending_user_query_indicator =
FeatureFlag::PendingUserQueryIndicator.override_enabled(true);

let terminal = add_window_with_cloud_mode_terminal(&mut app);
terminal.update(&mut app, |view, ctx| {
let conversation_id = enter_cloud_setup_with_conversation(view, ctx);

view.enqueue_followup_prompt(
"legacy follow up".to_owned(),
QueuedQueryOrigin::CompactAndSlashCommand,
conversation_id,
ctx,
);

assert!(QueuedQueryModel::as_ref(ctx)
.queue(conversation_id)
.is_empty());
assert!(view.queued_prompt_callback.is_some());
assert!(view.pending_user_query_view_id.is_some());
});
});
}

#[test]
fn complete_drain_after_error_drain_continues_with_next_row() {
// After an Error/Cancelled drain pops one row and the user later submits successfully, the
Expand Down
30 changes: 21 additions & 9 deletions app/src/workspace/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ use crate::ai::blocklist::suggested_rule_modal::{
SuggestedRuleAndId, SuggestedRuleModal, SuggestedRuleModalEvent,
};
use crate::ai::blocklist::{
BlocklistAIHistoryEvent, PendingQueryState, SerializedBlockListItem, SlashCommandRequest,
FORK_PREFIX,
BlocklistAIHistoryEvent, PendingQueryState, QueuedQueryOrigin, SerializedBlockListItem,
SlashCommandRequest, FORK_PREFIX,
};
use crate::ai::cloud_agent_settings::CloudAgentSettings;
#[cfg(target_family = "wasm")]
Expand Down Expand Up @@ -12152,10 +12152,10 @@ impl Workspace {
});

if let Some(prompt) = initial_prompt {
terminal_view.send_user_query_after_next_conversation_finished(
terminal_view.enqueue_followup_prompt(
prompt,
/* show_close_button */ true,
/* show_send_now_button */ false,
crate::ai::blocklist::QueuedQueryOrigin::ForkAndCompactSlashCommand,
forked_conversation_id,
terminal_view_ctx,
);
}
Expand Down Expand Up @@ -12249,10 +12249,22 @@ impl Workspace {
});

if let Some(prompt) = initial_prompt {
terminal.send_user_query_after_next_conversation_finished(
prompt, /* show_close_button */ true,
/* show_send_now_button */ false, ctx,
);
// The slash-command handler at
// `app/src/terminal/input/slash_commands/mod.rs` for `/compact-and` short-circuits
// when there is no active conversation, so `selected_conversation_id` is set by the
// time we get here. Skip the follow-up if for some reason that invariant is broken.
if let Some(conversation_id) = terminal
.ai_context_model()
.as_ref(ctx)
.selected_conversation_id(ctx)
{
terminal.enqueue_followup_prompt(
prompt,
QueuedQueryOrigin::CompactAndSlashCommand,
conversation_id,
ctx,
);
}
}
});
}
Expand Down
Loading